feat(portal): 添加OCR识别身份证、银行卡功能

- 在 ApplicationConfigConstant 中添加 OCR 相关的配置常量
- 新增 BankCardRecognitionAction 类实现银行卡识别功能- 新增 IDCardRecognitionAction 类实现身份证识别功能
- 添加 DataConvertMapper 接口和对应的 XML 文件用于数据转换
This commit is contained in:
dxfeng 2025-07-29 17:04:15 +08:00
parent b4835fc6a9
commit 01daf17c15
5 changed files with 355 additions and 0 deletions

View File

@ -0,0 +1,99 @@
package com.weaver.seconddev.portal.action.entry;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.weaver.common.base.entity.result.WeaResult;
import com.weaver.esb.api.rpc.EsbServerlessRpcRemoteInterface;
import com.weaver.eteams.file.client.file.FileData;
import com.weaver.eteams.file.client.file.FileObj;
import com.weaver.file.ud.api.FileDownloadService;
import com.weaver.seconddev.portal.constant.ApplicationConfigConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/**
* @author:dxfeng
* @createTime: 2025/07/29
* @version: 1.0
*/
@Slf4j
@Service("bankCardRecognitionAction")
public class BankCardRecognitionAction implements EsbServerlessRpcRemoteInterface {
@Autowired
FileDownloadService fileDownloadService;
@Override
public WeaResult<Map<String, Object>> execute(Map<String, Object> params) {
Map<String, Object> returnMap = new HashMap<>();
Long fileId = Convert.toLong(params.get("fileId"), null);
log.error("fileId==" + fileId);
if (null == fileId) {
return WeaResult.success(returnMap);
}
FileData fileData = fileDownloadService.downloadFile(fileId);
FileObj fileObj = fileData.getFileObj();
InputStream inputStream = fileData.getInputStream();
String fileName = fileObj.getName();
log.error("fileName==" + fileName);
try {
log.error("inputStream==" + inputStream.available());
} catch (IOException e) {
log.error("inputStream获取异常", e);
throw new RuntimeException(e);
}
String response = callBankCardOcrApi(inputStream, fileName);
log.error("response==" + response);
// 正面响应数据
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.getBoolean("isSuccess")) {
JSONObject data = jsonObject.getJSONObject("data");
JSONArray resultArray = data.getJSONArray("result");
if (resultArray.size() > 0) {
JSONObject result = resultArray.getJSONObject(0);
returnMap.put("org", result.get("org"));
returnMap.put("number", result.get("number"));
returnMap.put("valid_thru", result.get("valid_thru"));
returnMap.put("type", result.get("type"));
returnMap.put("valid_from", result.get("valid_from"));
returnMap.put("holder", result.get("holder"));
}
}
log.error("returnMap==" + JSON.toJSONString(returnMap));
return WeaResult.success(returnMap);
}
/**
* 调用千里聆身份证识别接口
*
* @param inputStream 文件输入流
* @param fileName 文件名
* @return
*/
public static String callBankCardOcrApi(InputStream inputStream, String fileName) {
byte[] bytes = IoUtil.readBytes(inputStream);
long currentTime = System.currentTimeMillis();
HttpResponse response = HttpRequest.post(ApplicationConfigConstant.BANK_CARD_OCR_URL)
.header("Content-Type", "multipart/form-data")
.header("sign", IDCardRecognitionAction.getSign(currentTime))
.header("appId", ApplicationConfigConstant.OCR_APP_ID)
.header("timestamp", String.valueOf(currentTime))
.form("image_file", bytes, fileName)
.execute();
return response.body();
}
}

View File

