现正做一个项目需要手机端拍照或上传文件来识别身份证信息,同时图片文件需要保存到服务器,尝试使用Blazor Server技术,因为也不是很熟悉心理也没底,借助了trae想不到很快就能搞定,只要说出需求,连代码都不用写就能帮你做好了,当然需要不断的提出需求修改才能达到自己想要的效果,现在AI写代码真牛,还能帮你查看代码中发现的问题并改正,以后码农要失业了。
先来看下最终的效果图

项目的整体目录:

下面放下简单的代码,需要的可以参考或直接用AI
前端IdCardRecognition.razor:
@page "/idcard"
@page "/"
@using System.IO
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
@inject IWebHostEnvironment Environment
@inject HttpClient Http
@inject NavigationManager Navigation
@rendermode InteractiveServer
<PageTitle>身份证识别系统</PageTitle>
<div class="idcard-container">
<h1>身份证识别系统</h1>
<p class="subtitle">拍摄、上传身份证正反面,自动识别信息</p>
<!-- ID Card List Layout -->
<div class="idcard-list">
<!-- Front Side -->
<div class="idcard-item">
<div class="idcard-header">
<span class="idcard-label">身份证正面</span>
@if (HasFrontImage)
{
<span class="status-badge success">已拍摄</span>
}
else
{
<span class="status-badge pending">待拍摄</span>
}
</div>
@if (!HasFrontImage)
{
<div class="camera-box">
<div class="overlay-guide">
<div class="id-card-frame">
<div class="corner corner-tl"></div>
<div class="corner corner-tr"></div>
<div class="corner corner-bl"></div>
<div class="corner corner-br"></div>
</div>
<p class="guide-text">请点击下面“拍照”或“选择本地文件”</p>
</div>
</div>
<div class="camera-controls">
<label class="btn btn-sm btn-primary" disabled="@isUploadingFront">
@if (isUploadingFront)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
<text> 处理中...</text>
}
else
{
<text>📷 拍照</text>
}
<InputFile
accept="image/*"
capture="environment"
OnChange="HandleFrontFileUpload"
style="display: none;" />
</label>
<label class="btn btn-sm btn-secondary" disabled="@isUploadingFront">
@if (isUploadingFront)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
<text> 处理中...</text>
}
else
{
<text>📁 选择本地文件</text>
}
<InputFile
accept="image/*"
OnChange="HandleFrontFileUpload"
style="display: none;" />
</label>
</div>
@if (isUploadingFront)
{
<div class="upload-progress">
<div class="progress-bar"></div>
<span class="progress-text">上传中... @uploadTimeFront 秒</span>
</div>
}
}
else
{
<div class="captured-preview">
<img src="@frontImagePreview" alt="身份证正面" />
<button @onclick="RetakeFront" class="btn btn-sm btn-secondary">重新拍摄</button>
</div>
@if (!string.IsNullOrEmpty(uploadTimeFrontDisplay))
{
<div class="upload-time">⏱️ 上传耗时: @uploadTimeFrontDisplay</div>
}
}
</div>
<!-- Back Side -->
<div class="idcard-item">
<div class="idcard-header">
<span class="idcard-label">身份证反面</span>
@if (HasBackImage)
{
<span class="status-badge success">已拍摄</span>
}
else
{
<span class="status-badge pending">待拍摄</span>
}
</div>
@if (!HasBackImage)
{
<div class="camera-box">
<div class="overlay-guide">
<div class="id-card-frame">
<div class="corner corner-tl"></div>
<div class="corner corner-tr"></div>
<div class="corner corner-bl"></div>
<div class="corner corner-br"></div>
</div>
<p class="guide-text">请点击下面“拍照”或“选择本地文件”</p>
</div>
</div>
<div class="camera-controls">
<label class="btn btn-sm btn-primary" disabled="@isUploadingBack">
@if (isUploadingBack)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
<text> 处理中...</text>
}
else
{
<text>📷 拍照</text>
}
<InputFile
accept="image/*"
capture="environment"
OnChange="HandleBackFileUpload"
style="display: none;" />
</label>
<label class="btn btn-sm btn-secondary" disabled="@isUploadingBack">
@if (isUploadingBack)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
<text> 处理中...</text>
}
else
{
<text>📁 选择本地文件</text>
}
<InputFile
accept="image/*"
OnChange="HandleBackFileUpload"
style="display: none;" />
</label>
</div>
@if (isUploadingBack)
{
<div class="upload-progress">
<div class="progress-bar"></div>
<span class="progress-text">上传中... @uploadTimeBack 秒</span>
</div>
}
}
else
{
<div class="captured-preview">
<img src="@backImagePreview" alt="身份证反面" />
<button @onclick="RetakeBack" class="btn btn-sm btn-secondary">重新拍摄</button>
</div>
@if (!string.IsNullOrEmpty(uploadTimeBackDisplay))
{
<div class="upload-time">⏱️ 上传耗时: @uploadTimeBackDisplay</div>
}
}
</div>
</div>
<!-- Submit Button -->
@if (HasFrontImage && HasBackImage)
{
<div class="submit-section">
<button @onclick="SubmitImages" class="btn btn-lg btn-primary" disabled="@(isSubmitting || isRecognized)">
@if (isSubmitting)
{
<span class="spinner-border spinner-border-sm" role="status"></span>
<text> 提交中...</text>
}
else if (isRecognized)
{
<text>✓ 已成功识别</text>
}
else
{
<text>提交并识别身份证</text>
}
</button>
<button @onclick="ResetAll" class="btn btn-lg btn-secondary">
重置
</button>
</div>
}
<!-- Recognition Results -->
@if (recognitionResult != null)
{
<div class="result-section">
<h2>识别结果信息</h2>
<div class="result-grid">
<div class="result-card">
<h3>身份证正面信息</h3>
<table class="table">
<tbody>
<tr><th>姓名</th><td>@recognitionResult.Name</td></tr>
<tr><th>性别</th><td>@recognitionResult.Gender</td></tr>
<tr><th>民族</th><td>@recognitionResult.Nation</td></tr>
<tr><th>出生日期</th><td>@recognitionResult.BirthDate</td></tr>
<tr><th>住址</th><td>@recognitionResult.Address</td></tr>
<tr><th>身份证号</th><td>@recognitionResult.IdNumber</td></tr>
</tbody>
</table>
</div>
<div class="result-card">
<h3>身份证反面信息</h3>
<table class="table">
<tbody>
<tr><th>签发机关</th><td>@recognitionResult.Authority</td></tr>
<tr><th>有效期限</th><td>@recognitionResult.ValidPeriod</td></tr>
</tbody>
</table>
</div>
</div>
@if (!string.IsNullOrEmpty(recognitionResult.FrontImagePath) || !string.IsNullOrEmpty(recognitionResult.BackImagePath))
{
<div class="saved-images-section">
<h3>服务器保存的图片</h3>
<div class="saved-images">
@if (!string.IsNullOrEmpty(recognitionResult.FrontImagePath))
{
<div class="saved-image-item">
<h4>正面</h4>
<img src="@recognitionResult.FrontImagePath" alt="身份证正面" />
<p class="image-path">@recognitionResult.FrontImagePath</p>
</div>
}
@if (!string.IsNullOrEmpty(recognitionResult.BackImagePath))
{
<div class="saved-image-item">
<h4>反面</h4>
<img src="@recognitionResult.BackImagePath" alt="身份证反面" />
<p class="image-path">@recognitionResult.BackImagePath</p>
</div>
}
</div>
</div>
}
<div class="result-actions">
<button @onclick="ResetAll" class="btn btn-secondary">重新识别</button>
<button @onclick="CopyResults" class="btn btn-primary">复制信息</button>
</div>
</div>
}
<!-- Status Message -->
@if (!string.IsNullOrEmpty(statusMessage))
{
<div class="status-message @(statusMessageType)">
@statusMessage
</div>
}
</div>
@code {
private string? frontImagePreview;
private string? backImagePreview;
private bool HasFrontImage => !string.IsNullOrEmpty(frontImagePreview);
private bool HasBackImage => !string.IsNullOrEmpty(backImagePreview);
private bool isSubmitting = false;
private bool isRecognized = false;
private bool isUploadingFront = false;
private bool isUploadingBack = false;
private int uploadTimeFront = 0;
private int uploadTimeBack = 0;
private string uploadTimeFrontDisplay = "";
private string uploadTimeBackDisplay = "";
private DateTime? uploadStartTimeFront;
private DateTime? uploadStartTimeBack;
private IdCardInfo? recognitionResult;
private string statusMessage = "";
private string statusMessageType = "";
private async Task HandleFrontFileUpload(InputFileChangeEventArgs e)
{
isUploadingFront = true;
uploadStartTimeFront = DateTime.Now;
uploadTimeFront = 0;
uploadTimeFrontDisplay = "";
var timer = new System.Timers.Timer(1000);
timer.Elapsed += (sender, args) => {
if (isUploadingFront && uploadStartTimeFront.HasValue)
{
uploadTimeFront = (int)(DateTime.Now - uploadStartTimeFront.Value).TotalSeconds;
}
};
timer.Start();
try
{
var file = e.File;
if (file == null) return;
if (!file.ContentType.StartsWith("image/"))
{
ShowStatus("请选择图片文件", "error");
return;
}
if (file.Size > 10 * 1024 * 1024)
{
ShowStatus("图片大小不能超过10MB", "error");
return;
}
ShowStatus("正在处理图片...", "info");
using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
var bytes = memoryStream.ToArray();
var base64 = Convert.ToBase64String(bytes);
frontImagePreview = $"data:{file.ContentType};base64,{base64}";
var totalSeconds = (int)(DateTime.Now - uploadStartTimeFront.Value).TotalSeconds;
uploadTimeFrontDisplay = totalSeconds == 0 ? "小于1秒" : $"{totalSeconds}秒";
ShowStatus("图片上传成功!", "success");
}
catch (Exception ex)
{
ShowStatus($"上传失败: {ex.Message}", "error");
}
finally
{
timer.Stop();
timer.Dispose();
isUploadingFront = false;
}
}
private void RetakeFront()
{
frontImagePreview = null;
uploadTimeFrontDisplay = "";
}
private async Task HandleBackFileUpload(InputFileChangeEventArgs e)
{
isUploadingBack = true;
uploadStartTimeBack = DateTime.Now;
uploadTimeBack = 0;
uploadTimeBackDisplay = "";
var timer = new System.Timers.Timer(1000);
timer.Elapsed += (sender, args) => {
if (isUploadingBack && uploadStartTimeBack.HasValue)
{
uploadTimeBack = (int)(DateTime.Now - uploadStartTimeBack.Value).TotalSeconds;
}
};
timer.Start();
try
{
var file = e.File;
if (file == null) return;
if (!file.ContentType.StartsWith("image/"))
{
ShowStatus("请选择图片文件", "error");
return;
}
if (file.Size > 10 * 1024 * 1024)
{
ShowStatus("图片大小不能超过10MB", "error");
return;
}
ShowStatus("正在处理图片...", "info");
using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
var bytes = memoryStream.ToArray();
var base64 = Convert.ToBase64String(bytes);
backImagePreview = $"data:{file.ContentType};base64,{base64}";
var totalSeconds = (int)(DateTime.Now - uploadStartTimeBack.Value).TotalSeconds;
uploadTimeBackDisplay = totalSeconds == 0 ? "小于1秒" : $"{totalSeconds}秒";
ShowStatus("图片上传成功!", "success");
}
catch (Exception ex)
{
ShowStatus($"上传失败: {ex.Message}", "error");
}
finally
{
timer.Stop();
timer.Dispose();
isUploadingBack = false;
}
}
private void RetakeBack()
{
backImagePreview = null;
uploadTimeBackDisplay = "";
}
private async Task SubmitImages()
{
if (!HasFrontImage || !HasBackImage)
{
ShowStatus("请先拍摄身份证正反面", "error");
return;
}
isSubmitting = true;
ShowStatus("正在识别身份证信息...", "info");
try
{
await RecognizeIdCard();
}
catch (Exception ex)
{
ShowStatus($"识别失败: {ex.Message}", "error");
}
finally
{
isSubmitting = false;
}
}
private string ExtractBase64(string dataUrl)
{
if (string.IsNullOrEmpty(dataUrl))
return string.Empty;
if (dataUrl.StartsWith("data:"))
{
var commaIndex = dataUrl.IndexOf(',');
if (commaIndex > 0)
{
return dataUrl.Substring(commaIndex + 1);
}
}
return dataUrl;
}
private async Task RecognizeIdCard()
{
if (!HasFrontImage || !HasBackImage)
{
ShowStatus("请先拍摄身份证正反面", "error");
return;
}
ShowStatus("正在识别身份证信息...", "info");
try
{
var requestData = new
{
frontImage = frontImagePreview,
backImage = backImagePreview
};
var apiUri = new Uri(new Uri(Navigation.BaseUri), "api/idcard/recognize");
var response = await Http.PostAsJsonAsync(apiUri, requestData);
if (response.IsSuccessStatusCode)
{
recognitionResult = await response.Content.ReadFromJsonAsync<IdCardInfo>();
isRecognized = true;
ShowStatus("识别成功!", "success");
}
else
{
var error = await response.Content.ReadAsStringAsync();
ShowStatus($"识别失败: {error}", "error");
}
}
catch (Exception ex)
{
ShowStatus($"识别失败: {ex.Message}", "error");
}
}
private void ResetAll()
{
frontImagePreview = null;
backImagePreview = null;
recognitionResult = null;
isRecognized = false;
uploadTimeFrontDisplay = "";
uploadTimeBackDisplay = "";
ShowStatus("", "");
}
private async Task CopyResults()
{
if (recognitionResult == null) return;
var text = $@"身份证信息:
姓名:{recognitionResult.Name}
性别:{recognitionResult.Gender}
民族:{recognitionResult.Nation}
出生日期:{recognitionResult.BirthDate}
住址:{recognitionResult.Address}
身份证号:{recognitionResult.IdNumber}
签发机关:{recognitionResult.Authority}
有效期限:{recognitionResult.ValidPeriod}";
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
ShowStatus("信息已复制到剪贴板", "success");
}
private void ShowStatus(string message, string type)
{
statusMessage = message;
statusMessageType = type;
StateHasChanged();
}
public class IdCardInfo
{
public string Name { get; set; } = "";
public string Gender { get; set; } = "";
public string Nation { get; set; } = "";
public string BirthDate { get; set; } = "";
public string Address { get; set; } = "";
public string IdNumber { get; set; } = "";
public string Authority { get; set; } = "";
public string ValidPeriod { get; set; } = "";
public string FrontImagePath { get; set; } = "";
public string BackImagePath { get; set; } = "";
}
}
控制器IdCardController.cs:
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace PhoneCameraApp.Controllers;
//阿里OCR服务文档:https://market.aliyun.com/detail/cmapi010401?spm=5176.29867242_210807074.0.0.44e83e7e5U4Fj7#sku=yuncode4401000018
[ApiController]
[Route("api/[controller]")]
public class IdCardController : ControllerBase
{
private readonly ILogger<IdCardController> _logger;
private readonly IConfiguration _configuration;
private readonly IWebHostEnvironment _environment;
private readonly IHttpContextAccessor _httpContextAccessor;
public IdCardController(ILogger<IdCardController> logger, IConfiguration configuration, IWebHostEnvironment environment, IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_configuration = configuration;
_environment = environment;
_httpContextAccessor = httpContextAccessor;
}
private string GetBasePath()
{
var pathBase = _httpContextAccessor.HttpContext?.Request.PathBase.ToString() ?? "";
_logger.LogInformation("GetBasePath: {PathBase}", pathBase);
return pathBase;
}
[HttpPost("recognize")]
public async Task<IActionResult> RecognizeIdCard([FromBody] IdCardRequest request)
{
try
{
if (string.IsNullOrEmpty(request.FrontImage) || string.IsNullOrEmpty(request.BackImage))
{
return BadRequest("请提供身份证正反面图片");
}
// Extract base64 data from data URLs
var frontBase64 = ExtractBase64(request.FrontImage);
var backBase64 = ExtractBase64(request.BackImage);
if (string.IsNullOrEmpty(frontBase64) || string.IsNullOrEmpty(backBase64))
{
return BadRequest("图片格式不正确");
}
// Convert base64 to byte arrays
var frontBytes = Convert.FromBase64String(frontBase64);
var backBytes = Convert.FromBase64String(backBase64);
// Save images to server
var savedPaths = await SaveImagesToServer(frontBytes, backBytes);
// Perform OCR recognition
var frontResult = await RecognizeFrontIdCard(frontBytes);
var backResult = await RecognizeBackIdCard(backBytes);
// Combine results
var result = new IdCardInfo
{
Name = frontResult.Name,
Gender = frontResult.Gender,
Nation = frontResult.Nation,
BirthDate = frontResult.BirthDate,
Address = frontResult.Address,
IdNumber = frontResult.IdNumber,
Authority = backResult.Authority,
ValidPeriod = backResult.ValidPeriod,
FrontImagePath = savedPaths.FrontPath,
BackImagePath = savedPaths.BackPath
};
_logger.LogInformation("返回结果: FrontImagePath={FrontPath}, BackImagePath={BackPath}", result.FrontImagePath, result.BackImagePath);
_logger.LogInformation("身份证识别成功: {Name}, {IdNumber}", result.Name, result.IdNumber);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "身份证识别失败");
return StatusCode(500, $"识别失败: {ex.Message}");
}
}
private string ExtractBase64(string dataUrl)
{
if (string.IsNullOrEmpty(dataUrl))
return string.Empty;
// Check if it's a data URL
if (dataUrl.StartsWith("data:"))
{
var commaIndex = dataUrl.IndexOf(',');
if (commaIndex > 0)
{
return dataUrl.Substring(commaIndex + 1);
}
}
return dataUrl;
}
private async Task<(string FrontPath, string BackPath)> SaveImagesToServer(byte[] frontBytes, byte[] backBytes)
{
// Create upload directory if it doesn't exist
var uploadDir = Path.Combine(_environment.WebRootPath, "uploads", "idcards");
if (!Directory.Exists(uploadDir))
{
Directory.CreateDirectory(uploadDir);
}
// Generate unique filenames with timestamp
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
var frontFileName = $"front_{timestamp}.jpg";
var backFileName = $"back_{timestamp}.jpg";
var frontPath = Path.Combine(uploadDir, frontFileName);
var backPath = Path.Combine(uploadDir, backFileName);
// Save front image
await System.IO.File.WriteAllBytesAsync(frontPath, frontBytes);
// Save back image
await System.IO.File.WriteAllBytesAsync(backPath, backBytes);
// Return relative paths for display (include base path for subdirectory deployment)
var basePath = GetBasePath();
var relativeFrontPath = $"{basePath}/uploads/idcards/{frontFileName}";
var relativeBackPath = $"{basePath}/uploads/idcards/{backFileName}";
return (relativeFrontPath, relativeBackPath);
}
//调用阿里处理身份证正面
private async Task<FrontIdCardResult> RecognizeFrontIdCard(byte[] imageBytes)
{
var appCode = _configuration["OCR:Aliyun:AppCode"];
var useAppCodeApi = !string.IsNullOrEmpty(appCode);
if (useAppCodeApi)
{
return await RecognizeWithAliyunMarket(imageBytes, "face");
}
else
{
return await RecognizeWithAliyun(imageBytes, "face");
}
}
//调用阿里处理身份证反面
private async Task<BackIdCardResult> RecognizeBackIdCard(byte[] imageBytes)
{
var appCode = _configuration["OCR:Aliyun:AppCode"];
var useAppCodeApi = !string.IsNullOrEmpty(appCode);
if (useAppCodeApi)
{
return await RecognizeWithAliyunMarketBack(imageBytes);
}
else
{
return await RecognizeWithAliyunBack(imageBytes);
}
}
#region Aliyun Market OCR Integration (AppCode-based)
private async Task<FrontIdCardResult> RecognizeWithAliyunMarket(byte[] imageBytes, string side)
{
var appCode = _configuration["OCR:Aliyun:AppCode"];
if (string.IsNullOrEmpty(appCode))
{
throw new InvalidOperationException("阿里云AppCode未配置。请在appsettings.json中配置OCR:Aliyun:AppCode");
}
var url = "https://cardnumber.market.alicloudapi.com/rest/160601/ocr/ocr_idcard.json";
var imageBase64 = Convert.ToBase64String(imageBytes);
var requestBody = new
{
image = imageBase64,
configure = new
{
side = side,
quality_info = true
}
};
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"APPCODE {appCode}");
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
var jsonBody = JsonSerializer.Serialize(requestBody);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
_logger.LogInformation("调用阿里云市场OCR API(正面): {Url}", url);
var response = await httpClient.PostAsync(url, content);
var responseJson = await response.Content.ReadAsStringAsync();
_logger.LogInformation("阿里云市场OCR响应状态: {StatusCode}", response.StatusCode);
_logger.LogInformation("阿里云市场OCR响应内容: {Response}", responseJson);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("阿里云市场OCR调用失败: {StatusCode}, 响应: {Response}", response.StatusCode, responseJson);
throw new Exception($"阿里云市场OCR调用失败 (HTTP {response.StatusCode}): {responseJson}");
}
try
{
var result = JsonSerializer.Deserialize<AliyunMarketOcrFaceResponse>(responseJson);
if (result?.Success == true)
{
return new FrontIdCardResult
{
Name = result.Name ?? "",
Gender = result.Sex ?? "",
Nation = result.Nationality ?? "",
BirthDate = FormatBirthDate(result.Birth ?? ""),
Address = result.Address ?? "",
IdNumber = result.Num ?? ""
};
}
else
{
var errorMsg = result?.ErrorMsg ?? result?.Error ?? "未知错误";
_logger.LogError("阿里云市场OCR返回错误: {Error}", errorMsg);
throw new Exception($"OCR识别失败: {errorMsg}");
}
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON解析失败,原始响应: {Response}", responseJson);
throw new Exception($"阿里云市场OCR响应解析失败: {ex.Message}. 原始响应: {responseJson}");
}
}
private async Task<BackIdCardResult> RecognizeWithAliyunMarketBack(byte[] imageBytes)
{
var appCode = _configuration["OCR:Aliyun:AppCode"];
if (string.IsNullOrEmpty(appCode))
{
throw new InvalidOperationException("阿里云AppCode未配置。请在appsettings.json中配置OCR:Aliyun:AppCode");
}
var url = "https://cardnumber.market.alicloudapi.com/rest/160601/ocr/ocr_idcard.json";
var imageBase64 = Convert.ToBase64String(imageBytes);
var requestBody = new
{
image = imageBase64,
configure = new
{
side = "back",
quality_info = true
}
};
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"APPCODE {appCode}");
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
var jsonBody = JsonSerializer.Serialize(requestBody);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
_logger.LogInformation("调用阿里云市场OCR API(反面): {Url}", url);
var response = await httpClient.PostAsync(url, content);
var responseJson = await response.Content.ReadAsStringAsync();
_logger.LogInformation("阿里云市场OCR响应状态: {StatusCode}", response.StatusCode);
_logger.LogInformation("阿里云市场OCR响应内容: {Response}", responseJson);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("阿里云市场OCR调用失败: {StatusCode}, 响应: {Response}", response.StatusCode, responseJson);
throw new Exception($"阿里云市场OCR调用失败 (HTTP {response.StatusCode}): {responseJson}");
}
try
{
var result = JsonSerializer.Deserialize<AliyunMarketOcrBackResponse>(responseJson);
if (result?.Success == true)
{
var validPeriod = "";
if (!string.IsNullOrEmpty(result.EndDate))
{
validPeriod = result.EndDate == "长期" ? $"{result.StartDate}-长期" : $"{result.StartDate}-{result.EndDate}";
}
return new BackIdCardResult
{
Authority = result.Authority ?? "",
ValidPeriod = validPeriod
};
}
else
{
var errorMsg = result?.ErrorMsg ?? result?.Error ?? "未知错误";
_logger.LogError("阿里云市场OCR返回错误: {Error}", errorMsg);
throw new Exception($"OCR识别失败: {errorMsg}");
}
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON解析失败,原始响应: {Response}", responseJson);
throw new Exception($"阿里云市场OCR响应解析失败: {ex.Message}. 原始响应: {responseJson}");
}
}
#endregion
#region Aliyun Official OCR Integration
private async Task<FrontIdCardResult> RecognizeWithAliyun(byte[] imageBytes, string side)
{
var accessKeyId = _configuration["OCR:Aliyun:AccessKeyId"];
var accessKeySecret = _configuration["OCR:Aliyun:AccessKeySecret"];
// Check if credentials are configured
if (string.IsNullOrEmpty(accessKeyId) || string.IsNullOrEmpty(accessKeySecret))
{
throw new InvalidOperationException("阿里云AccessKey未配置。请在appsettings.json中配置OCR:Aliyun:AccessKeyId和AccessKeySecret");
}
// Official Aliyun OCR API endpoint
var url = "https://ocr.cn-shanghai.aliyuncs.com/";
// Convert image to base64 for Aliyun API
var imageBase64 = Convert.ToBase64String(imageBytes);
// Generate common parameters
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
var nonce = Guid.NewGuid().ToString("N");
// Prepare request parameters for POST body
var parameters = new Dictionary<string, string>
{
["AccessKeyId"] = accessKeyId,
["Action"] = "RecognizeIdentityCard",
["Format"] = "JSON",
["ImageURL"] = $"data:image/jpeg;base64,{imageBase64}",
["Side"] = side,
["SignatureMethod"] = "HMAC-SHA1",
["SignatureNonce"] = nonce,
["SignatureVersion"] = "1.0",
["Timestamp"] = timestamp,
["Version"] = "2021-07-07"
};
// Generate signature
var signature = GenerateSignature(parameters, accessKeySecret);
parameters["Signature"] = signature;
// Remove Signature from body parameters (only used in query string)
var bodyParams = new Dictionary<string, string>(parameters);
bodyParams.Remove("Signature");
using var httpClient = new HttpClient();
// Build URL with signature
var signedUrl = $"{url}?Signature={Uri.EscapeDataString(signature)}";
// Prepare request body
var jsonBody = JsonSerializer.Serialize(bodyParams);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
_logger.LogInformation("调用阿里云OCR API: {Url}, 侧边: {Side}", url, side);
var response = await httpClient.PostAsync(signedUrl, content);
var responseJson = await response.Content.ReadAsStringAsync();
_logger.LogInformation("阿里云OCR响应状态: {StatusCode}", response.StatusCode);
_logger.LogInformation("阿里云OCR响应内容: {Response}", responseJson);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("阿里云OCR调用失败: {StatusCode}, 响应: {Response}", response.StatusCode, responseJson);
throw new Exception($"阿里云OCR调用失败 (HTTP {response.StatusCode}): {responseJson}");
}
try
{
var result = JsonSerializer.Deserialize<AliyunOfficialOcrResponse>(responseJson);
if (side == "face" && result?.Data?.Face != null)
{
var faceData = result.Data.Face;
return new FrontIdCardResult
{
Name = faceData.Name ?? "",
Gender = faceData.Sex ?? "",
Nation = faceData.Nationality ?? "",
BirthDate = faceData.BirthDate ?? "",
Address = faceData.Address ?? "",
IdNumber = faceData.IdNum ?? ""
};
}
else
{
// For back side or no data, return empty front result
return new FrontIdCardResult
{
Name = "",
Gender = "",
Nation = "",
BirthDate = "",
Address = "",
IdNumber = ""
};
}
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON解析失败,原始响应: {Response}", responseJson);
throw new Exception($"阿里云OCR响应解析失败: {ex.Message}. 原始响应: {responseJson}");
}
}
private async Task<BackIdCardResult> RecognizeWithAliyunBack(byte[] imageBytes)
{
var accessKeyId = _configuration["OCR:Aliyun:AccessKeyId"];
var accessKeySecret = _configuration["OCR:Aliyun:AccessKeySecret"];
// Check if credentials are configured
if (string.IsNullOrEmpty(accessKeyId) || string.IsNullOrEmpty(accessKeySecret))
{
throw new InvalidOperationException("阿里云AccessKey未配置。请在appsettings.json中配置OCR:Aliyun:AccessKeyId和AccessKeySecret");
}
// Official Aliyun OCR API endpoint
var url = "https://ocr.cn-shanghai.aliyuncs.com/";
// Convert image to base64 for Aliyun API
var imageBase64 = Convert.ToBase64String(imageBytes);
// Generate common parameters
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
var nonce = Guid.NewGuid().ToString("N");
// Prepare request parameters for POST body
var parameters = new Dictionary<string, string>
{
["AccessKeyId"] = accessKeyId,
["Action"] = "RecognizeIdentityCard",
["Format"] = "JSON",
["ImageURL"] = $"data:image/jpeg;base64,{imageBase64}",
["Side"] = "back",
["SignatureMethod"] = "HMAC-SHA1",
["SignatureNonce"] = nonce,
["SignatureVersion"] = "1.0",
["Timestamp"] = timestamp,
["Version"] = "2021-07-07"
};
// Generate signature
var signature = GenerateSignature(parameters, accessKeySecret);
parameters["Signature"] = signature;
// Remove Signature from body parameters (only used in query string)
var bodyParams = new Dictionary<string, string>(parameters);
bodyParams.Remove("Signature");
using var httpClient = new HttpClient();
// Build URL with signature
var signedUrl = $"{url}?Signature={Uri.EscapeDataString(signature)}";
// Prepare request body
var jsonBody = JsonSerializer.Serialize(bodyParams);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
_logger.LogInformation("调用阿里云OCR API(反面): {Url}", url);
var response = await httpClient.PostAsync(signedUrl, content);
var responseJson = await response.Content.ReadAsStringAsync();
_logger.LogInformation("阿里云OCR响应状态: {StatusCode}", response.StatusCode);
_logger.LogInformation("阿里云OCR响应内容: {Response}", responseJson);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("阿里云OCR调用失败: {StatusCode}, 响应: {Response}", response.StatusCode, responseJson);
throw new Exception($"阿里云OCR调用失败 (HTTP {response.StatusCode}): {responseJson}");
}
try
{
var result = JsonSerializer.Deserialize<AliyunOfficialOcrResponse>(responseJson);
if (result?.Data?.Back != null)
{
var backData = result.Data.Back;
return new BackIdCardResult
{
Authority = backData.Authority ?? "",
ValidPeriod = $"{backData.StartDate}-{backData.EndDate}"
};
}
return new BackIdCardResult
{
Authority = "",
ValidPeriod = ""
};
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON解析失败,原始响应: {Response}", responseJson);
throw new Exception($"阿里云OCR响应解析失败: {ex.Message}. 原始响应: {responseJson}");
}
}
private string GenerateSignature(Dictionary<string, string> parameters, string accessKeySecret)
{
// Sort parameters by key
var sortedParams = parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value);
// Build canonicalized query string
var queryString = string.Join("&",
sortedParams.Select(p => $"{PercentEncode(p.Key)}={PercentEncode(p.Value)}"));
// Build string to sign
var stringToSign = $"POST&{PercentEncode("/")}&{PercentEncode(queryString)}";
// Compute HMAC-SHA1 signature
var keyBytes = Encoding.UTF8.GetBytes(accessKeySecret + "&");
var messageBytes = Encoding.UTF8.GetBytes(stringToSign);
using var hmac = new System.Security.Cryptography.HMACSHA1(keyBytes);
var hashBytes = hmac.ComputeHash(messageBytes);
return Convert.ToBase64String(hashBytes);
}
private string PercentEncode(string value)
{
if (string.IsNullOrEmpty(value))
return "";
var encoded = Uri.EscapeDataString(value);
return encoded.Replace("*", "%2A").Replace("+", "%20").Replace("%7E", "~");
}
private string FormatBirthDate(string birthDate)
{
if (string.IsNullOrEmpty(birthDate) || birthDate.Length != 8)
return birthDate;
return $"{birthDate.Substring(0, 4)}-{birthDate.Substring(4, 2)}-{birthDate.Substring(6, 2)}";
}
#endregion
#region Models
public class IdCardRequest
{
public string FrontImage { get; set; } = "";
public string BackImage { get; set; } = "";
}
public class IdCardInfo
{
public string Name { get; set; } = "";
public string Gender { get; set; } = "";
public string Nation { get; set; } = "";
public string BirthDate { get; set; } = "";
public string Address { get; set; } = "";
public string IdNumber { get; set; } = "";
public string Authority { get; set; } = "";
public string ValidPeriod { get; set; } = "";
public string FrontImagePath { get; set; } = "";
public string BackImagePath { get; set; } = "";
}
public class FrontIdCardResult
{
public string Name { get; set; } = "";
public string Gender { get; set; } = "";
public string Nation { get; set; } = "";
public string BirthDate { get; set; } = "";
public string Address { get; set; } = "";
public string IdNumber { get; set; } = "";
}
public class BackIdCardResult
{
public string Authority { get; set; } = "";
public string ValidPeriod { get; set; } = "";
}
// Aliyun Official OCR Response Model
private class AliyunOfficialOcrResponse
{
[System.Text.Json.Serialization.JsonPropertyName("RequestId")]
public string? RequestId { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("Data")]
public IdCardData? Data { get; set; }
}
private class IdCardData
{
[System.Text.Json.Serialization.JsonPropertyName("Face")]
public FaceData? Face { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("Back")]
public BackData? Back { get; set; }
}
private class FaceData
{
[System.Text.Json.Serialization.JsonPropertyName("Name")]
public string? Name { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("Sex")]
public string? Sex { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("Nationality")]
public string? Nationality { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("BirthDate")]
public string? BirthDate { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("Address")]
public string? Address { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("IdNum")]
public string? IdNum { get; set; }
}
private class BackData
{
[System.Text.Json.Serialization.JsonPropertyName("Authority")]
public string? Authority { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("StartDate")]
public string? StartDate { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("EndDate")]
public string? EndDate { get; set; }
}
private class AliyunMarketOcrResponse
{
[System.Text.Json.Serialization.JsonPropertyName("success")]
public bool Success { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("error")]
public string? Error { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("error_msg")]
public string? ErrorMsg { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("name")]
public string? Name { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("sex")]
public string? Sex { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("nationality")]
public string? Nationality { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("birth")]
public string? Birth { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("address")]
public string? Address { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("num")]
public string? Num { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("authority")]
public string? Authority { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("start_date")]
public string? StartDate { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("end_date")]
public string? EndDate { get; set; }
}
private class AliyunMarketOcrFaceResponse : AliyunMarketOcrResponse
{
}
private class AliyunMarketOcrBackResponse : AliyunMarketOcrResponse
{
}
#endregion
}
配置文件appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"PathBase": "/idcard",
"OCR": {
"Service": "aliyun",
"Aliyun": {
"AccessKeyId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"AccessKeySecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"AppCode": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
里面的AppCode需要填写自己的,是调用阿里云的身份证OCR识别服务,详细使用可以参考文档。
需要完整代码的放在网盘里,有需要的可以下载,链接:https://pan.quark.cn/s/21313c105487
阿里OCR申请接口文档:
https://market.aliyun.com/detail/cmapi010401?spm=5176.29867242_210807074.0.0.44e83e7e5U4Fj7#sku=yuncode4401000018
https://www.88531.cn/?p=49264
创作不易,用心坚持,请喝一怀爱心咖啡!继续坚持创作~~
