package jnpf.base.model.ocr.model; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import jnpf.base.model.ocr.OcrModel; import jnpf.base.model.ocr.OcrResModel; import jnpf.util.DateUtil; import jnpf.util.StringUtil; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * ocr解析发票模型 * * @author JNPF开发平台组 * @version v6.1.0 * @copyright 引迈信息技术有限公司 * @date 2025/7/29 16:58:47 */ @EqualsAndHashCode(callSuper = true) @Data @Schema(description = "ocr解析发票模型") @NoArgsConstructor public class InvoiceLicModel extends OcrModel { @Schema(description = "发票名称") private String title; @Schema(description = "发票代码") private String code; @Schema(description = "发票号码") private String number; @Schema(description = "开票日期") private Long date; @Schema(description = "校验码") private String checkCode; @Schema(description = "密码区") private String password; @Schema(description = "购买方名称") private String buyerName; @Schema(description = "购买方纳税人识别号") private String buyerTaxId; @Schema(description = "购买方地址/电话") private String buyerContact; @Schema(description = "购买方开户行及账号") private String buyerBankAccount; @Schema(description = "合计金额") private String totalAmount; @Schema(description = "合计税额") private String totalTaxAmount; @Schema(description = "价税合计(大写)") private String totalAmountUpper; @Schema(description = "价税合计(小写)") private String totalAmountLower; @Schema(description = "销售方名称") private String sellerName; @Schema(description = "销售方纳税人识别号") private String sellerTaxId; @Schema(description = "销售方地址/电话") private String sellerContact; @Schema(description = "销售方开户行及账号") private String sellerBankAccount; @Schema(description = "备注") private String remarks; @Schema(description = "收款人") private String payee; @Schema(description = "复核人") private String reviewer; @Schema(description = "开票人") private String issuer; @Schema(description = "项目详情") private List invoiceDetail = new ArrayList<>(); @Schema(description = "最大宽度") @JsonIgnore private Integer widthMax = 0; @Schema(description = "最大高度") @JsonIgnore private Integer heightMax = 0; @Override public void extract(String url, JSONObject body) { JSONObject systemJson = this.getSystemJson(url, body); List param = getParam(systemJson); if (StringUtil.isEmpty(this.getContent()) || !this.getContent().contains("发票")) { return; } //两种发票--带和不带密码区 if (this.getStrList().contains("密") || this.getStrList().contains("码") || this.getStrList().contains("区") || this.getContent().contains("校验码")) { this.setInfoOne(param); } else { this.setInfoTwo(param); } this.setContent(null); this.setStrList(null); } //数据转换 private List getParam(JSONObject obj) { //识别 List list = new ArrayList<>(); List listJosn = new ArrayList<>(); if (obj != null && obj.containsKey("status") && Objects.equals("000", String.valueOf(obj.get("status")))) { JSONArray results = obj.getJSONArray("results"); for (Object result : results) { JSONArray m = (JSONArray) result; for (Object n : m) { JSONObject o = (JSONObject) n; list.add(o.getString("text")); if (StringUtil.isEmpty(o.getString("text"))) continue; OcrResModel resModel = new OcrResModel(); resModel.setText(o.getString("text").replace(" ", "")); resModel.setConfidence(o.getBigDecimal("confidence")); JSONArray textRegion = o.getJSONArray("text_region"); if (textRegion.size() == 4) { JSONArray o1 = (JSONArray) textRegion.get(0); JSONArray o2 = (JSONArray) textRegion.get(2); resModel.setX1(o1.getInteger(0)); resModel.setY1(o1.getInteger(1)); resModel.setX2(o2.getInteger(0)); resModel.setY2(o2.getInteger(1)); if (resModel.getX1() > this.widthMax) { this.widthMax = resModel.getX1(); } if (resModel.getX2() > this.widthMax) { this.widthMax = resModel.getX2(); } if (resModel.getY1() > this.heightMax) { this.heightMax = resModel.getX1(); } if (resModel.getY2() > this.heightMax) { this.heightMax = resModel.getX1(); } listJosn.add(resModel); } } } } this.setStrList(list); this.setContent(String.join("", list)); return listJosn; } //第一种发票类型 private void setInfoOne(List param) { //购买方底部线 Integer h1 = param.stream().filter(t -> "单位".equals(t.getText()) || "数量".equals(t.getText()) || "单价".equals(t.getText())) .collect(Collectors.groupingBy(OcrResModel::getY1, Collectors.counting())) .entrySet().stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse(0); //货物服务清单以上部分 List collect1 = param.stream().filter(t -> t.getY1() < h1).collect(Collectors.toList()); //头部信息 topInfo(collect1); //购买方信息 buyerInfo(collect1); //货物服务清单以下部分 List collect2 = param.stream().filter(t -> t.getY1() >= h1 - 10).collect(Collectors.toList()); //统计和备注 countAndRemark(collect2); //销售方信息 sellerDown(collect2); } //字段解析 private String getStr(String key, OcrResModel item, List list, Integer index) { String value = ""; String text = item.getText(); if (text.contains(key)) { if (text.endsWith(":")) { if ((index + 1) < list.size() && list.get(index + 1) != null && list.get(index + 1).getY1() < item.getY2()) { value = list.get(index + 1).getText(); } } else { value = text.substring(text.indexOf(":") + 1); } } return value; } //上部分 private void topInfo(List collect1) { int xMid = widthMax / 2; int[] hSet = collect1.stream().filter(t -> "名".equals(t.getText()) || t.getText().contains("称:") || t.getText().startsWith("名称")) .map(OcrResModel::getY1).collect(Collectors.toSet()).stream().mapToInt(Integer::intValue).toArray(); if (hSet.length > 0) { //购买方名称以上部分 List collect3 = collect1.stream().filter(t -> t.getY2() < hSet[0]).collect(Collectors.toList()); OcrResModel ocrResModel1 = collect3.stream().filter(t -> t.getX1() < xMid && t.getX2() > xMid).findFirst().orElse(null); title = ocrResModel1 != null ? ocrResModel1.getText() : null; int i = 0; for (OcrResModel item : collect3) { if (StringUtil.isEmpty(code)) code = getStr("代码:", item, collect3, i); if (StringUtil.isEmpty(number)) number = getStr("号码:", item, collect3, i); if (StringUtil.isEmpty(number) && item.getText().startsWith("No")) { number = item.getText().substring(2); } if (date == null) { String dateStr = getStr("开票", item, collect3, i); if (StringUtil.isNotEmpty(dateStr)) { dateStr = dateStr.length() > 11 ? dateStr.substring(dateStr.length() - 11) : dateStr; Date date1 = DateUtil.checkDate(dateStr, "yyyy年MM月dd日"); date = date1 != null ? date1.getTime() : null; } } if (StringUtil.isEmpty(checkCode)) checkCode = getStr("校验码", item, collect3, i); i++; } //补充,如果识别不到代码和号码 if (StringUtil.isEmpty(code)) { for (OcrResModel item : collect3) { if (Pattern.compile("\\d{10,12}").matcher(item.getText()).matches()) { code = item.getText(); return; } } } } } //购买方 private void buyerInfo(List collect1) { int xMid = widthMax / 2; OcrResModel namModel = collect1.stream().filter(t -> "名".equals(t.getText()) || t.getText().contains("称:") || "名称:".equals(t.getText())).findFirst().orElse(null); //购买方部分 List collect4 = namModel != null ? collect1.stream().filter(t -> t.getY2() > namModel.getY1() - 5).collect(Collectors.toList()) : collect1; //购买方左边 List collect5 = collect4.stream().filter(t -> t.getX1() < xMid).collect(Collectors.toList()); int j = 0; for (OcrResModel item : collect5) { if (StringUtil.isEmpty(buyerName)) buyerName = getStr("称:", item, collect5, j); if (StringUtil.isEmpty(buyerTaxId)) buyerTaxId = getStr("识别号:", item, collect5, j); if (StringUtil.isEmpty(buyerContact)) buyerContact = getStr("电话:", item, collect5, j); if (StringUtil.isEmpty(buyerBankAccount)) buyerBankAccount = getStr("标识:", item, collect5, j); if (StringUtil.isEmpty(buyerBankAccount)) buyerBankAccount = getStr("账号:", item, collect5, j); j++; } //购买方右边 List collect6 = collect4.stream().filter(t -> t.getX1() > xMid).collect(Collectors.toList()); OcrResModel ocrResModel = collect6.stream().filter(t -> "密".equals(t.getText()) || "码".equals(t.getText()) || "区".equals(t.getText())).findFirst().orElse(null); if (ocrResModel != null) { password = collect6.stream().filter(t -> t.getX1() > ocrResModel.getX2()).map(OcrResModel::getText).collect(Collectors.joining()); } else { password = collect6.stream().map(OcrResModel::getText).collect(Collectors.joining()); } } //下部分 private void countAndRemark(List collect2) { OcrResModel countModel = collect2.stream().filter(t -> "合".equals(t.getText()) || "计".equals(t.getText())).findFirst().orElse(null); if (countModel != null) { int indexCount = collect2.indexOf(countModel); if (collect2.get(indexCount + 1).getX1() < widthMax / 2) { indexCount = indexCount + 1; } totalAmount = collect2.get(indexCount + 1).getText(); totalTaxAmount = collect2.get(indexCount + 2).getText(); if (StringUtil.isNotEmpty(totalAmount) && !Character.isDigit(totalAmount.charAt(0))) { totalAmount = totalAmount.substring(1); } if (StringUtil.isNotEmpty(totalTaxAmount) && !Character.isDigit(totalTaxAmount.charAt(0))) { totalTaxAmount = totalTaxAmount.substring(1); } //中间部分数组 List itemCollect = collect2.stream().filter(t -> t.getY2() < countModel.getY1()).collect(Collectors.toList()); itemInfo(itemCollect); } OcrResModel countUpper = collect2.stream().filter(t -> "价税合计(大写)".equals(t.getText()) || t.getText().contains("税合")).findFirst().orElse(null); if (countUpper != null) { int indexCountUpper = collect2.indexOf(countUpper); totalAmountUpper = collect2.get(indexCountUpper + 1).getText(); String tal = collect2.get(indexCountUpper + 2).getText(); if (tal.endsWith("写)")) { totalAmountLower = collect2.get(indexCountUpper + 3).getText(); } else { totalAmountLower = tal.contains(")") && tal.split(")").length > 1 ? tal.split(")")[1] : tal; } if (StringUtil.isNotEmpty(totalAmountLower) && !Character.isDigit(totalAmountLower.charAt(0))) { totalAmountLower = totalAmountLower.substring(1); } } int downIndex = countUpper != null ? countUpper.getY2() : heightMax / 2; //下面部分数组 List downCollect = collect2.stream().filter(t -> t.getY2() > downIndex).collect(Collectors.toList()); int i = 0; for (OcrResModel item : downCollect) { if (StringUtil.isEmpty(payee)) { String str = getStr("收款人:", item, downCollect, i); if (StringUtil.isNotEmpty(str) && !str.contains("复核")) { payee = str; } } if (StringUtil.isEmpty(reviewer)) { String str = getStr("复核:", item, downCollect, i); if (StringUtil.isNotEmpty(str) && !str.contains("开票人")) { reviewer = str; } } if (StringUtil.isEmpty(issuer)) issuer = getStr("开票人:", item, downCollect, i); i++; } //备注 OcrResModel remarkM = collect2.stream().filter(t -> "备".equals(t.getText()) || "注".equals(t.getText())).findFirst().orElse(null); if (remarkM != null) { OcrResModel remarkLimit = collect2.stream().filter(t -> t.getText().contains("开票人")).findFirst().orElse(null); List collect = downCollect.stream().filter(t -> { if (remarkLimit != null) { return t.getX1() > remarkM.getX2() && t.getY1() > downIndex && t.getY1() < remarkLimit.getY1(); } return t.getX1() > remarkM.getX2() && t.getY1() > downIndex; }).collect(Collectors.toList()); remarks = collect.stream().map(OcrResModel::getText).collect(Collectors.joining()); } } //销售方 private void sellerDown(List collect) { OcrResModel countUpper = collect.stream().filter(t -> "价税合计(大写)".equals(t.getText()) || "税合".equals(t.getText())).findFirst().orElse(null); int downIndex = countUpper != null ? countUpper.getY2() : heightMax / 2; //下面部分数组 List downCollect = collect.stream().filter(t -> t.getY2() > downIndex).collect(Collectors.toList()); int i = 0; for (OcrResModel item : downCollect) { if (StringUtil.isEmpty(sellerName)) sellerName = getStr("称:", item, downCollect, i); if (StringUtil.isEmpty(sellerTaxId)) sellerTaxId = getStr("识别号:", item, downCollect, i); if (StringUtil.isEmpty(sellerContact)) sellerContact = getStr("电话:", item, downCollect, i); if (StringUtil.isEmpty(sellerBankAccount)) sellerBankAccount = getStr("标识:", item, downCollect, i); if (StringUtil.isEmpty(sellerBankAccount)) sellerBankAccount = getStr("账号:", item, downCollect, i); i++; } } //中间物资信息 private void itemInfo(List itemCollect) { OcrResModel itemTitle = itemCollect.stream().filter(t -> "单位".equals(t.getText()) || "数量".equals(t.getText()) || "税率".equals(t.getText())).findFirst().orElse(null); OcrResModel nameT = itemCollect.stream().filter(t -> t.getText().contains("服务名称") || t.getText().contains("货物或") || t.getText().contains("项目名称")).findFirst().orElse(null); OcrResModel typeT = itemCollect.stream().filter(t -> t.getText().equals("规格型号") || t.getText().contains("型号")).findFirst().orElse(null); OcrResModel unitT = itemCollect.stream().filter(t -> t.getText().equals("单位")).findFirst().orElse(null); OcrResModel numT = itemCollect.stream().filter(t -> t.getText().equals("数量")).findFirst().orElse(null); if (numT == null) { numT = itemCollect.stream().filter(t -> t.getText().equals("数")).findFirst().orElse(null); } OcrResModel priceT = itemCollect.stream().filter(t -> t.getText().equals("单价")).findFirst().orElse(null); OcrResModel amountT = itemCollect.stream().filter(t -> t.getText().equals("金额")).findFirst().orElse(null); if (amountT == null) { amountT = itemCollect.stream().filter(t -> t.getText().equals("金")).findFirst().orElse(null); } OcrResModel rateT = itemCollect.stream().filter(t -> t.getText().contains("税率") || t.getText().equals("税单")).findFirst().orElse(null); OcrResModel taxAmountT = itemCollect.stream().filter(t -> t.getText().equals("税额")).findFirst().orElse(null); if (taxAmountT == null) { taxAmountT = itemCollect.stream().filter(t -> t.getText().equals("税")).findFirst().orElse(null); } if (nameT == null || typeT == null || unitT == null || numT == null || priceT == null || amountT == null || rateT == null || taxAmountT == null) { return; } List eachModel = new ArrayList<>(); eachModel.add(0, nameT); eachModel.add(1, typeT); eachModel.add(2, unitT); eachModel.add(3, numT); eachModel.add(4, priceT); eachModel.add(5, amountT); eachModel.add(6, rateT); eachModel.add(7, taxAmountT); eachModel.add(8, itemTitle); itemInfoParse(itemCollect, eachModel); } private void itemInfoParse(List itemCollect, List eachModel) { List nameList = new ArrayList<>();//品名 List typeList = new ArrayList<>();//类型 List unitList = new ArrayList<>();//单位 List numList = new ArrayList<>();//数量 List priceList = new ArrayList<>();//单价 List amountList = new ArrayList<>();//金额 List rateList = new ArrayList<>();//税率 List taxAmountList = new ArrayList<>();//税额 List collect = itemCollect.stream().filter(t -> t.getY1() > eachModel.get(8).getY2() - 8).collect(Collectors.toList());//误差给8 for (OcrResModel item : collect) { if (item.getX2() > eachModel.get(0).getX1() && item.getX1() < (eachModel.get(1).getX1() + eachModel.get(0).getX2()) / 2) { nameList.add(item); } if (item.getX1() > (eachModel.get(1).getX1() + eachModel.get(0).getX2()) / 2 && item.getX1() < (eachModel.get(2).getX1() + eachModel.get(1).getX2()) / 2) { typeList.add(item); } if (item.getX1() > (eachModel.get(2).getX1() + eachModel.get(1).getX2()) / 2 && item.getX1() < (eachModel.get(3).getX1() + eachModel.get(2).getX2()) / 2) { unitList.add(item); } if (item.getX1() > (eachModel.get(3).getX1() + eachModel.get(2).getX2()) / 2 && item.getX1() < (eachModel.get(4).getX1() + eachModel.get(3).getX2()) / 2) { numList.add(item); } if (item.getX1() > (eachModel.get(4).getX1() + eachModel.get(3).getX2()) / 2 && item.getX1() < (eachModel.get(5).getX1() + eachModel.get(4).getX2()) / 2) { priceList.add(item); } if (item.getX1() > (eachModel.get(5).getX1() + eachModel.get(4).getX2()) / 2 && item.getX1() < (eachModel.get(6).getX1() + eachModel.get(6).getX1()) / 2) { amountList.add(item); } if (item.getX1() > (eachModel.get(6).getX1() + eachModel.get(6).getX1()) / 2 && item.getX1() < eachModel.get(7).getX1()) { if (StringUtil.isNotEmpty(item.getText()) && !Character.isDigit(item.getText().length() - 1)) { item.setText(item.getText().substring(0, item.getText().length() - 1)); } rateList.add(item); } if (item.getX1() > eachModel.get(7).getX1()) { taxAmountList.add(item); } } Map> map = new HashMap<>(); map.put("nameList", nameList); map.put("typeList", typeList); map.put("unitList", unitList); map.put("numList", numList); map.put("priceList", priceList); map.put("amountList", amountList); map.put("rateList", rateList); map.put("taxAmountList", taxAmountList); invoiceDetail = InvoiceDetail.getList(map); } //+++++++++++++++++++++++++++++++++++++++++第二种发票类型+++++++++++++++++++++++++++++++++++++++ //第二种发票类型 private void setInfoTwo(List param) { //购买方底部线 Integer h1 = param.stream().filter(t -> "单位".equals(t.getText()) || "数量".equals(t.getText()) || "单价".equals(t.getText())) .collect(Collectors.groupingBy(OcrResModel::getY1, Collectors.counting())) .entrySet().stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse(0); //货物服务清单以上部分 List collect1 = param.stream().filter(t -> t.getY2() < h1).collect(Collectors.toList()); //头部信息 topInfo(collect1); //购买方信息 和销售方信息 buyerAndSeller(collect1); //货物服务清单以下部分 List collect2 = param.stream().filter(t -> t.getY1() >= h1 - 10).collect(Collectors.toList()); //统计和备注 countAndRemark(collect2); } //第二种发票购买方和销售方 private void buyerAndSeller(List upList) { int xMid = widthMax / 2; OcrResModel namModel = upList.stream().filter(t -> t.getText().startsWith("名称:")).findFirst().orElse(null); if (namModel == null) return; //购买方部分 List collect4 = upList.stream().filter(t -> t.getY2() > namModel.getY1() - 10).collect(Collectors.toList()); //购买方左边 List collect5 = collect4.stream().filter(t -> t.getX1() < xMid).collect(Collectors.toList()); int i = 0; for (OcrResModel item : collect5) { if (StringUtil.isEmpty(buyerName)) buyerName = getStr("名称:", item, collect5, i); if (StringUtil.isEmpty(buyerTaxId)) buyerTaxId = getStr("识别号:", item, collect5, i); i++; } //购买方右边 List collect6 = collect4.stream().filter(t -> t.getX1() > xMid).collect(Collectors.toList()); int j = 0; for (OcrResModel item : collect6) { if (StringUtil.isEmpty(sellerName)) sellerName = getStr("名称:", item, collect5, j); if (StringUtil.isEmpty(sellerTaxId)) sellerTaxId = getStr("识别号:", item, collect5, j); j++; } } }