diff --git a/miniapp/pages/chat/index.vue b/miniapp/pages/chat/index.vue index 9fe1f08..508c4ba 100644 --- a/miniapp/pages/chat/index.vue +++ b/miniapp/pages/chat/index.vue @@ -45,7 +45,7 @@ - + @@ -199,7 +199,7 @@ @@ -257,7 +257,7 @@ diff --git a/miniapp/pages/index/index.vue b/miniapp/pages/index/index.vue index 7b27a36..a3977b5 100644 --- a/miniapp/pages/index/index.vue +++ b/miniapp/pages/index/index.vue @@ -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) diff --git a/miniapp/pages/profile/detail.vue b/miniapp/pages/profile/detail.vue index 32acf1c..b629a10 100644 --- a/miniapp/pages/profile/detail.vue +++ b/miniapp/pages/profile/detail.vue @@ -60,7 +60,7 @@ @@ -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}` }) diff --git a/miniapp/pages/realname/index.vue b/miniapp/pages/realname/index.vue index 77cac12..32224ea 100644 --- a/miniapp/pages/realname/index.vue +++ b/miniapp/pages/realname/index.vue @@ -21,21 +21,22 @@ - - 实名认证已完成 - - - 姓名: - {{ maskedName }} - - - 身份证号: - {{ maskedIdNumber }} - + + + - - 🛡️ - 已实名认证 + + + + 已实名信息 + + 姓名 + {{ maskedName }} + + + 身份证 + {{ maskedIdNumber }} + @@ -44,17 +45,17 @@ - + {{ currentStep > 1 ? '✓' : '1' }} 支付费用 - + {{ currentStep > 2 ? '✓' : '2' }} 上传证件 - + 3 认证完成 @@ -97,7 +98,7 @@ - + 联系客服 @@ -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; diff --git a/miniapp/store/config.js b/miniapp/store/config.js index 4bd57df..ae0633b 100644 --- a/miniapp/store/config.js +++ b/miniapp/store/config.js @@ -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() diff --git a/server/src/XiangYi.AppApi/appsettings.json b/server/src/XiangYi.AppApi/appsettings.json index 7ad0006..df6ca4b 100644 --- a/server/src/XiangYi.AppApi/appsettings.json +++ b/server/src/XiangYi.AppApi/appsettings.json @@ -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" } } } diff --git a/server/src/XiangYi.Infrastructure/Extensions/InfrastructureExtensions.cs b/server/src/XiangYi.Infrastructure/Extensions/InfrastructureExtensions.cs index 82f7fb6..aa1499b 100644 --- a/server/src/XiangYi.Infrastructure/Extensions/InfrastructureExtensions.cs +++ b/server/src/XiangYi.Infrastructure/Extensions/InfrastructureExtensions.cs @@ -50,9 +50,9 @@ public static class InfrastructureExtensions services.Configure(configuration.GetSection("Sms")); services.AddScoped(); - // RealName + // RealName - 阿里云实名认证 services.Configure(configuration.GetSection("RealName")); - services.AddScoped(); + services.AddScoped(); // WeChat services.Configure(configuration.GetSection("WeChat")); diff --git a/server/src/XiangYi.Infrastructure/RealName/AliyunRealNameProvider.cs b/server/src/XiangYi.Infrastructure/RealName/AliyunRealNameProvider.cs new file mode 100644 index 0000000..7269fad --- /dev/null +++ b/server/src/XiangYi.Infrastructure/RealName/AliyunRealNameProvider.cs @@ -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; + +/// +/// 阿里云实名认证服务实现 +/// 使用阿里云身份证二要素核验API(纯服务端接入) +/// +public class AliyunRealNameProvider : IRealNameProvider +{ + private readonly AliyunRealNameOptions _options; + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + // 身份证二要素核验API + private const string IdCardVerifyHost = "cloudauth.aliyuncs.com"; + private const string ApiVersion = "2019-03-07"; + + public AliyunRealNameProvider( + IOptions options, + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _options = options.Value.Aliyun ?? new AliyunRealNameOptions(); + _logger = logger; + _httpClient = httpClientFactory.CreateClient("AliyunRealName"); + } + + /// + /// 身份证二要素验证(姓名+身份证号) + /// + public async Task VerifyIdCardAsync(string name, string idCard) + { + try + { + var parameters = new SortedDictionary + { + { "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(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); + } + } + + /// + /// 身份证三要素验证(阿里云纯API模式不支持,返回失败) + /// + public Task VerifyWithPhotoAsync(string name, string idCard, string photoBase64) + { + _logger.LogWarning("阿里云纯API模式不支持身份证三要素验证(人脸比对)"); + return Task.FromResult(RealNameResult.Fail("NOT_SUPPORTED", "当前配置不支持人脸比对验证,请使用二要素验证")); + } + + /// + /// 身份证OCR识别(正面)- 阿里云纯API模式不支持 + /// + public Task OcrIdCardFrontAsync(string imageBase64) + { + _logger.LogWarning("阿里云纯API模式不支持身份证OCR识别"); + return Task.FromResult(IdCardOcrResult.Fail("NOT_SUPPORTED", "当前配置不支持身份证OCR识别,请手动输入身份信息")); + } + + /// + /// 身份证OCR识别(反面)- 阿里云纯API模式不支持 + /// + public Task OcrIdCardBackAsync(string imageBase64) + { + _logger.LogWarning("阿里云纯API模式不支持身份证OCR识别"); + return Task.FromResult(IdCardOcrResult.Fail("NOT_SUPPORTED", "当前配置不支持身份证OCR识别,请手动输入身份信息")); + } + + /// + /// 生成阿里云API签名 + /// + private string GenerateSignature(SortedDictionary 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); + } + + /// + /// URL编码(阿里云特殊规则) + /// + 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; + } + + /// + /// 获取错误信息 + /// + 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 +} diff --git a/server/src/XiangYi.Infrastructure/RealName/RealNameOptions.cs b/server/src/XiangYi.Infrastructure/RealName/RealNameOptions.cs index 8bcd1f3..e12add1 100644 --- a/server/src/XiangYi.Infrastructure/RealName/RealNameOptions.cs +++ b/server/src/XiangYi.Infrastructure/RealName/RealNameOptions.cs @@ -11,53 +11,33 @@ public class RealNameOptions public const string SectionName = "RealName"; /// - /// 实名认证提供商类型:Tencent, Aliyun + /// 实名认证提供商类型:Aliyun /// - public string Provider { get; set; } = "Tencent"; + public string Provider { get; set; } = "Aliyun"; /// - /// 腾讯云实名认证配置 + /// 阿里云实名认证配置 /// - public TencentRealNameOptions Tencent { get; set; } = new(); - - /// - /// 阿里云实名认证配置(预留) - /// - public AliyunRealNameOptions? Aliyun { get; set; } + public AliyunRealNameOptions Aliyun { get; set; } = new(); } /// -/// 腾讯云实名认证配置 -/// -public class TencentRealNameOptions -{ - /// - /// SecretId - /// - public string SecretId { get; set; } = string.Empty; - - /// - /// SecretKey - /// - public string SecretKey { get; set; } = string.Empty; - - /// - /// 区域 - /// - public string Region { get; set; } = "ap-guangzhou"; - - /// - /// 人脸比对相似度阈值(0-100) - /// - public decimal SimilarityThreshold { get; set; } = 80; -} - -/// -/// 阿里云实名认证配置(预留) +/// 阿里云实名认证配置 /// public class AliyunRealNameOptions { + /// + /// AccessKeyId + /// public string AccessKeyId { get; set; } = string.Empty; + + /// + /// AccessKeySecret + /// public string AccessKeySecret { get; set; } = string.Empty; - public string AppCode { get; set; } = string.Empty; + + /// + /// 区域ID(默认cn-shanghai) + /// + public string RegionId { get; set; } = "cn-shanghai"; } diff --git a/server/src/XiangYi.Infrastructure/RealName/TencentRealNameProvider.cs b/server/src/XiangYi.Infrastructure/RealName/TencentRealNameProvider.cs deleted file mode 100644 index 0da7ae3..0000000 --- a/server/src/XiangYi.Infrastructure/RealName/TencentRealNameProvider.cs +++ /dev/null @@ -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; - -/// -/// 腾讯云实名认证服务实现 -/// 使用腾讯云人脸核身API -/// -public class TencentRealNameProvider : IRealNameProvider -{ - private readonly TencentRealNameOptions _options; - private readonly ILogger _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 options, - IHttpClientFactory httpClientFactory, - ILogger logger) - { - _options = options.Value.Tencent; - _logger = logger; - _httpClient = httpClientFactory.CreateClient("TencentRealName"); - } - - public async Task 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 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); - } - } - - /// - /// 身份证OCR识别(正面) - /// - public async Task 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); - } - } - - /// - /// 身份证OCR识别(反面) - /// - public async Task 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 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(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 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(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; } - } - - /// - /// 腾讯云OCR API响应 - /// - 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; } - } -}