提交修改

This commit is contained in:
quyixiao 2026-06-06 00:59:55 +08:00
parent 1af445bcd8
commit b6e2a376e6
3 changed files with 636 additions and 15 deletions

View File

@ -1,43 +1,585 @@
package com.heyu.api.controller.car; package com.heyu.api.controller.car;
import com.heyu.api.baidu.handle.traffic.BVehicleLicenseHandle;
import com.heyu.api.baidu.request.traffic.BVehicleLicenseRequest;
import com.heyu.api.controller.BaseController; import com.heyu.api.controller.BaseController;
import com.heyu.api.controller.ocr.BaiduOcrResult;
import com.heyu.api.data.annotation.EbAuthentication; import com.heyu.api.data.annotation.EbAuthentication;
import com.heyu.api.data.constants.ApiConstants; import com.heyu.api.data.constants.ApiConstants;
import com.heyu.api.data.utils.MapUtils;
import com.heyu.api.data.utils.R; import com.heyu.api.data.utils.R;
import com.heyu.api.data.utils.StringUtils; import com.heyu.api.data.utils.StringUtils;
import com.heyu.api.request.car.VehicleLicenseRequest; import com.heyu.api.request.car.VehicleLicenseRequest;
import org.springframework.beans.factory.annotation.Autowired; import com.heyu.api.resp.car.RecognizeDrivingLicenseBackResp;
import com.heyu.api.resp.car.RecognizeDrivingLicenseFaceResp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* 行驶证识别控制器
* <p>
* 能力说明对机动车行驶证<strong>主页正页</strong><strong>副页</strong><strong>电子行驶证</strong>进行结构化识别
* 底层对接百度智能云文字识别行驶证识别能力由本服务完成参数校验请求转发与字段映射
* </p>
*
* <h3>百度官方文档</h3>
* <ul>
* <li>产品文档<a href="https://cloud.baidu.com/doc/OCR/s/yk3h7y3ks">行驶证识别</a></li>
* <li>接口地址{@code POST https://aip.baidubce.com/rest/2.0/ocr/v1/vehicle_license}</li>
* </ul>
*
* <h3>本服务接口</h3>
* <ul>
* <li>路径{@code POST /driving/license/recognize}</li>
* <li>Content-Type{@code application/x-www-form-urlencoded} {@code application/json}{@code @RequestBody}</li>
* <li>鉴权{@link EbAuthentication}Tencent 鉴权头见项目网关配置</li>
* </ul>
*
* <h3>请求参数{@link VehicleLicenseRequest}</h3>
* <ul>
* <li>imageBase64 / imageUrl二选一图片 jpg/jpeg/png/bmp编码后4M</li>
* <li>side{@code front} 主页默认{@code back} 副页映射百度 {@code vehicle_license_side}</li>
* </ul>
*
* <h3>返回约定重要</h3>
* <ul>
* <li>入参校验失败{@code R.error()}<strong>未调用百度不计费</strong></li>
* <li>已调用百度识别链路一律 {@code R.ok()}异常说明写入 {@code msg}对外不暴露百度字样</li>
* </ul>
*
* @author heyu
* @since 1.0.0
* @see VehicleLicenseRequest
* @see RecognizeDrivingLicenseFaceResp
* @see RecognizeDrivingLicenseBackResp
*/
@Slf4j
@RestController @RestController
@RequestMapping("/driving/license") @RequestMapping("/driving/license")
public class RecognizeDrivingLicenseController extends BaseController { public class RecognizeDrivingLicenseController extends BaseController {
@Autowired /** 百度行驶证识别 API 路径 */
private BVehicleLicenseHandle bVehicleLicenseHandle; private static final String VEHICLE_LICENSE_URI = "/rest/2.0/ocr/v1/vehicle_license";
private static final String SIDE_DEFAULT_HINT = "未传(将按正页处理)";
/**
* 行驶证识别
*
* @param request 行驶证识别请求
* @return 正页返回 {@link RecognizeDrivingLicenseFaceResp}副页返回 {@link RecognizeDrivingLicenseBackResp}
*/
@EbAuthentication(tencent = ApiConstants.TENCENT_AUTH) @EbAuthentication(tencent = ApiConstants.TENCENT_AUTH)
@PostMapping("/recognize") @PostMapping("/recognize")
public R recognize(@RequestBody VehicleLicenseRequest request) { public R recognize(@RequestBody VehicleLicenseRequest request) {
return BaiduOcrResult.raw(bVehicleLicenseHandle.handle(toBaiduRequest(request))); long start = System.currentTimeMillis();
RecognizeContext ctx = null;
try {
String validateError = validateRequest(request);
if (validateError != null) {
log.info("行驶证识别:参数检查没通过,直接返回错误(还没调识别、不扣费)。{} 返回给客户:{}",
buildInputLogContext(request), abbreviate(validateError, 120));
return R.error(validateError);
}
ctx = new RecognizeContext(
resolveVehicleLicenseSide(request),
ImageMode.of(request),
buildInputLogContext(request));
String content = buildRequestContent(request, ctx.side);
if (isBlank(content)) {
log.error("行驶证识别:组装请求失败,请求里没带有效图片。识别{}{}。{}",
sideDesc(ctx.side), ctx.imageMode.desc, ctx.inputLog);
return okResult(ctx.side, formatHint(
"报文组装异常",
"识别请求体在序列化后未包含任何影像载荷,平台侧无法受理本次识别",
"请核对 imageBase64 是否已 urlencode、imageUrl 是否非空字符串",
"目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageMode.name()
), null);
}
Map<String, Object> platformResult = callPlatform(content, ctx);
if (platformResult == null) {
return okResult(ctx.side, formatHint(
"服务无回执",
"识别指令已下发,但在约定时间内未收到平台可解析的 JSON 回执",
"建议间隔 3~5 秒重试;若连续失败,请记录 traceId、调用时刻与 side 并联系技术支持",
"目标页面=" + sideLabel(ctx.side) + ",影像模式=" + ctx.imageMode.name()
), null);
}
Object data = ApiConstants.back.equals(ctx.side)
? buildBackResp(platformResult)
: buildFaceResp(platformResult);
String hint = resolvePlatformHint(platformResult, ctx, data);
logRecognizeResult(ctx, platformResult, data, hint, start);
return okResult(ctx.side, hint, data);
} catch (Exception e) {
long cost = System.currentTimeMillis() - start;
String side = ctx != null ? ctx.side
: (request != null ? resolveVehicleLicenseSide(request) : ApiConstants.front);
String mode = ctx != null ? ctx.imageMode.desc
: (request != null ? ImageMode.of(request).desc : "未知");
log.error("行驶证识别:程序运行出错,耗时 {} ms。识别{}{}。客户传的:{}。异常:{} - {}",
cost, sideDesc(side), mode, buildInputLogContext(request),
e.getClass().getSimpleName(),
e.getMessage() != null ? e.getMessage() : "无具体说明", e);
return okResult(side, formatHint(
"运行时故障",
"服务端在处理识别流程时抛出未预期异常,识别结果不可用",
"请勿重复高频重试;请保存 traceId、异常发生时间及 side由技术支持结合堆栈进一步定位",
"异常类型=" + e.getClass().getSimpleName()
+ (e.getMessage() != null ? ",摘要=" + e.getMessage() : "")
), null);
}
} }
private BVehicleLicenseRequest toBaiduRequest(VehicleLicenseRequest request) { private Map<String, Object> callPlatform(String content, RecognizeContext ctx) {
BVehicleLicenseRequest bRequest = new BVehicleLicenseRequest(); int len = content.length();
log.info("行驶证识别:开始调用平台识别。识别{}{},请求大小约 {} 字节。{}",
sideDesc(ctx.side), ctx.imageMode.desc, len, ctx.inputLog);
Map<String, Object> result = requestBaidu(VEHICLE_LICENSE_URI, content);
if (result == null) {
log.error("行驶证识别:平台没有返回结果(可能网络超时、鉴权失败或响应无法解析)。"
+ "识别{}{},请求约 {} 字节。{}",
sideDesc(ctx.side), ctx.imageMode.desc, len, ctx.inputLog);
}
return result;
}
private String resolvePlatformHint(Map<String, Object> platformResult, RecognizeContext ctx, Object data) {
Object errorCodeObj = platformResult.get("error_code");
if (errorCodeObj != null) {
String errorCode = String.valueOf(errorCodeObj);
Object errorMsgObj = platformResult.get("error_msg");
String errorMsg = errorMsgObj != null ? errorMsgObj.toString() : "平台未返回文字描述";
String category = PlatformError.categoryOf(errorCode);
log.error("行驶证识别:平台拒绝了本次识别。[{}] 错误码 {},原因:{}。识别{}{}。客户传的:{}。{}",
category, errorCode, errorMsg,
sideDesc(ctx.side), ctx.imageMode.desc,
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
return formatHint(
category,
PlatformError.reasonOf(errorCode),
PlatformError.suggestionOf(errorCode, sideLabel(ctx.side)),
"错误码=" + errorCode + ",错误描述=" + errorMsg + ",目标页面=" + sideLabel(ctx.side)
);
}
if (isWordsResultEmpty(platformResult)) {
log.info("行驶证识别:平台返回了,但没有识别结果数据(可能不是行驶证或 side 传错)。识别{}{}。客户传的:{}。{}",
sideDesc(ctx.side), ctx.imageMode.desc,
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
return formatHint(
"结构化结果缺失",
"平台回执未包含可解析的识别结果集合,无法进入字段映射环节",
"优先排查:① 上传内容是否为行驶证对应页面;② side 是否与实物一致(正页 front、副页 back"
+ "③ 使用 URL 时确保图片链接可公网访问且无防盗链",
"目标页面=" + sideLabel(ctx.side) + ",解析状态=结果集为空"
);
}
if (isAllStringFieldsBlank(data)) {
log.info("行驶证识别:平台有返回,但号牌、所有人等字段一个都没识别出来(可能传错正/副页或图片不清晰)。"
+ "识别{}{}。客户传的:{}。平台回执:{}",
sideDesc(ctx.side), ctx.imageMode.desc,
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
return formatHint(
"字段映射为空",
"平台回执已通过结构校验,但号牌号码、所有人、车辆类型等结构化字段均未命中",
ApiConstants.back.equals(ctx.side)
? "副页识别请确认 side=back画面包含检验记录、核定载人数、外廓尺寸等区域"
: "正页识别请确认 side=front画面须包含完整行驶证主页电子行驶证需保证截图清晰",
"目标页面=" + sideLabel(ctx.side) + ",已映射字段数=0"
);
}
return null;
}
private void logRecognizeResult(RecognizeContext ctx, Map<String, Object> platformResult,
Object data, String hint, long start) {
long cost = System.currentTimeMillis() - start;
int mapped = countNonBlankStringFields(data);
if (StringUtils.isNotBlank(hint)) {
log.info("行驶证识别:处理结束(接口仍返回成功,但带了提示信息)。耗时 {} ms识别出 {} 个字段。"
+ "识别{}{}。客户传的:{}。平台回执:{}。给客户的提示:{}",
cost, mapped, sideDesc(ctx.side), ctx.imageMode.desc,
ctx.inputLog, buildPlatformReceiptSummary(platformResult), abbreviate(hint, 120));
} else {
log.info("行驶证识别:识别成功。耗时 {} ms共识别出 {} 个字段。识别{}{}。客户传的:{}。平台回执:{}",
cost, mapped, sideDesc(ctx.side), ctx.imageMode.desc,
ctx.inputLog, buildPlatformReceiptSummary(platformResult));
}
}
private String validateRequest(VehicleLicenseRequest request) {
if (request == null) { if (request == null) {
return bRequest; log.info("行驶证识别:没收到任何请求参数,直接拒绝,未调用识别、不扣费");
return formatHint(
"入参绑定失败",
"控制器未接收到可绑定的请求对象,所有业务字段均为空",
"请确认使用 POST 提交字段名与接口文档一致imageBase64 / imageUrl / side",
"绑定结果=VehicleLicenseRequest 为 null"
);
} }
bRequest.setImageBase64(request.getImageBase64()); if (isBlank(request.getImageBase64()) && isBlank(request.getImageUrl())) {
bRequest.setImageUrl(request.getImageUrl()); log.info("行驶证识别没传图片imageBase64 和 imageUrl 都为空),直接拒绝,未调用识别、不扣费。{}",
if (StringUtils.isNotBlank(request.getSide())) { buildInputLogContext(request));
bRequest.setVehicleLicenseSide(request.getSide()); return formatHint(
"影像源缺失",
"imageBase64 与 imageUrl 均未提供,识别引擎没有可处理的图像输入",
"任选其一:① imageBase64 传 jpg/jpeg/png/bmp 的 base64urlencode 后≤4M"
+ "② imageUrl 传可公网直连的地址≤1024 字符,关闭防盗链)",
"side=" + (isBlank(request.getSide()) ? SIDE_DEFAULT_HINT : request.getSide().trim())
);
}
if (StringUtils.isNotBlank(request.getImageBase64()) && StringUtils.isNotBlank(request.getImageUrl())) {
log.info("行驶证识别:客户同时传了 Base64 和图片链接,按规则只用 Base64忽略链接。{}",
buildInputLogContext(request));
}
if (resolveVehicleLicenseSide(request) == null) {
log.info("行驶证识别side 参数写错了(只支持 front 正页、back 副页),直接拒绝,未调用识别、不扣费。{}",
buildInputLogContext(request));
return formatHint(
"页面标识无效",
"参数 side 的取值无法映射到行驶证主页或副页识别模式",
"行驶证主页(含电子行驶证主页)请传 front副页请传 back",
"当前 side=" + request.getSide() + ",合法取值=front|back"
);
}
return null;
}
private String resolveVehicleLicenseSide(VehicleLicenseRequest request) {
if (request == null || isBlank(request.getSide())) {
return ApiConstants.front;
}
String side = request.getSide().trim();
if (ApiConstants.front.equals(side) || ApiConstants.face.equals(side)) {
return ApiConstants.front;
}
if (ApiConstants.back.equals(side)) {
return ApiConstants.back;
}
return null;
}
private String sideLabel(String side) {
return ApiConstants.back.equals(side) ? "副页(back)" : "正页(front)";
}
private String sideDesc(String side) {
return ApiConstants.back.equals(side) ? "行驶证副页" : "行驶证正页";
}
private String buildRequestContent(VehicleLicenseRequest request, String vehicleLicenseSide) {
StringBuilder sb = new StringBuilder();
if (StringUtils.isNotBlank(request.getImageBase64())) {
sb.append("&image=").append(request.getImageBase64());
} else if (StringUtils.isNotBlank(request.getImageUrl())) {
sb.append("&url=").append(request.getImageUrl());
}
sb.append("&detect_direction=false");
sb.append("&vehicle_license_side=").append(vehicleLicenseSide);
sb.append("&unified=false");
sb.append("&quality_warn=false");
sb.append("&risk_warn=false");
return sb.toString();
}
private R okResult(String vehicleLicenseSide, String hint, Object data) {
if (data == null) {
data = ApiConstants.back.equals(vehicleLicenseSide)
? new RecognizeDrivingLicenseBackResp()
: new RecognizeDrivingLicenseFaceResp();
}
R result = StringUtils.isNotBlank(hint) ? R.ok(hint) : R.ok();
return result.setData(data);
}
private RecognizeDrivingLicenseFaceResp buildFaceResp(Map<String, Object> data) {
RecognizeDrivingLicenseFaceResp resp = new RecognizeDrivingLicenseFaceResp();
resp.setPlateNumber(getWords(data, "号牌号码"));
resp.setVehicleType(getWords(data, "车辆类型"));
resp.setOwner(getWords(data, "所有人"));
resp.setAddress(getWords(data, "住址"));
resp.setUseCharacter(getWords(data, "使用性质"));
resp.setModel(getWords(data, "品牌型号"));
resp.setVin(getWords(data, "车辆识别代号"));
resp.setEngineNumber(getWords(data, "发动机号码"));
resp.setRegisterDate(firstNonBlank(getWords(data, "注册日期"), getWords(data, "注册登记日期")));
resp.setIssueDate(getWords(data, "发证日期"));
resp.setIssueAuthority(firstNonBlank(getWords(data, "发证单位"), getWords(data, "发证机关")));
return resp;
}
private RecognizeDrivingLicenseBackResp buildBackResp(Map<String, Object> data) {
RecognizeDrivingLicenseBackResp resp = new RecognizeDrivingLicenseBackResp();
resp.setPlateNumber(getWords(data, "号牌号码"));
resp.setFileNumber(getWords(data, "档案编号"));
resp.setApprovedPassengerCapacity(getWords(data, "核定载人数"));
resp.setGrossMass(getWords(data, "总质量"));
resp.setUnladenMass(getWords(data, "整备质量"));
resp.setApprovedLoad(getWords(data, "核定载质量"));
resp.setOverallDimension(getWords(data, "外廓尺寸"));
resp.setTractionMass(getWords(data, "准牵引总质量"));
resp.setInspectionRecord(getWords(data, "检验记录"));
resp.setEnergyType(firstNonBlank(getWords(data, "能源种类"), getWords(data, "燃油类型")));
resp.setRemark(getWords(data, "备注"));
resp.setCertificateNumber(getWords(data, "证芯编号"));
return resp;
}
private String getWords(Map<String, Object> data, String field) {
return MapUtils.getByExpr(data, "words_result." + field + ".words");
}
private String firstNonBlank(String first, String second) {
return StringUtils.isNotBlank(first) ? first : second;
}
@SuppressWarnings("unchecked")
private boolean isWordsResultEmpty(Map<String, Object> platformResult) {
Object wordsResult = platformResult.get("words_result");
if (wordsResult == null) {
return true;
}
if (wordsResult instanceof Map) {
return ((Map<String, Object>) wordsResult).isEmpty();
}
return false;
}
private static int countNonBlankStringFields(Object data) {
if (data == null) {
return 0;
}
int count = 0;
for (Field f : data.getClass().getDeclaredFields()) {
if (!String.class.equals(f.getType())) {
continue;
}
try {
f.setAccessible(true);
Object v = f.get(data);
if (v instanceof String && StringUtils.isNotBlank((String) v)) {
count++;
}
} catch (IllegalAccessException ignored) {
// ignore
}
}
return count;
}
private static boolean isAllStringFieldsBlank(Object data) {
return countNonBlankStringFields(data) == 0;
}
private String formatHint(String category, String reason, String suggestion, String detail) {
StringBuilder sb = new StringBuilder();
sb.append("【行驶证识别·").append(category).append("").append(reason);
if (StringUtils.isNotBlank(detail)) {
sb.append("|定位信息:").append(detail);
}
sb.append("|处置指引:").append(suggestion);
return sb.toString();
}
private String abbreviate(String text, int maxLen) {
if (text == null || text.length() <= maxLen) {
return text;
}
return text.substring(0, maxLen) + "...";
}
private String buildInputLogContext(VehicleLicenseRequest request) {
if (request == null) {
return "未收到请求体";
}
String sideParam = isBlank(request.getSide()) ? "未传(默认按正页)" : request.getSide().trim();
String resolvedSide = resolveVehicleLicenseSide(request);
String targetPage = resolvedSide == null ? "side 无法识别" : sideDesc(resolvedSide);
ImageMode mode = ImageMode.of(request);
return String.format("识别%sside 参数=%s%sBase64 长度 %d 字符,图片链接长度 %d 字符",
targetPage, sideParam, mode.desc,
textLength(request.getImageBase64()), textLength(request.getImageUrl()));
}
private String buildPlatformReceiptSummary(Map<String, Object> platformResult) {
if (platformResult == null) {
return "平台无任何回执";
}
Object errorCode = platformResult.get("error_code");
Object errorMsg = platformResult.get("error_msg");
Object logId = platformResult.get("log_id");
if (errorCode != null) {
return String.format("平台返回失败,错误码 %s说明%s流水号 %s",
errorCode,
errorMsg != null ? errorMsg : "",
logId != null ? logId : "");
}
Object wordsResultNum = platformResult.get("words_result_num");
Object wordsResult = platformResult.get("words_result");
int fieldCount = wordsResult instanceof Map ? ((Map<?, ?>) wordsResult).size() : 0;
return String.format("平台返回成功,识别结果约 %s 项、结构化字段 %d 个,流水号 %s",
wordsResultNum != null ? wordsResultNum : "未知",
fieldCount,
logId != null ? logId : "");
}
private int textLength(String value) {
return value == null ? 0 : value.length();
}
private static class RecognizeContext {
final String side;
final ImageMode imageMode;
final String inputLog;
RecognizeContext(String side, ImageMode imageMode, String inputLog) {
this.side = side;
this.imageMode = imageMode;
this.inputLog = inputLog;
}
}
private enum ImageMode {
BASE64("使用 Base64 编码图片"),
URL("使用图片链接"),
BASE64_WITH_URL_IGNORED("同时传了 Base64 和链接,实际使用 Base64"),
NONE("未传图片"),
UNKNOWN("图片提交方式未知");
final String desc;
ImageMode(String desc) {
this.desc = desc;
}
static ImageMode of(VehicleLicenseRequest req) {
if (req == null) {
return UNKNOWN;
}
boolean hasBase64 = StringUtils.isNotBlank(req.getImageBase64());
boolean hasUrl = StringUtils.isNotBlank(req.getImageUrl());
if (hasBase64 && hasUrl) {
return BASE64_WITH_URL_IGNORED;
}
if (hasBase64) {
return BASE64;
}
if (hasUrl) {
return URL;
}
return NONE;
}
}
private enum PlatformError {
QPS_DAILY_LIMIT("17", "日配额耗尽",
"本接口当日累计调用次数已触达平台日配额上限,本次识别请求被拒绝",
"请安排次日再试,或联系运营提升日调用配额"),
QPS_RATE_LIMIT("18", "并发限流",
"单位时间请求过于密集,已触发平台 QPS 限流,请拉长调用间隔后重试",
"请在客户端增加限流/退避策略,避免瞬时并发过高"),
QPS_TOTAL_EXHAUSTED("19", "总配额耗尽",
"平台侧总调用配额已耗尽,需运营侧扩容或次日再试",
"请联系运营确认平台总配额与续费状态"),
PARAM_FORMAT("100", "平台业务拒绝",
"提交的表单字段组合不符合行驶证识别接口契约,请对照文档核对字段名与取值",
null),
AUTH_INVALID("110", "鉴权失效",
"平台鉴权凭证校验未通过,属服务端配置异常,需运维刷新令牌",
"属服务端鉴权配置问题,需技术支持检查平台密钥与令牌缓存"),
AUTH_EXPIRED("111", "鉴权失效",
"平台鉴权凭证已超过有效期,属服务端配置异常,需运维重新签发",
"属服务端令牌过期,需技术支持重新获取并刷新缓存"),
PARAM_ILLEGAL("216100", "影像不合规", "存在非法或与接口不匹配的参数项", null),
IMAGE_MISSING("216101", "影像不合规", "影像入参缺失:未提供可用的 image 或 url 字段", null),
PARAM_TOO_LONG("216103", "影像不合规", "单个参数字段长度超限(常见于 url 过长或 base64 体积过大)", null),
APP_NOT_ENABLED("216110", "影像不合规",
"当前应用未开通行驶证识别能力或授权范围不包含本接口", null),
IMAGE_EMPTY("216200", "影像不合规",
"解码后的图片数据为空,上传环节可能未完成或内容损坏",
"请重新上传图片,确认 base64 未截断、未混入换行或非法字符"),
IMAGE_FORMAT("216201", "影像不合规",
"图片编码格式不在允许列表,仅支持 jpg、jpeg、png、bmp",
"请将图片转为 jpg/jpeg/png/bmp 之一后重新编码上传"),
IMAGE_SIZE("216202", "影像不合规",
"像素尺寸或编码后体积不满足规范(边长 15~4096px编码后≤4M",
"请压缩或裁剪图片保证最长边≤4096px、最短边≥15px且 urlencode 后≤4M"),
IMAGE_BLUR("216203", "影像不合规",
"引擎无法从当前画面中稳定提取行驶证文本,多见于模糊、过曝或非证件照",
null),
LAYOUT_FAIL("216630", "影像不合规",
"证件版面识别失败,建议重拍并保证四角完整、无遮挡",
"请平铺证件拍摄,避免手指遮挡关键字段;副页识别务必传 side=back"),
ENGINE_INTERNAL("282000", "引擎内部错误",
"平台识别引擎内部异常,属短暂性故障,可间隔数秒后重试",
"短暂性故障,建议 5~10 秒后单次重试,不宜连续轰炸接口");
private static final String IMAGE_PREFIX = "2162";
private static final String DEFAULT_CATEGORY = "平台业务拒绝";
private static final String IMAGE_CATEGORY = "影像不合规";
private static final String DEFAULT_REASON = "平台返回了未在本地维护的错误码,需结合错误描述人工判读";
private static final String DEFAULT_SUGGESTION =
"请依据错误描述调整入参或影像后重试;持续失败请附带 traceId 联系技术支持";
private static final Map<String, PlatformError> BY_CODE;
static {
Map<String, PlatformError> m = new HashMap<>();
for (PlatformError e : EnumSet.allOf(PlatformError.class)) {
m.put(e.code, e);
}
BY_CODE = Collections.unmodifiableMap(m);
}
final String code;
final String category;
final String reason;
final String suggestion;
PlatformError(String code, String category, String reason, String suggestion) {
this.code = code;
this.category = category;
this.reason = reason;
this.suggestion = suggestion;
}
static String categoryOf(String code) {
PlatformError e = BY_CODE.get(code);
if (e != null) {
return e.category;
}
if (code != null && code.startsWith(IMAGE_PREFIX)) {
return IMAGE_CATEGORY;
}
return DEFAULT_CATEGORY;
}
static String reasonOf(String code) {
PlatformError e = BY_CODE.get(code);
return e != null ? e.reason : DEFAULT_REASON;
}
static String suggestionOf(String code, String sideLabel) {
PlatformError e = BY_CODE.get(code);
if (e == IMAGE_BLUR) {
return "请在自然光下重拍确保证面占画面主体且文字可辨side=" + sideLabel;
}
if (e != null && e.suggestion != null) {
return e.suggestion;
}
return DEFAULT_SUGGESTION;
} }
return bRequest;
} }
} }

View File

@ -0,0 +1,79 @@
package com.api.test;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
/**
* 图片转 Base64 工具测试用
* <p>
* 用于本地构造驾驶证识别等接口需要的 imageBase64 入参
* 支持本地文件路径与 HTTP(S) 远程 URL 两种来源
*/
public class ImageToBase64 {
/** 读 URL 时的连接超时(毫秒) */
private static final int CONNECT_TIMEOUT_MS = 5_000;
/** 读 URL 时的读取超时(毫秒) */
private static final int READ_TIMEOUT_MS = 15_000;
/**
* 将图片转为 Base64 字符串不带换行不带 data: 前缀
*
* @param source 本地文件绝对/相对路径 http(s):// 开头的远程地址
* @return Base64 字符串
*/
public static String toBase64(String source) throws IOException {
if (source == null || source.trim().isEmpty()) {
throw new IllegalArgumentException("source 不能为空");
}
byte[] bytes = source.startsWith("http://") || source.startsWith("https://")
? readFromUrl(source)
: readFromFile(source);
return Base64.getEncoder().encodeToString(bytes);
}
private static byte[] readFromFile(String path) throws IOException {
Path p = Paths.get(path);
if (!Files.exists(p)) {
throw new IOException("文件不存在: " + path);
}
return Files.readAllBytes(p);
}
private static byte[] readFromUrl(String url) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
conn.setReadTimeout(READ_TIMEOUT_MS);
conn.setRequestMethod("GET");
try (InputStream in = conn.getInputStream()) {
int status = conn.getResponseCode();
if (status / 100 != 2) {
throw new IOException("下载图片失败HTTP " + status + "url=" + url);
}
byte[] buf = new byte[8192];
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
int n;
while ((n = in.read(buf)) > 0) {
out.write(buf, 0, n);
}
return out.toByteArray();
} finally {
conn.disconnect();
}
}
public static void main(String[] args) throws IOException {
String source = args.length > 0
? args[0]
: "/Users/quyixiao/gita/eb-service-api/api-web/api-interface/src/test/resources/aaa.jpg";
String base64 = toBase64(source);
System.out.println("长度: " + base64.length());
System.out.println(base64);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB