实名修改.
This commit is contained in:
parent
4ab2c2b237
commit
af5ceac9a7
|
|
@ -45,7 +45,7 @@
|
|||
<!-- 用户信息卡片 -->
|
||||
<view class="user-info-card" @click="handleViewProfile">
|
||||
<view class="user-avatar">
|
||||
<image :src="targetAvatar || '/static/default-avatar.png'" mode="aspectFill" />
|
||||
<image :src="targetAvatar" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<view class="user-name-row">
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
<image
|
||||
v-if="!message.isMine"
|
||||
class="avatar"
|
||||
:src="targetAvatar || '/static/default-avatar.png'"
|
||||
:src="targetAvatar"
|
||||
mode="aspectFill"
|
||||
@click="handleViewProfile"
|
||||
/>
|
||||
|
|
@ -257,7 +257,7 @@
|
|||
<image
|
||||
v-if="message.isMine"
|
||||
class="avatar"
|
||||
:src="myAvatar || '/static/default-avatar.png'"
|
||||
:src="myAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
|
|
|
|||
|
|
@ -935,11 +935,8 @@ export default {
|
|||
isMember: userStore.isMember
|
||||
})
|
||||
|
||||
// 检查服务号关注弹窗(从其他页面返回时)
|
||||
// 页面栈 > 1 表示是从其他页面返回
|
||||
const pages = getCurrentPages()
|
||||
const isFromOtherPage = pages.length > 1
|
||||
configStore.checkServiceAccountPopup(isFromOtherPage)
|
||||
// 检查服务号关注弹窗
|
||||
configStore.checkServiceAccountPopup()
|
||||
|
||||
// 检查订阅消息提醒
|
||||
configStore.checkSubscribeReminder(userStore.isMember)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@
|
|||
<view class="header-left">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="userDetail.avatar || '/static/default-avatar.png'"
|
||||
:src="userDetail.avatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
|
|
@ -597,6 +597,21 @@ const handleContact = async () => {
|
|||
return
|
||||
}
|
||||
|
||||
// 如果对方已实名,检查当前用户是否也已实名
|
||||
if (userDetail.value?.isRealName && !userStore.isRealName) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '对方已开启实名相亲,请先完成实名认证才能联系',
|
||||
confirmText: '去认证',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({ url: '/pages/realname/index' })
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isUnlocked.value) {
|
||||
// 已解锁,直接跳转聊天页
|
||||
uni.navigateTo({ url: `/pages/chat/index?targetUserId=${userId.value}` })
|
||||
|
|
|
|||
|
|
@ -21,21 +21,22 @@
|
|||
|
||||
<!-- 已认证状态 (Requirements 12.4) -->
|
||||
<view v-if="isVerified" class="verified-section" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
|
||||
<view class="verified-icon">✓</view>
|
||||
<view class="verified-title">实名认证已完成</view>
|
||||
<view class="verified-info">
|
||||
<view class="info-item">
|
||||
<text class="label">姓名:</text>
|
||||
<text class="value">{{ maskedName }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">身份证号:</text>
|
||||
<text class="value">{{ maskedIdNumber }}</text>
|
||||
</view>
|
||||
<!-- 盾牌图标 -->
|
||||
<view class="verified-shield">
|
||||
<image src="/static/ic_real_name.png" mode="widthFix" class="shield-img" />
|
||||
</view>
|
||||
<view class="verified-badge">
|
||||
<text class="badge-icon">🛡️</text>
|
||||
<text class="badge-text">已实名认证</text>
|
||||
|
||||
<!-- 已实名信息卡片 -->
|
||||
<view class="verified-card">
|
||||
<view class="card-title">已实名信息</view>
|
||||
<view class="card-row">
|
||||
<text class="row-label">姓名</text>
|
||||
<text class="row-value">{{ maskedName }}</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="row-label">身份证</text>
|
||||
<text class="row-value">{{ maskedIdNumber }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -44,17 +45,17 @@
|
|||
<!-- 固定的步骤指示器 -->
|
||||
<view class="step-header-fixed" :style="{ top: (statusBarHeight + 44) + 'px' }">
|
||||
<view class="step-indicator">
|
||||
<view class="step-item" :class="{ active: currentStep === 1, completed: currentStep > 1 }" @click="handleStepClick(1)">
|
||||
<view class="step-item" :class="{ active: currentStep === 1, completed: currentStep > 1 }">
|
||||
<view class="step-num">{{ currentStep > 1 ? '✓' : '1' }}</view>
|
||||
<text class="step-text">支付费用</text>
|
||||
</view>
|
||||
<view class="step-line" :class="{ completed: currentStep > 1 }"></view>
|
||||
<view class="step-item" :class="{ active: currentStep === 2, completed: currentStep > 2 }" @click="handleStepClick(2)">
|
||||
<view class="step-item" :class="{ active: currentStep === 2, completed: currentStep > 2 }">
|
||||
<view class="step-num">{{ currentStep > 2 ? '✓' : '2' }}</view>
|
||||
<text class="step-text">上传证件</text>
|
||||
</view>
|
||||
<view class="step-line" :class="{ completed: currentStep > 2 }"></view>
|
||||
<view class="step-item" :class="{ active: currentStep === 3 }" @click="handleStepClick(3)">
|
||||
<view class="step-item" :class="{ active: currentStep === 3 }">
|
||||
<view class="step-num">3</view>
|
||||
<text class="step-text">认证完成</text>
|
||||
</view>
|
||||
|
|
@ -97,7 +98,7 @@
|
|||
|
||||
<!-- 联系客服 -->
|
||||
<view class="contact-service" @click="handleContactService">
|
||||
<image class="service-avatar" src="/static/ic_service.png" mode="aspectFill" />
|
||||
<!-- <image class="service-avatar" src="/static/ic_service.png" mode="aspectFill" /> -->
|
||||
<text class="service-text">联系客服</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -551,13 +552,6 @@
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击步骤指示器跳转(测试用)
|
||||
*/
|
||||
const handleStepClick = (step) => {
|
||||
currentStep.value = step
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initPage()
|
||||
})
|
||||
|
|
@ -581,7 +575,6 @@
|
|||
handleGoMember,
|
||||
handleNextStep,
|
||||
handleContactService,
|
||||
handleStepClick,
|
||||
handleSubmitVerification,
|
||||
handleDone
|
||||
}
|
||||
|
|
@ -591,9 +584,8 @@
|
|||
userStore.restoreFromStorage()
|
||||
|
||||
// 如果已登录,重新获取实名认证状态(用户可能从会员页面返回,已开通会员)
|
||||
const { getToken } = require('@/utils/storage.js')
|
||||
if (getToken()) {
|
||||
const { get } = require('@/api/request.js')
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
try {
|
||||
const res = await get('/realname/status')
|
||||
if (res && (res.success || res.code === 0) && res.data) {
|
||||
|
|
@ -675,81 +667,62 @@
|
|||
|
||||
// 已认证状态
|
||||
.verified-section {
|
||||
padding: 60rpx 30rpx;
|
||||
padding-top: 60rpx;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 40rpx 30rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #f8f8f8;
|
||||
overflow: hidden;
|
||||
|
||||
.verified-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 60rpx;
|
||||
color: #fff;
|
||||
margin-bottom: 30rpx;
|
||||
.verified-shield {
|
||||
margin-top: 60rpx;
|
||||
margin-bottom: 60rpx;
|
||||
|
||||
.shield-img {
|
||||
width: 400rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.verified-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.verified-info {
|
||||
.verified-card {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx;
|
||||
|
||||
.info-item {
|
||||
.card-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
.row-label {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
width: 160rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
.row-value {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.verified-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 32rpx;
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||
border-radius: 40rpx;
|
||||
|
||||
.badge-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 26rpx;
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 未认证状态
|
||||
|
|
@ -979,7 +952,7 @@
|
|||
.contact-service {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
top: 380rpx;
|
||||
top: 480rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -233,12 +233,11 @@ export const useConfigStore = defineStore('config', {
|
|||
* 检查是否应该显示服务号关注弹窗
|
||||
* 条件:
|
||||
* 1. 用户未关注服务号(从后端获取真实状态)
|
||||
* 2. 从其他页面返回首页时
|
||||
* 2. 弹窗已启用
|
||||
* 3. 弹出后5分钟内不再弹出
|
||||
* 4. 一天最多弹出3次
|
||||
* @param {boolean} isFromOtherPage - 是否从其他页面返回
|
||||
*/
|
||||
checkServiceAccountPopup(isFromOtherPage = false) {
|
||||
checkServiceAccountPopup() {
|
||||
// 从 userStore 获取真实的关注状态
|
||||
const userStore = useUserStore()
|
||||
|
||||
|
|
@ -248,18 +247,12 @@ export const useConfigStore = defineStore('config', {
|
|||
return
|
||||
}
|
||||
|
||||
// 如果没有配置服务号弹窗,不显示
|
||||
// 如果没有配置服务号弹窗或未启用,不显示
|
||||
if (!this.serviceAccountPopup || this.serviceAccountPopup.status !== 1) {
|
||||
this.showServiceAccountPopup = false
|
||||
return
|
||||
}
|
||||
|
||||
// 如果不是从其他页面返回,不显示
|
||||
if (!isFromOtherPage) {
|
||||
this.showServiceAccountPopup = false
|
||||
return
|
||||
}
|
||||
|
||||
const today = getTodayDateString()
|
||||
const now = Date.now()
|
||||
|
||||
|
|
|
|||
|
|
@ -100,12 +100,11 @@
|
|||
"Region": "ap-shanghai"
|
||||
},
|
||||
"RealName": {
|
||||
"Provider": "Tencent",
|
||||
"Tencent": {
|
||||
"SecretId": "AKIDVyMfzKZdZP8zkNyOdsFuSsBJDB7EScs0",
|
||||
"SecretKey": "89GWr7JPWYTL8ueHlAYowGZnvzKZjqs9",
|
||||
"Region": "ap-shanghai",
|
||||
"SimilarityThreshold": 80
|
||||
"Provider": "Aliyun",
|
||||
"Aliyun": {
|
||||
"AccessKeyId": "LTAI5tL1E4EDWRsDfwfLLzZK",
|
||||
"AccessKeySecret": "hWVR4ifecKNyfRqRWNW0QcX22Sppry",
|
||||
"RegionId": "cn-shanghai"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ public static class InfrastructureExtensions
|
|||
services.Configure<SmsOptions>(configuration.GetSection("Sms"));
|
||||
services.AddScoped<ISmsProvider, AliyunSmsProvider>();
|
||||
|
||||
// RealName
|
||||
// RealName - 阿里云实名认证
|
||||
services.Configure<RealNameOptions>(configuration.GetSection("RealName"));
|
||||
services.AddScoped<IRealNameProvider, TencentRealNameProvider>();
|
||||
services.AddScoped<IRealNameProvider, AliyunRealNameProvider>();
|
||||
|
||||
// WeChat
|
||||
services.Configure<WeChatOptions>(configuration.GetSection("WeChat"));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace XiangYi.Infrastructure.RealName;
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云实名认证服务实现
|
||||
/// 使用阿里云身份证二要素核验API(纯服务端接入)
|
||||
/// </summary>
|
||||
public class AliyunRealNameProvider : IRealNameProvider
|
||||
{
|
||||
private readonly AliyunRealNameOptions _options;
|
||||
private readonly ILogger<AliyunRealNameProvider> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
// 身份证二要素核验API
|
||||
private const string IdCardVerifyHost = "cloudauth.aliyuncs.com";
|
||||
private const string ApiVersion = "2019-03-07";
|
||||
|
||||
public AliyunRealNameProvider(
|
||||
IOptions<RealNameOptions> options,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<AliyunRealNameProvider> logger)
|
||||
{
|
||||
_options = options.Value.Aliyun ?? new AliyunRealNameOptions();
|
||||
_logger = logger;
|
||||
_httpClient = httpClientFactory.CreateClient("AliyunRealName");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证二要素验证(姓名+身份证号)
|
||||
/// </summary>
|
||||
public async Task<RealNameResult> VerifyIdCardAsync(string name, string idCard)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parameters = new SortedDictionary<string, string>
|
||||
{
|
||||
{ "Action", "Id2MetaVerify" },
|
||||
{ "Version", ApiVersion },
|
||||
{ "Format", "JSON" },
|
||||
{ "AccessKeyId", _options.AccessKeyId },
|
||||
{ "SignatureMethod", "HMAC-SHA1" },
|
||||
{ "Timestamp", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") },
|
||||
{ "SignatureVersion", "1.0" },
|
||||
{ "SignatureNonce", Guid.NewGuid().ToString("N") },
|
||||
{ "ParamType", "normal" },
|
||||
{ "IdentifyNum", idCard },
|
||||
{ "UserName", name }
|
||||
};
|
||||
|
||||
var signature = GenerateSignature(parameters);
|
||||
parameters.Add("Signature", signature);
|
||||
|
||||
var queryString = string.Join("&", parameters.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));
|
||||
var requestUrl = $"https://{IdCardVerifyHost}/?{queryString}";
|
||||
|
||||
_logger.LogInformation("阿里云实名认证请求: {Url}", requestUrl.Replace(_options.AccessKeyId, "***"));
|
||||
|
||||
var response = await _httpClient.GetAsync(requestUrl);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
_logger.LogInformation("阿里云实名认证响应: {Response}", content);
|
||||
|
||||
var result = JsonSerializer.Deserialize<AliyunIdVerifyResponse>(content, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (result?.Code == "200" && result.ResultObject?.BizCode == "1")
|
||||
{
|
||||
_logger.LogInformation("阿里云身份证二要素验证通过: {IdCard}", MaskIdCard(idCard));
|
||||
return RealNameResult.Success(result.RequestId);
|
||||
}
|
||||
|
||||
var errorMsg = GetErrorMessage(result?.ResultObject?.BizCode, result?.Message);
|
||||
_logger.LogWarning("阿里云身份证二要素验证失败: {IdCard}, BizCode: {BizCode}, Error: {Error}",
|
||||
MaskIdCard(idCard), result?.ResultObject?.BizCode, errorMsg);
|
||||
|
||||
return RealNameResult.Fail(
|
||||
result?.ResultObject?.BizCode ?? result?.Code ?? "UNKNOWN",
|
||||
errorMsg,
|
||||
result?.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "阿里云身份证二要素验证异常: {IdCard}", MaskIdCard(idCard));
|
||||
return RealNameResult.Fail("EXCEPTION", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证三要素验证(阿里云纯API模式不支持,返回失败)
|
||||
/// </summary>
|
||||
public Task<RealNameResult> VerifyWithPhotoAsync(string name, string idCard, string photoBase64)
|
||||
{
|
||||
_logger.LogWarning("阿里云纯API模式不支持身份证三要素验证(人脸比对)");
|
||||
return Task.FromResult(RealNameResult.Fail("NOT_SUPPORTED", "当前配置不支持人脸比对验证,请使用二要素验证"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别(正面)- 阿里云纯API模式不支持
|
||||
/// </summary>
|
||||
public Task<IdCardOcrResult> OcrIdCardFrontAsync(string imageBase64)
|
||||
{
|
||||
_logger.LogWarning("阿里云纯API模式不支持身份证OCR识别");
|
||||
return Task.FromResult(IdCardOcrResult.Fail("NOT_SUPPORTED", "当前配置不支持身份证OCR识别,请手动输入身份信息"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别(反面)- 阿里云纯API模式不支持
|
||||
/// </summary>
|
||||
public Task<IdCardOcrResult> OcrIdCardBackAsync(string imageBase64)
|
||||
{
|
||||
_logger.LogWarning("阿里云纯API模式不支持身份证OCR识别");
|
||||
return Task.FromResult(IdCardOcrResult.Fail("NOT_SUPPORTED", "当前配置不支持身份证OCR识别,请手动输入身份信息"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成阿里云API签名
|
||||
/// </summary>
|
||||
private string GenerateSignature(SortedDictionary<string, string> parameters)
|
||||
{
|
||||
var canonicalizedQueryString = string.Join("&",
|
||||
parameters.Select(p => $"{PercentEncode(p.Key)}={PercentEncode(p.Value)}"));
|
||||
|
||||
var stringToSign = $"GET&%2F&{PercentEncode(canonicalizedQueryString)}";
|
||||
|
||||
using var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_options.AccessKeySecret + "&"));
|
||||
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
|
||||
return Convert.ToBase64String(hashBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// URL编码(阿里云特殊规则)
|
||||
/// </summary>
|
||||
private static string PercentEncode(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return string.Empty;
|
||||
|
||||
var encoded = Uri.EscapeDataString(value);
|
||||
// 阿里云特殊编码规则
|
||||
encoded = encoded.Replace("+", "%20")
|
||||
.Replace("*", "%2A")
|
||||
.Replace("%7E", "~");
|
||||
return encoded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取错误信息
|
||||
/// </summary>
|
||||
private static string GetErrorMessage(string? bizCode, string? message)
|
||||
{
|
||||
return bizCode switch
|
||||
{
|
||||
"1" => "验证通过",
|
||||
"2" => "姓名与身份证号不一致",
|
||||
"3" => "身份证号不存在",
|
||||
"4" => "身份证号格式错误",
|
||||
"5" => "服务异常,请稍后重试",
|
||||
_ => message ?? "验证失败,请检查身份信息是否正确"
|
||||
};
|
||||
}
|
||||
|
||||
private static string MaskIdCard(string idCard)
|
||||
{
|
||||
if (string.IsNullOrEmpty(idCard) || idCard.Length < 10)
|
||||
return "***";
|
||||
return $"{idCard[..4]}**********{idCard[^4..]}";
|
||||
}
|
||||
|
||||
#region 响应模型
|
||||
|
||||
private class AliyunIdVerifyResponse
|
||||
{
|
||||
public string? Code { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? RequestId { get; set; }
|
||||
public AliyunIdVerifyResultObject? ResultObject { get; set; }
|
||||
}
|
||||
|
||||
private class AliyunIdVerifyResultObject
|
||||
{
|
||||
public string? BizCode { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -11,53 +11,33 @@ public class RealNameOptions
|
|||
public const string SectionName = "RealName";
|
||||
|
||||
/// <summary>
|
||||
/// 实名认证提供商类型:Tencent, Aliyun
|
||||
/// 实名认证提供商类型:Aliyun
|
||||
/// </summary>
|
||||
public string Provider { get; set; } = "Tencent";
|
||||
public string Provider { get; set; } = "Aliyun";
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云实名认证配置
|
||||
/// 阿里云实名认证配置
|
||||
/// </summary>
|
||||
public TencentRealNameOptions Tencent { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云实名认证配置(预留)
|
||||
/// </summary>
|
||||
public AliyunRealNameOptions? Aliyun { get; set; }
|
||||
public AliyunRealNameOptions Aliyun { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云实名认证配置
|
||||
/// </summary>
|
||||
public class TencentRealNameOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// SecretId
|
||||
/// </summary>
|
||||
public string SecretId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// SecretKey
|
||||
/// </summary>
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 区域
|
||||
/// </summary>
|
||||
public string Region { get; set; } = "ap-guangzhou";
|
||||
|
||||
/// <summary>
|
||||
/// 人脸比对相似度阈值(0-100)
|
||||
/// </summary>
|
||||
public decimal SimilarityThreshold { get; set; } = 80;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云实名认证配置(预留)
|
||||
/// 阿里云实名认证配置
|
||||
/// </summary>
|
||||
public class AliyunRealNameOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// AccessKeyId
|
||||
/// </summary>
|
||||
public string AccessKeyId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// AccessKeySecret
|
||||
/// </summary>
|
||||
public string AccessKeySecret { get; set; } = string.Empty;
|
||||
public string AppCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 区域ID(默认cn-shanghai)
|
||||
/// </summary>
|
||||
public string RegionId { get; set; } = "cn-shanghai";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,397 +0,0 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace XiangYi.Infrastructure.RealName;
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云实名认证服务实现
|
||||
/// 使用腾讯云人脸核身API
|
||||
/// </summary>
|
||||
public class TencentRealNameProvider : IRealNameProvider
|
||||
{
|
||||
private readonly TencentRealNameOptions _options;
|
||||
private readonly ILogger<TencentRealNameProvider> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private const string ServiceHost = "faceid.tencentcloudapi.com";
|
||||
private const string ServiceName = "faceid";
|
||||
private const string ApiVersion = "2018-03-01";
|
||||
|
||||
public TencentRealNameProvider(
|
||||
IOptions<RealNameOptions> options,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<TencentRealNameProvider> logger)
|
||||
{
|
||||
_options = options.Value.Tencent;
|
||||
_logger = logger;
|
||||
_httpClient = httpClientFactory.CreateClient("TencentRealName");
|
||||
}
|
||||
|
||||
public async Task<RealNameResult> VerifyIdCardAsync(string name, string idCard)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
IdCard = idCard,
|
||||
Name = name
|
||||
};
|
||||
|
||||
var response = await CallApiAsync("IdCardVerification", requestBody);
|
||||
|
||||
if (response?.Response?.Result == "0")
|
||||
{
|
||||
_logger.LogInformation("身份证二要素验证通过: {IdCard}", MaskIdCard(idCard));
|
||||
return RealNameResult.Success(response.Response.RequestId);
|
||||
}
|
||||
|
||||
var errorMsg = response?.Response?.Description ?? "验证失败";
|
||||
_logger.LogWarning("身份证二要素验证失败: {IdCard}, Error: {Error}",
|
||||
MaskIdCard(idCard), errorMsg);
|
||||
|
||||
return RealNameResult.Fail(
|
||||
response?.Response?.Result ?? "UNKNOWN",
|
||||
errorMsg,
|
||||
response?.Response?.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "身份证二要素验证异常: {IdCard}", MaskIdCard(idCard));
|
||||
return RealNameResult.Fail("EXCEPTION", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<RealNameResult> VerifyWithPhotoAsync(string name, string idCard, string photoBase64)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
IdCard = idCard,
|
||||
Name = name,
|
||||
ImageBase64 = photoBase64
|
||||
};
|
||||
|
||||
var response = await CallApiAsync("IdCardOCRVerification", requestBody);
|
||||
|
||||
if (response?.Response?.Result == "0")
|
||||
{
|
||||
var score = decimal.TryParse(response.Response.Sim, out var s) ? s : 0;
|
||||
|
||||
if (score >= _options.SimilarityThreshold)
|
||||
{
|
||||
_logger.LogInformation("身份证三要素验证通过: {IdCard}, Score: {Score}",
|
||||
MaskIdCard(idCard), score);
|
||||
return RealNameResult.Success(response.Response.RequestId, score);
|
||||
}
|
||||
|
||||
_logger.LogWarning("身份证三要素验证相似度不足: {IdCard}, Score: {Score}, Threshold: {Threshold}",
|
||||
MaskIdCard(idCard), score, _options.SimilarityThreshold);
|
||||
|
||||
return RealNameResult.Fail(
|
||||
"LOW_SIMILARITY",
|
||||
$"人脸相似度不足,当前: {score},要求: {_options.SimilarityThreshold}",
|
||||
response.Response.RequestId);
|
||||
}
|
||||
|
||||
var errorMsg = response?.Response?.Description ?? "验证失败";
|
||||
_logger.LogWarning("身份证三要素验证失败: {IdCard}, Error: {Error}",
|
||||
MaskIdCard(idCard), errorMsg);
|
||||
|
||||
return RealNameResult.Fail(
|
||||
response?.Response?.Result ?? "UNKNOWN",
|
||||
errorMsg,
|
||||
response?.Response?.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "身份证三要素验证异常: {IdCard}", MaskIdCard(idCard));
|
||||
return RealNameResult.Fail("EXCEPTION", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别(正面)
|
||||
/// </summary>
|
||||
public async Task<IdCardOcrResult> OcrIdCardFrontAsync(string imageBase64)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
ImageBase64 = imageBase64,
|
||||
CardSide = "FRONT"
|
||||
};
|
||||
|
||||
var response = await CallOcrApiAsync("IDCardOCR", requestBody);
|
||||
|
||||
if (response?.Response?.Error != null)
|
||||
{
|
||||
_logger.LogWarning("身份证正面OCR识别失败: {Error}", response.Response.Error.Message);
|
||||
return IdCardOcrResult.Fail(
|
||||
response.Response.Error.Code ?? "UNKNOWN",
|
||||
response.Response.Error.Message ?? "识别失败",
|
||||
response.Response.RequestId);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(response?.Response?.Name) || string.IsNullOrEmpty(response?.Response?.IdNum))
|
||||
{
|
||||
_logger.LogWarning("身份证正面OCR识别结果为空");
|
||||
return IdCardOcrResult.Fail("EMPTY_RESULT", "未能识别到身份证信息,请重新拍照");
|
||||
}
|
||||
|
||||
_logger.LogInformation("身份证正面OCR识别成功: {Name}", MaskName(response.Response.Name));
|
||||
return IdCardOcrResult.SuccessFront(
|
||||
response.Response.Name,
|
||||
response.Response.IdNum,
|
||||
response.Response.Sex,
|
||||
response.Response.Nation,
|
||||
response.Response.Birth,
|
||||
response.Response.Address,
|
||||
response.Response.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "身份证正面OCR识别异常");
|
||||
return IdCardOcrResult.Fail("EXCEPTION", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别(反面)
|
||||
/// </summary>
|
||||
public async Task<IdCardOcrResult> OcrIdCardBackAsync(string imageBase64)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
ImageBase64 = imageBase64,
|
||||
CardSide = "BACK"
|
||||
};
|
||||
|
||||
var response = await CallOcrApiAsync("IDCardOCR", requestBody);
|
||||
|
||||
if (response?.Response?.Error != null)
|
||||
{
|
||||
_logger.LogWarning("身份证反面OCR识别失败: {Error}", response.Response.Error.Message);
|
||||
return IdCardOcrResult.Fail(
|
||||
response.Response.Error.Code ?? "UNKNOWN",
|
||||
response.Response.Error.Message ?? "识别失败",
|
||||
response.Response.RequestId);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(response?.Response?.Authority))
|
||||
{
|
||||
_logger.LogWarning("身份证反面OCR识别结果为空");
|
||||
return IdCardOcrResult.Fail("EMPTY_RESULT", "未能识别到身份证信息,请重新拍照");
|
||||
}
|
||||
|
||||
_logger.LogInformation("身份证反面OCR识别成功");
|
||||
return IdCardOcrResult.SuccessBack(
|
||||
response.Response.Authority,
|
||||
response.Response.ValidDate ?? "",
|
||||
response.Response.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "身份证反面OCR识别异常");
|
||||
return IdCardOcrResult.Fail("EXCEPTION", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TencentOcrApiResponse?> CallOcrApiAsync(string action, object requestBody)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var date = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
var payload = JsonSerializer.Serialize(requestBody);
|
||||
|
||||
// OCR使用不同的服务域名
|
||||
var ocrHost = "ocr.tencentcloudapi.com";
|
||||
var ocrService = "ocr";
|
||||
var authorization = GenerateAuthorizationForOcr(action, timestamp, date, payload, ocrHost, ocrService);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"https://{ocrHost}/")
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
request.Headers.Add("Authorization", authorization);
|
||||
request.Headers.Add("X-TC-Action", action);
|
||||
request.Headers.Add("X-TC-Version", "2018-11-19"); // OCR API版本
|
||||
request.Headers.Add("X-TC-Timestamp", timestamp.ToString());
|
||||
request.Headers.Add("X-TC-Region", _options.Region);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonSerializer.Deserialize<TencentOcrApiResponse>(content, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
}
|
||||
|
||||
private string GenerateAuthorizationForOcr(string action, long timestamp, string date, string payload, string host, string service)
|
||||
{
|
||||
var algorithm = "TC3-HMAC-SHA256";
|
||||
var httpRequestMethod = "POST";
|
||||
var canonicalUri = "/";
|
||||
var canonicalQueryString = "";
|
||||
var canonicalHeaders = $"content-type:application/json; charset=utf-8\nhost:{host}\n";
|
||||
var signedHeaders = "content-type;host";
|
||||
var hashedRequestPayload = Sha256Hex(payload);
|
||||
|
||||
var canonicalRequest = $"{httpRequestMethod}\n{canonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
|
||||
|
||||
var credentialScope = $"{date}/{service}/tc3_request";
|
||||
var hashedCanonicalRequest = Sha256Hex(canonicalRequest);
|
||||
var stringToSign = $"{algorithm}\n{timestamp}\n{credentialScope}\n{hashedCanonicalRequest}";
|
||||
|
||||
var secretDate = HmacSha256($"TC3{_options.SecretKey}", date);
|
||||
var secretService = HmacSha256(secretDate, service);
|
||||
var secretSigning = HmacSha256(secretService, "tc3_request");
|
||||
var signature = HmacSha256Hex(secretSigning, stringToSign);
|
||||
|
||||
return $"{algorithm} Credential={_options.SecretId}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}";
|
||||
}
|
||||
|
||||
private static string MaskName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name) || name.Length < 2)
|
||||
return "***";
|
||||
return $"{name[0]}**";
|
||||
}
|
||||
|
||||
private async Task<TencentApiResponse?> CallApiAsync(string action, object requestBody)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var date = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
var payload = JsonSerializer.Serialize(requestBody);
|
||||
|
||||
var authorization = GenerateAuthorization(action, timestamp, date, payload);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"https://{ServiceHost}/")
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
request.Headers.Add("Authorization", authorization);
|
||||
request.Headers.Add("X-TC-Action", action);
|
||||
request.Headers.Add("X-TC-Version", ApiVersion);
|
||||
request.Headers.Add("X-TC-Timestamp", timestamp.ToString());
|
||||
request.Headers.Add("X-TC-Region", _options.Region);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonSerializer.Deserialize<TencentApiResponse>(content, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
}
|
||||
|
||||
private string GenerateAuthorization(string action, long timestamp, string date, string payload)
|
||||
{
|
||||
var algorithm = "TC3-HMAC-SHA256";
|
||||
var httpRequestMethod = "POST";
|
||||
var canonicalUri = "/";
|
||||
var canonicalQueryString = "";
|
||||
var canonicalHeaders = $"content-type:application/json; charset=utf-8\nhost:{ServiceHost}\n";
|
||||
var signedHeaders = "content-type;host";
|
||||
var hashedRequestPayload = Sha256Hex(payload);
|
||||
|
||||
var canonicalRequest = $"{httpRequestMethod}\n{canonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
|
||||
|
||||
var credentialScope = $"{date}/{ServiceName}/tc3_request";
|
||||
var hashedCanonicalRequest = Sha256Hex(canonicalRequest);
|
||||
var stringToSign = $"{algorithm}\n{timestamp}\n{credentialScope}\n{hashedCanonicalRequest}";
|
||||
|
||||
var secretDate = HmacSha256($"TC3{_options.SecretKey}", date);
|
||||
var secretService = HmacSha256(secretDate, ServiceName);
|
||||
var secretSigning = HmacSha256(secretService, "tc3_request");
|
||||
var signature = HmacSha256Hex(secretSigning, stringToSign);
|
||||
|
||||
return $"{algorithm} Credential={_options.SecretId}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}";
|
||||
}
|
||||
|
||||
private static string Sha256Hex(string input)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(bytes).ToLower();
|
||||
}
|
||||
|
||||
private static byte[] HmacSha256(string key, string data)
|
||||
{
|
||||
return HmacSha256(Encoding.UTF8.GetBytes(key), data);
|
||||
}
|
||||
|
||||
private static byte[] HmacSha256(byte[] key, string data)
|
||||
{
|
||||
using var hmac = new HMACSHA256(key);
|
||||
return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
private static string HmacSha256Hex(byte[] key, string data)
|
||||
{
|
||||
return Convert.ToHexString(HmacSha256(key, data)).ToLower();
|
||||
}
|
||||
|
||||
private static string MaskIdCard(string idCard)
|
||||
{
|
||||
if (string.IsNullOrEmpty(idCard) || idCard.Length < 10)
|
||||
return "***";
|
||||
return $"{idCard[..4]}**********{idCard[^4..]}";
|
||||
}
|
||||
|
||||
private class TencentApiResponse
|
||||
{
|
||||
public TencentApiResponseData? Response { get; set; }
|
||||
}
|
||||
|
||||
private class TencentApiResponseData
|
||||
{
|
||||
public string? Result { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Sim { get; set; }
|
||||
public string? RequestId { get; set; }
|
||||
public TencentApiError? Error { get; set; }
|
||||
}
|
||||
|
||||
private class TencentApiError
|
||||
{
|
||||
public string? Code { get; set; }
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云OCR API响应
|
||||
/// </summary>
|
||||
private class TencentOcrApiResponse
|
||||
{
|
||||
public TencentOcrResponseData? Response { get; set; }
|
||||
}
|
||||
|
||||
private class TencentOcrResponseData
|
||||
{
|
||||
// 正面信息
|
||||
public string? Name { get; set; }
|
||||
public string? Sex { get; set; }
|
||||
public string? Nation { get; set; }
|
||||
public string? Birth { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? IdNum { get; set; }
|
||||
|
||||
// 反面信息
|
||||
public string? Authority { get; set; }
|
||||
public string? ValidDate { get; set; }
|
||||
|
||||
public string? RequestId { get; set; }
|
||||
public TencentApiError? Error { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user