@ -0,0 +1,208 @@
package com.weaver.seconddev.portal.action.entry;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.weaver.common.base.entity.result.WeaResult;
import com.weaver.esb.api.rpc.EsbServerlessRpcRemoteInterface;
import com.weaver.eteams.file.client.file.FileData;
import com.weaver.eteams.file.client.file.FileObj;
import com.weaver.file.ud.api.FileDownloadService;
import com.weaver.seconddev.portal.constant.ApplicationConfigConstant;
import com.weaver.seconddev.portal.entity.param.BaseParam;
import com.weaver.seconddev.portal.mapper.dictionary.DataConvertMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* @author:dxfeng
* @createTime: 2025/07/29
* @version: 1.0
*/
@Slf4j
@Service("idCardRecognitionAction")
public class IDCardRecognitionAction implements EsbServerlessRpcRemoteInterface {
@Autowired
FileDownloadService fileDownloadService;
@Autowired
DataConvertMapper dataConvertMapper;
BaseParam baseParam = new BaseParam();
@Override
public WeaResult<Map<String, Object>> execute(Map<String, Object> params) {
Map<String, Object> returnMap = new HashMap<>();
Long fileId = Convert.toLong(params.get("fileId"), null);
log.error("fileId==" + fileId);
String nationalityConvert = Convert.toStr(params.get("nationalityConvert"), "");
log.error("nationalityConvert==" + nationalityConvert);
if (null == fileId) {
return WeaResult.success(returnMap);
}
FileData fileData = fileDownloadService.downloadFile(fileId);
FileObj fileObj = fileData.getFileObj();
InputStream inputStream = fileData.getInputStream();
String fileName = fileObj.getName();
log.error("fileName==" + fileName);
try {
log.error("inputStream==" + inputStream.available());
} catch (IOException e) {
log.error("inputStream获取异常", e);
throw new RuntimeException(e);
}
String response = callIdCardOcrApi(inputStream, fileName);
// 正面响应数据
//String response = "{\"isSuccess\": true, \"data\": {\"page_num\": \"1\", \"result\": [{\"姓名\": \"王某某\", \"性别\": \"\", \"民族\": \"\", \"出生\": \"1989年3月21日\", \"住址\": \"上海市浦东新区塘桥街道蓝村路xxx号\", \"公民身份号码\": \"370112198903217890\", \"标签\": \"头像面\"}]}, \"status_code\": 5200}";
log.error("response==" + response);
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.getBoolean("isSuccess")) {
JSONObject data = jsonObject.getJSONObject("data");
JSONArray resultArray = data.getJSONArray("result");
if (resultArray.size() > 0) {
JSONObject result = resultArray.getJSONObject(0);
returnMap.put("name", result.getString("姓名"));
returnMap.put("sex", result.getString("性别"));
String nation = result.getString("民族");
if (StringUtils.isNotBlank(nation) && StringUtils.isNotBlank(nationalityConvert)) {
Long nationId = dataConvertMapper.getIdByName(baseParam, "nation", nation + "");
log.error("nationId==" + nationId);
nation = Convert.toStr(nationId, "");
log.error("nation==" + nation);
}
returnMap.put("nation", nation);
returnMap.put("birthday", Convert.toStr(result.getString("出生"), "").replace("", "-").replace("", "-").replace("", "-"));
returnMap.put("address", result.getString("住址"));
String idNumber = result.getString("公民身份号码");
returnMap.put("idCard", idNumber);
// 根据身份证号计算年龄 性别
if (StringUtils.isNotBlank(idNumber)) {
returnMap.put("age", getAge(idNumber));
returnMap.put("gender", getGender(idNumber));
}
returnMap.put("issueAuthority", result.getString("签发机关"));
returnMap.put("validity", result.getString("有效期限"));
}
}
log.error("returnMap==" + JSON.toJSONString(returnMap));
return WeaResult.success(returnMap);
}
/**
* 调用千里聆身份证识别接口
*
* @param inputStream 文件输入流
* @param fileName 文件名
* @return
*/
public static String callIdCardOcrApi(InputStream inputStream, String fileName) {
byte[] bytes = IoUtil.readBytes(inputStream);
long currentTime = System.currentTimeMillis();
HttpResponse response = HttpRequest.post(ApplicationConfigConstant.ID_CARD_OCR_URL)
.header("Content-Type", "multipart/form-data")
.header("sign", getSign(currentTime))
.header("appId", ApplicationConfigConstant.OCR_APP_ID)
.header("timestamp", String.valueOf(currentTime))
.form("img", bytes, fileName)
.execute();
return response.body();
}
/**
* 千里聆签名
*
* @param timestamp 当前时间戳(毫秒数)
* @return
*/
public static String getSign(long timestamp) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(ApplicationConfigConstant.OCR_APP_ID.getBytes());
md5.update((timestamp + "").getBytes());
md5.update(ApplicationConfigConstant.OCR_APP_SECRET.getBytes());
byte[] bytes = md5.digest();
return (new BigInteger(1, bytes)).toString(16);
} catch (NoSuchAlgorithmException var8) {
throw new RuntimeException("不支持的加密算法", var8);
}
}
/**
* 根据身份证号获取性别
*
* @param idCard 身份证号
* @return 性别""""
* @throws IllegalArgumentException 身份证号不合法时抛出
*/
public static String getGender(String idCard) {
// 校验身份证号长度
if (idCard == null || (idCard.length() != 18 && idCard.length() != 15)) {
throw new IllegalArgumentException("身份证号长度不合法");
}
// 18位身份证取第17位15位身份证取第15位
char genderChar;
if (idCard.length() == 18) {
genderChar = idCard.charAt(16);
} else {
genderChar = idCard.charAt(14);
}
// 奇数为男偶数为女
return (Integer.parseInt(String.valueOf(genderChar)) % 2 == 1) ? "male" : "female";
}
/**
* 根据身份证号计算年龄
*
* @param idCard 身份证号
* @return 年龄
* @throws IllegalArgumentException 身份证号不合法时抛出
*/
public static int getAge(String idCard) {
// 校验身份证号长度
if (idCard == null || (idCard.length() != 18 && idCard.length() != 15)) {
throw new IllegalArgumentException("身份证号长度不合法");
}
// 解析出生日期
LocalDate birthDate;
if (idCard.length() == 18) {
// 18位身份证第7-14位为出生日期yyyyMMdd
String birthStr = idCard.substring(6, 14);
birthDate = LocalDate.parse(birthStr, DateTimeFormatter.ofPattern("yyyyMMdd"));
} else {
// 15位身份证第7-12位为出生日期yyMMdd默认19xx年
String birthStr = "19" + idCard.substring(6, 12);
birthDate = LocalDate.parse(birthStr, DateTimeFormatter.ofPattern("yyyyMMdd"));
}
// 计算与当前日期的差距
LocalDate now = LocalDate.now();
Period period = Period.between(birthDate, now);
return period.getYears();
}
}

