现正做一个项目需要手机端拍照或上传文件来识别身份证信息,同时图片文件需要保存到服务器,尝试使用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

www.npspro.cn软师兄
软师兄 » Blazor Server开发一个手机拍照识别身份证信息OCR使用阿里接口
50T免费网盘资源大集合【持续更中~~~~】:点击查看