Platform upgrade: login auth fix, OCR/OSS upload, system menus and dicts, weather dashboard, weighing and invoice modules
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AgriculturalPlatform.Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云市场身份证 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)。
|
||||
/// </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 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<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] + "...";
|
||||
}
|
||||
Reference in New Issue
Block a user