实名修改.

This commit is contained in:
18631081161 2026-01-28 23:59:08 +08:00
parent 4ab2c2b237
commit af5ceac9a7
10 changed files with 294 additions and 542 deletions

View File

@ -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>

View File

@ -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)

View File

@ -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}` })

View File

@ -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;

View File

@ -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()

View File

@ -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"
}
}
}

View File

@ -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"));

View File

@ -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
}

View File

@ -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";
}

View File

@ -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; }
}
}