Files

193 lines
8.5 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
namespace AgriculturalPlatform.Api.Services;
/// <summary>
/// 阿里云市场身份证 OCR 识别服务。
/// 接口:POST https://swcardpack.market.alicloudapi.com/ocr/idcard
/// 认证:请求头 Authorization: APPCODE &lt;AppCode&gt;(无需 AppKey/AppSecret
/// 传输:优先 image_urlOSS 私有桶 10 分钟签名 URL,实测兼容性最好);
/// 本地存储或签名失败时回退 image_base64。
/// 返回:result 容器(兼容 data),成功码 0000/1000/0/200 等;
/// 正面字段 name/number/sex/address,反面 authority/valid_date,字段名做多别名兼容。
/// 配置:appsettings.json 的 Ocr 节点(Provider=aliyun、AppCode、Url、MaxBytes)。
/// </summary>
public sealed class AliyunOcrService(IConfiguration cfg, IHttpClientFactory httpFactory, IFileStorage storage) : IOcrService
{
public async Task<IdCardOcrResult> RecognizeAsync(Stream image, string side, string fileName = "idcard.jpg", string? storageKey = null)
{
var appCode = cfg["Ocr:AppCode"];
var url = cfg["Ocr:Url"] ?? "https://swcardpack.market.alicloudapi.com/ocr/idcard";
if (string.IsNullOrWhiteSpace(appCode))
return new IdCardOcrResult(Success: false,
Message: "未配置阿里云 OCR AppCodeappsettings.json 的 Ocr:AppCode");
var client = httpFactory.CreateClient("ocr");
// 方式一:image_urlOSS 签名 URL),可绕过接口对 base64 的兼容问题
if (!string.IsNullOrWhiteSpace(storageKey))
{
var presigned = storage.GeneratePresignedUrl(storageKey);
if (!string.IsNullOrWhiteSpace(presigned))
{
using var req = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string> { ["image_url"] = presigned })
};
req.Headers.TryAddWithoutValidation("Authorization", $"APPCODE {appCode}");
try
{
using var resp = await client.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
if (resp.IsSuccessStatusCode) return ParseResult(body, side);
if (!TryParseResult(body, out var r)) // 业务层报错(8888 等)也透传给用户
return r;
}
catch (Exception ex)
{
return new IdCardOcrResult(Success: false, Message: $"OCR 调用异常(image_url):{ex.Message}");
}
}
}
// 方式二:image_base64 回退
if (image.CanSeek) image.Position = 0;
await using var buffer = new MemoryStream();
await image.CopyToAsync(buffer);
var maxBytes = cfg.GetValue("Ocr:MaxBytes", 4L * 1024 * 1024);
if (buffer.Length > maxBytes)
return new IdCardOcrResult(Success: false, Message: "图片过大(超过 4MB),请压缩后重试");
var base64 = Convert.ToBase64String(buffer.ToArray());
using var req2 = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string> { ["image_base64"] = base64 })
};
req2.Headers.TryAddWithoutValidation("Authorization", $"APPCODE {appCode}");
try
{
using var resp2 = await client.SendAsync(req2);
var body2 = await resp2.Content.ReadAsStringAsync();
if (!resp2.IsSuccessStatusCode)
return new IdCardOcrResult(Success: false,
Message: $"OCR 接口返回 HTTP {(int)resp2.StatusCode}{Truncate(body2, 200)}");
return ParseResult(body2, side);
}
catch (Exception ex)
{
return new IdCardOcrResult(Success: false, Message: $"OCR 调用异常:{ex.Message}");
}
}
/// <summary>尝试解析接口返回;无法解析出 JSON 时返回 false</summary>
private static bool TryParseResult(string json, out IdCardOcrResult result)
{
try
{
using var doc = JsonDocument.Parse(json);
result = ParseDoc(doc.RootElement, "");
return true;
}
catch (JsonException)
{
result = new IdCardOcrResult(Success: false, Message: "OCR 返回数据无法解析");
return false;
}
}
/// <summary>解析接口返回 JSON(兼容 result/data 容器、多字段别名)</summary>
private static IdCardOcrResult ParseResult(string json, string side)
{
try
{
using var doc = JsonDocument.Parse(json);
return ParseDoc(doc.RootElement, side);
}
catch (JsonException)
{
return new IdCardOcrResult(Success: false, Message: "OCR 返回数据无法解析");
}
}
private static IdCardOcrResult ParseDoc(JsonElement root, string side)
{
var data = root;
if (root.ValueKind == JsonValueKind.Object)
{
if (root.TryGetProperty("result", out var r) && r.ValueKind == JsonValueKind.Object)
data = r;
else if (root.TryGetProperty("data", out var d) && d.ValueKind == JsonValueKind.Object)
data = d;
}
// 接口级失败判定(success=false 或 result_code/code 非成功码)
var ok = GetBool(root, "success") ?? GetBool(root, "ok") ?? true;
var code = GetStr(root, "result_code") ?? GetStr(root, "code") ?? GetStr(root, "status") ?? "";
var msg = GetStr(root, "result_msg") ?? GetStr(root, "msg")
?? GetStr(root, "message") ?? GetStr(root, "errmsg") ?? "";
if (!ok && !IsSuccessCode(code))
return new IdCardOcrResult(Success: false,
Message: string.IsNullOrWhiteSpace(msg) ? $"OCR 识别失败(code={code}" : $"{msg}code={code}");
var name = GetFirst(data, "name", "realName", "trueName", "姓名");
var idCard = GetFirst(data, "number", "id", "idCard", "idNumber", "cardNo", "num", "身份证号");
var gender = NormalizeGender(GetFirst(data, "sex", "gender", "性别"));
var address = GetFirst(data, "address", "addr", "住址");
var authority = GetFirst(data, "authority", "issuingAuthority", "issueOrg", "signOrg", "签发机关");
var validDate = GetFirst(data, "valid_date", "validDate", "validity", "expiry", "有效期");
// 正面看姓名/证件号,反面看签发机关/有效期;任一命中即视为识别成功
var success = side == "back"
? !string.IsNullOrWhiteSpace(authority) || !string.IsNullOrWhiteSpace(validDate)
: !string.IsNullOrWhiteSpace(name) || !string.IsNullOrWhiteSpace(idCard);
return new IdCardOcrResult(
name, idCard, gender, address, "", "",
success, success ? "识别完成" : "未识别到有效信息,请确保图片清晰、证件完整");
}
private static bool IsSuccessCode(string code) =>
code is "0000" or "0" or "200" or "1000" or "000000" or "1";
private static bool? GetBool(JsonElement e, string key) =>
e.ValueKind == JsonValueKind.Object && e.TryGetProperty(key, out var v)
? v.ValueKind switch
{
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.String when bool.TryParse(v.GetString(), out var b) => b,
_ => null
}
: null;
private static string? GetStr(JsonElement e, string key) =>
e.ValueKind == JsonValueKind.Object && e.TryGetProperty(key, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
private static string GetFirst(JsonElement e, params string[] keys)
{
if (e.ValueKind != JsonValueKind.Object) return "";
foreach (var k in keys)
if (e.TryGetProperty(k, out var v) && v.ValueKind is JsonValueKind.String)
{
var s = v.GetString()?.Trim();
if (!string.IsNullOrWhiteSpace(s)) return s!;
}
return "";
}
private static string NormalizeGender(string? v) => v switch
{
null or "" => "",
"1" or "M" or "male" or "Male" or "男" => "男",
"2" or "F" or "female" or "Female" or "女" => "女",
_ => v
};
private static string Truncate(string s, int max) =>
s.Length <= max ? s : s[..max] + "...";
}