View File

@ -23,4 +23,17 @@ public class ApplicationConfigConstant {
* 组织中心APP Secret * 组织中心APP Secret
*/ */
public static final String ORGANIZATION_APP_SECRET = "332ed6328a15f6189efa4d2ac5935bc1"; public static final String ORGANIZATION_APP_SECRET = "332ed6328a15f6189efa4d2ac5935bc1";
/**
* 千里聆服务APP ID
*/
public static final String OCR_APP_ID = "6ou6wvl8";
/**
* 千里聆服务APP SECRET
*/
public static final String OCR_APP_SECRET = "53fc247ffe4f3e8d6c96a5d0a9a222a7";
public static final String ID_CARD_OCR_URL = "https://open.easst.cn/openapi/rest/common/idcardocr";
public static final String BANK_CARD_OCR_URL = "https://open.easst.cn/openapi/rest/common/bank_card_ocr";
} }

View File

@ -0,0 +1,23 @@
package com.weaver.seconddev.portal.mapper.dictionary;
import com.weaver.seconddev.portal.entity.param.BaseParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* @author:dxfeng
* @createTime: 2025/07/29
* @version: 1.0
*/
@Mapper
public interface DataConvertMapper {
/**
* 根据名称类型获取ID
*
* @param param
* @param type
* @param name
* @return
*/
Long getIdByName(@Param("param") BaseParam param, @Param("type") String type, @Param("name") String name);
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.weaver.seconddev.portal.mapper.dictionary.DataConvertMapper">
<select id="getIdByName" resultType="java.lang.Long">
select t1.id from ${param.e10_other_business}.hr_dictionary_setting t1
where t1.delete_type = 0 and t1.tenant_key = #{param.tenantKey}
and t1.type = #{type}
and t1.name = #{name}
</select>
</mapper>