using System.Text.Json; namespace AgriculturalPlatform.Api.Services; /// /// 阿里云市场身份证 OCR 识别服务。 /// 接口:POST https://swcardpack.market.alicloudapi.com/ocr/idcard /// 认证:请求头 Authorization: APPCODE <AppCode>(无需 AppKey/AppSecret) /// 传输:优先 image_url(OSS 私有桶 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)。 /// public sealed class AliyunOcrService(IConfiguration cfg, IHttpClientFactory httpFactory, IFileStorage storage) : IOcrService { public async Task 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 AppCode(appsettings.json 的 Ocr:AppCode)"); var client = httpFactory.CreateClient("ocr"); // 方式一:image_url(OSS 签名 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 { ["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 { ["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}"); } } /// 尝试解析接口返回;无法解析出 JSON 时返回 false 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; } } /// 解析接口返回 JSON(兼容 result/data 容器、多字段别名) 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] + "..."; }