package com.visy.service;
import com.baidu.aip.util.Base64Util;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.visy.exception.ErrorEnum;
import com.visy.exception.BusinessException;
import com.visy.config.BaiduConfig;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 百度图片识别自定义实现(替代AipOcr)
* @author visy.wang
* @date 2022/1/19 9:24
*/
@Slf4j
@Service
public class AipOcrClient {
private static long expireTime = 0L; //token过期时间戳
private static String accessToken = null; //token值
@Autowired
private BaiduConfig baiduConfig; //百度认证信息,配置自己的apiKey和secretKey
@Autowired
private RestTemplate restTemplate;
//Url管理枚举
@Getter
@AllArgsConstructor
enum UrlKey {
ACCESS_TOKEN("https://aip.baidubce.com/oauth/2.0/token", "认证地址"),
DRIVING_LICENSE("https://aip.baidubce.com/rest/2.0/ocr/v1/driving_license", "驾驶证识别地址"),
VEHICLE_LICENSE("https://aip.baidubce.com/rest/2.0/ocr/v1/vehicle_license", "行驶证识别地址");
private final String url;
private final String desc;
}
/**
* 驾驶证识别
* @param image 图片(二进制)
* @param options 其他参数
* @return 识别结果
*/
public JSONObject drivingLicense(byte[] image, HashMap<String,String> options){
options.put("image", Base64Util.encode(image));
return request(UrlKey.DRIVING_LICENSE, options);
}
/**
* 驾驶证识别
* @param imageUrl 图片地址
* @param options 其他参数
* @return 识别结果
*/
public JSONObject drivingLicense(String imageUrl, HashMap<String,String> options){
options.put("url", imageUrl);
return request(UrlKey.DRIVING_LICENSE, options);
}
/**
* 行驶证识别
* @param image 图片(二进制)
* @param options 其他参数
* @return 识别结果
*/
public JSONObject vehicleLicense(byte[] image, HashMap<String,String> options){
options.put("image", Base64Util.encode(image));
return request(UrlKey.VEHICLE_LICENSE, options);
}
/**
* 行驶证识别
* @param imageUrl 图片地址
* @param options 其他参数
* @return 识别结果
*/
public JSONObject vehicleLicense(String imageUrl, HashMap<String,String> options){
options.put("url", imageUrl);
return request(UrlKey.VEHICLE_LICENSE, options);
}
//带Token过期检查的request,暂不需要,现已计算过期时间并在失效前重新获取
private JSONObject requestWithTokenCheck(UrlKey urlKey, Map<String,String> options){
JSONObject res = request(urlKey, options);
if(res.has("error_code")){
int errCode = res.getInt("error_code");
if(errCode==110 || errCode==111){
accessToken = null; //Token失效,清除并重新获取
return request(urlKey, options);
}
}
return res;
}
/**
* 请求
* @param urlKey url地址枚举
* @param options 其他参数
* @return 识别结果
*/
private JSONObject request(UrlKey urlKey, Map<String,String> options){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
String url = urlKey.getUrl();
MultiValueMap<String, String> queryStr = getQueryStr(urlKey);
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(url).queryParams(queryStr);
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
if(Objects.nonNull(options) && !options.isEmpty()){
options.forEach(formData::add);
}
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(formData, headers);
ResponseEntity<String> res = restTemplate.postForEntity(uriBuilder.toUriString(), request, String.class);
if(!HttpStatus.OK.equals(res.getStatusCode())){
throw buzError("请求失败:httpStatus=" + res.getStatusCode());
}
String resBody = res.getBody();
if(StringUtils.isBlank(resBody)){
throw buzError("响应体为空");
}
return new JSONObject(resBody);
}
private MultiValueMap<String, String> getQueryStr(UrlKey urlKey){
MultiValueMap<String, String> queryStr = new LinkedMultiValueMap<>();
if(UrlKey.ACCESS_TOKEN.equals(urlKey)){
queryStr.add("grant_type", "client_credentials");
queryStr.add("client_id", baiduConfig.getApiKey());
queryStr.add("client_secret", baiduConfig.getSecretKey());
}else{
queryStr.add("access_token", getAccessToken()); //添加认证信息
}
return queryStr;
}
/**
* 获取AccessToken
* @return AccessToken
*/
private String getAccessToken(){
long now = System.currentTimeMillis();
if(StringUtils.isNotBlank(accessToken) && now<expireTime){
//有缓存并在有效期内,直接使用
return accessToken;
}
try{
return getAccessToken(now);
}catch (Exception e){
throw buzError("AccessToken获取失败:" + e.getMessage());
}
}
private String getAccessToken(long now){
log.info("正在重新获取AccessToken...");
JSONObject body = request(UrlKey.ACCESS_TOKEN, null);
if(body.has("error")){
throw buzError(body.getString("error_description"));
}
int tokenExpires = body.getInt("expires_in"); //有效期,单位秒,默认30天
expireTime = now + (tokenExpires-86400L)*1000L; //提前1天失效
accessToken = body.getString("access_token"); //缓存token
log.info("认证成功,有效期至:{}", new Date(expireTime));
return accessToken;
}
/**
* 抛出业务异常
* @param errMsg 错误信息
* @return 业务异常
*/
private BusinessException buzError(String errMsg){
//BusinessException: 自定义业务异常
return new BusinessException(ErrorEnum.BUSINESS_ERR.getCode(), errMsg);
}
}
package com.visy.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author visy.wang
* @date 2022/1/19 9:28
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "baidu")
public class BaiduConfig {
private String appId;
private String apiKey;
private String secretKey;
}