package com.malk.guyuan.controller; import com.alibaba.fastjson.JSON; import com.malk.guyuan.server.model.McInvoiceDto; import com.malk.guyuan.server.model.McInvoiceKind; import com.malk.guyuan.server.tencent.TXYConf; import com.malk.guyuan.service.tencent.TXYInvoice; import com.malk.server.aliwork.YDConf; import com.malk.server.aliwork.YDParam; import com.malk.server.common.FilePath; import com.malk.server.common.McException; import com.malk.server.common.McR; import com.malk.service.aliwork.YDClient; import com.malk.service.aliwork.YDService; import com.malk.utils.*; import com.spire.pdf.PdfCompressionLevel; import com.spire.pdf.PdfDocument; import com.spire.pdf.PdfPageBase; import com.spire.pdf.exporting.PdfImageInfo; import com.spire.pdf.graphics.PdfBitmap; import com.tencentcloudapi.common.exception.TencentCloudSDKException; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import net.coobird.thumbnailator.Thumbnails; import org.apache.commons.lang3.StringUtils; import org.apache.poi.util.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.net.URL; import java.util.*; import java.util.stream.Collectors; /** * 错误抛出与拦截详见CatchException */ @Slf4j @RestController @RequestMapping public class IVController { @Autowired private TXYInvoice txyInvoice; @Autowired private YDClient ydClient; /// 优先获取字段, 新版本接口已支持字段返回 private String findValue(List> infos, String... names) { for (String name : names) { Optional optional = infos.stream().filter(info -> info.get("Name").equals(name)).findAny(); if (optional.isPresent()) { return String.valueOf(((Map) optional.get()).get("Value")); } } return ""; } // 兼容历史配置, 格式 (谷元) private String guyuanNameRepalce(String name) { if (name.contains("谷元")) { return UtilString.replaceBracketIsWhole(name); } else { return UtilString.replaceBracketIsSemiangle(name); } } /// url压缩转base64 @SneakyThrows private String imageUrlConvertBase64(String imageUrl) { ByteArrayOutputStream out = new ByteArrayOutputStream(); // scale(比例), outputQuality(质量) Thumbnails.fromURLs(Arrays.asList(new URL(imageUrl))).scale(0.5f).outputQuality(0.25f).toOutputStream(out); InputStream inputStream = new ByteArrayInputStream(out.toByteArray()); //转换为base64 byte[] bytes = IOUtils.toByteArray(inputStream); return Base64.getEncoder().encodeToString(bytes); } @Autowired private FilePath filePath; /// PDF压缩转base64 @SneakyThrows private String pdfUrlConvertBase64(String pdfUrl) { String fileName = "tmp_" + new Date().getTime() + ".pdf"; // 下载文件 File file = UtilFile.mkdirIfNot(fileName, filePath.getPath().getTmp()); UtilHttp.doDownload(pdfUrl, file); // PDF压缩 PdfDocument doc = new PdfDocument(); // 创建PdfDocument类的对象 doc.loadFromFile(file.getAbsolutePath()); // 加载PDF文档 doc.getFileInfo().setIncrementalUpdate(false); // 禁用增量更新 doc.setCompressionLevel(PdfCompressionLevel.Normal); // 将压缩级别设置为最佳 // 遍历文档页面 for (int i = 0; i < doc.getPages().getCount(); i++) { PdfPageBase page = doc.getPages().get(i); // 获取指定页面 PdfImageInfo[] images = page.getImagesInfo(); // 获取每个页面的图像信息集合 // 遍历集合中的所有项目 if (images != null && images.length > 0) for (int j = 0; j < images.length; j++) { PdfImageInfo image = images[j]; // 获取指定图片 PdfBitmap bp = new PdfBitmap(image.getImage()); bp.setQuality(30); // 设置压缩质量 page.replaceImage(j, bp); // 将原始图像替换为压缩图像 } } // 将结果文档保存至另一个PDF文档中: 覆盖 doc.saveToFile(file.getAbsolutePath()); doc.close(); // PDF转base64, 无需透出本地文件地址 String base64 = UtilFile.fileToBase64(file.getAbsolutePath()); // 删除临时PDF文件 UtilFile.deleteFile(file.getAbsolutePath()); return base64; } // prd 校验发票抬头, 购买方范围 private void validateBuyer(String BuyerName, String tips) { List corpNames = Arrays.asList( "谷元(上海)文化科技有限责任公司", "上海爱鱼文化传媒有限公司", "上海渔米可禧文化传媒有限公司", "上海巧豆文化传媒有限公司", "钰鸿文化创意(上海)有限公司", "上海攸元文化科技有限公司", "上海盈田演出经纪有限公司", "上海小蝠文化科技有限公司", "沐田居海(厦门)文化传媒有限公司", "北京元环文化科技有限责任公司", "厦门攸元文化科技有限公司", "厦门亨有文化科技有限公司", "厦门银雀思汀文化传媒有限公司", "上海渝泽信息科技有限公司", "厦门神谷飞流影视传媒有限公司", "厦门谷钛数字科技有限公司", "上海观情科技有限公司", "渔米可禧文化传媒(香港)有限公司"); McException.assertAccessException(!corpNames.contains(BuyerName), tips + ", 购买方名称不合法!"); } /** * 混票识别 [新版本] */ @PostMapping("invoice-iv2") McR invoice_iv2(@RequestBody Map data) throws TencentCloudSDKException { McException.assertParamException_Null(data, "url"); String image = ydClient.convertTemporaryUrl(data.get("url")); log.info("混票识别, 免登地址, {}", image); // 非PDF, 且内存大于3M, 压缩后上传 if (UtilMap.getFloat(data, "size") > 3.0f && !UtilMap.getBoolean(data, "isPdf")) { image = imageUrlConvertBase64(image); } if (UtilMap.getFloat(data, "size") > 6.0f && UtilMap.getBoolean(data, "isPdf")) { image = pdfUrlConvertBase64(image); } List invoices = (List) txyInvoice.doRecognizeGeneralInvoice(image).get("MixedInvoiceItems"); List result = invoices.stream().map(item -> { Map prop = UtilMap.getMap(UtilMap.getMap(item, "SingleInvoiceInfos"), UtilMap.getString(item, "SubType")); // ppExt: 通用字段定义 McInvoiceDto invoiceDto = McInvoiceDto.builder() .name(UtilMap.getString(item, "SubTypeDescription")) .kindName(UtilMap.getString(item, "TypeDescription")) .kind(UtilMap.getInt(item, "Type")) .code(UtilMap.getString(prop, "Code")) .serial(UtilMap.getString(prop, "Number")) .date(UtilString.replaceDateZH_cn(UtilMap.getString(prop, "Date"))) .checkCode(UtilMap.getString(prop, "CheckCode")) // ppExt: 多明细行时, 优先取值合计 [全电票返回了subTotal字段, 但值为空] .amount(UtilNumber.setBigDecimal(UtilMap.getString_first(prop, "SubTotal", "Total"))) .tax(UtilNumber.setBigDecimal(UtilMap.getString_first(prop, "SubTax", "Tax"))) .excludingTax(UtilNumber.setBigDecimal(UtilMap.getString(prop, "PretaxAmount"))) .buyerName(guyuanNameRepalce(UtilMap.getString(prop, "Buyer"))) // ppExt: 中央非税未返回税号官方说明: 非税发票理论是没有税号的,图片中属于信用代码 .buyerTaxId(UtilMap.getString(prop, "BuyerTaxID")) .sellerName(guyuanNameRepalce(UtilMap.getString_first(prop, "Seller", "Issuer"))) // 行程单: 填开单位 .sellerTaxId(UtilMap.getString_first(prop, "SellerTaxID", "AgentCode")) // 行程单: 销售单位代号 .passengerName(UtilMap.getString_first(prop, "Name", "UserName")) // 火车票, 行程单 // 交通出行 .seatType(UtilMap.getString(prop, "Seat")) .departureTime(UtilString.replaceDateZH_cn(UtilMap.getString(prop, "DateGetOn")) + " " + UtilMap.getString(prop, "TimeGetOn")) .departurePort(UtilMap.getString_first(prop, "StationGetOn", "Entrance", "Place")) // 火车票: 出发车站, 过路过桥费: 入口, 出租车: 发票所在地 .arrivePort(UtilMap.getString_first(prop, "StationGetOff", "Exit")) // 行程单, 火车票: 到达车站, 过路过桥费: 出口 .trainNo(UtilMap.getString_first(prop, "TrainNumber", "LicensePlate")) // 火车票: 车次, 出租车: 车牌号 .insuranceCosts(UtilNumber.setBigDecimal((UtilMap.getString(prop, "Insurance")))) // 行程单: 保险费 .fuelCosts(UtilNumber.setBigDecimal((UtilMap.getString(prop, "FuelSurcharge")))) // 行程单: 燃油附加费 .constructionCosts(UtilNumber.setBigDecimal((UtilMap.getString(prop, "AirDevelopmentFund")))) // 行程单: 民航发展基金 .build(); // ppExt: 机票行程单, 行程与座位信息在明细内 if ("机票行程单".equals(item.get("TypeDescription"))) { Map flight = (Map) UtilMap.getList(prop, "FlightItems").get(0); invoiceDto.setDepartureTime(UtilString.replaceDateZH_cn(UtilMap.getString(item, "DateGetOn")) + " " + UtilMap.getString(prop, "TimeGetOn")); invoiceDto.setDeparturePort(UtilMap.getString(flight, "StationGetOn")); invoiceDto.setArrivePort(UtilMap.getString(flight, "StationGetOff")); invoiceDto.setSeatType(UtilMap.getString(flight, "Seat")); } if ("出租车发票".equals(item.get("TypeDescription"))) { // 上下车时间 invoiceDto.setDepartureTime(UtilMap.getString(prop, "TimeGetOn") + " ~ " + UtilMap.getString(prop, "TimeGetOff")); } return invoiceDto; }).collect(Collectors.toList()); return McR.success(McInvoiceDto.formatResponse(result)); } /** * 混票识别 [旧版本, 已废弃] */ @PostMapping("invoice-iv") McR invoice_iv(@RequestBody Map data) throws TencentCloudSDKException { McException.assertParamException_Null(data, "url"); String image = ydClient.convertTemporaryUrl(data.get("url")); log.info("混票识别, 免登地址, {}", image); // 非PDF, 且内存大于3M, 压缩后上传 if (UtilMap.getFloat(data, "size") > 3.0f && !UtilMap.getBoolean(data, "isPdf")) { image = imageUrlConvertBase64(image); } if (UtilMap.getFloat(data, "size") > 6.0f && UtilMap.getBoolean(data, "isPdf")) { image = pdfUrlConvertBase64(image); } // ppExt: 通用字段定义 List invoices = (List) txyInvoice.doMixedInvoiceOCR(image).get("MixedInvoiceItems"); List result = invoices.stream().map(item -> { String kind = TXYConf.TYPE_INVOICE.get(item.get("Type").toString()); List> infos = (List>) item.get("SingleInvoiceInfos"); McInvoiceDto.assertSuccess(item, kind); // 响应断言 String invoiceName = findValue(infos, "发票名称"); if (kind.equals("全电发票")) { kind = invoiceName.contains("增值税专用发票") ? "全电专用发票" : "全电普通发票"; } if (kind.equals("增值税发票")) { kind = invoiceName.contains("增值税专用发票") ? "增值税专用发票" : "增值税普通发票"; if (invoiceName.contains("增值税电子")) { kind = invoiceName.contains("专用发票") ? "增值税电子专用发票" : "增值税电子普通发票"; } } McInvoiceDto invoiceDto = McInvoiceDto.builder() .name(invoiceName) .kindName(kind) .kind(McInvoiceKind.getKindCode(kind)) .code(findValue(infos, "发票代码", "票据代码")) // 发票, 非税发票 // 储存唯一ID [发票, 火车票, 行程单] .serial(findValue(infos, "发票号码", "编号", "电子客票号码", "票据号码").replace("No", "")) // 发票, 非税发票 .date(findValue(infos, "开票日期").replace("年", "-").replace("月", "-").replace("日", "")) .checkCode(findValue(infos, "校验码")) .amount(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "小写金额", "价税合计(小写)", "合计金额", "票价", "金额"))) // 发票, 全电票, 行程单, 火车票, 过路过桥费 .excludingTax(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "合计金额", "金额", "票价", "小写金额"))) // [ppExt: 多明细行时, 优先取值合计] 行程单, 火车票, 定额发票 .tax(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "合计税额"))) // 增值税发票 .buyerName(guyuanNameRepalce(findValue(infos, "购买方名称", "交款人"))) // 发票, 非税发票 .buyerTaxId(findValue(infos, "购买方识别号", "购买方统一社会信用代码/纳税人识别号", "交款人统一社会信用代码")) // 发票, 全电票, 非税发票 .sellerName(guyuanNameRepalce(findValue(infos, "销售方名称", "填开单位"))) // 行程单 .sellerTaxId(findValue(infos, "销售方识别号", "销售方统一社会信用代码/纳税人识别号", "销售单位代号")) // 发票, 全电票, 行程单 .passengerName(findValue(infos, "旅客姓名", "姓名")) // 行程单, 火车票 .seatType(findValue(infos, "座位等级", "席别")) // 行程单, 火车票 .departurePort(findValue(infos, "始发地", "出发站", "入口")) // 行程单, 火车票, 过路过桥费 .arrivePort(findValue(infos, "目的地", "到达站", "出口")) // 行程单, 火车票, 过路过桥费 .trainNo(findValue(infos, "航班号", "车次", "车牌号")) // 行程单, 火车票, 出租车 .insuranceCosts(UtilNumber.setBigDecimal((findValue(infos, "保险费")))) // 行程单 .fuelCosts(UtilNumber.setBigDecimal((findValue(infos, "燃油附加费")))) // 行程单 .constructionCosts(UtilNumber.setBigDecimal((findValue(infos, "民航发展基金")))) // 行程单 .build(); // 价格不一致情况下, 通过合计返回 if (!UtilNumber.equalBigDecimal(invoiceDto.getAmount(), invoiceDto.getExcludingTax().add(invoiceDto.getTax()))) { invoiceDto.setAmount(invoiceDto.getExcludingTax().add(invoiceDto.getTax())); } // 机票行程单 if (kind.equals(McInvoiceKind.JP.getDesc())) { String date = findValue(infos, "日期").replace("年", "-").replace("月", "-").replace("日", " "); invoiceDto.setDepartureTime(date + " " + findValue(infos, "时间")); } // 火车票 if (kind.equals(McInvoiceKind.HC.getDesc())) { invoiceDto.setDepartureTime(findValue(infos, "出发时间").replace("年", "-").replace("月", "-").replace("日", " ")); } // 出租车 if (kind.equals(McInvoiceKind.CZC.getDesc())) { String date = findValue(infos, "日期").replace("年", "-").replace("月", "-").replace("日", " "); invoiceDto.setDepartureTime(date + " " + findValue(infos, "上车")); } return invoiceDto; }).collect(Collectors.toList()); return McR.success(McInvoiceDto.formatResponse(result)); } /** * 发票查重, 验真 */ @PostMapping("invoice-va") McR invoice_va(@RequestBody Map data) { McException.assertParamException_Null(data, "param"); List invoices = JSON.parseArray(JSON.toJSONString(data.get("param")), McInvoiceDto.class); log.info("发票查重, 验真, {}", invoices); invoices.forEach(UtilMc.consumerWithIndex((item, index) -> { McInvoiceDto dto = (McInvoiceDto) item; String invoiceNo = dto.getSerial(); // 唯一标识, 发票号码 String serial = "第【" + (index + 1) + "】张发票"; validateBuyer(dto.getBuyerName(), serial + "有疑问"); McException.assertAccessException(StringUtils.isBlank(invoiceNo), serial + ", 识别结果为空, 请检查!"); YDParam ydParam = YDParam.builder() .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY") .searchFieldJson(JSON.toJSONString(UtilMap.map("radioField_liihyrtb, textField_liihyrt8", "否", invoiceNo))) .build(); List idList = (List) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form_id).getData(); if (idList.size() > 0) { McException.exceptionAccess(serial + "已存在, 请勿重复提交!"); } // prd 仅仅识别增值税普通发票 if (dto.getName().contains("普通发票")) { String serialTips = serial + "有疑问"; try { // ppExt: 识别与验真后抬头对比 Map rsp = txyInvoice.doVatInvoiceVerifyNew(dto.getName(), dto.getCode(), invoiceNo, dto.getDate(), String.valueOf(dto.getAmount()), dto.getCheckCode(), String.valueOf(dto.getExcludingTax()), serialTips); Map invoice = (Map) rsp.get("Invoice"); McException.assertAccessException(!dto.getBuyerName().equals(guyuanNameRepalce(invoice.get("BuyerName").toString())), serialTips + ", 购买方名称不匹配!"); McException.assertAccessException(!dto.getBuyerTaxId().equals(invoice.get("BuyerTaxCode")), serialTips + ", 购买方税号不匹配!"); McException.assertAccessException(!dto.getSellerName().equals(guyuanNameRepalce(invoice.get("SellerName").toString())), serialTips + ", 销售方名称不匹配!"); McException.assertAccessException(!dto.getSellerTaxId().equals(invoice.get("SellerTaxCode")), serialTips + ", 销售方税号不匹配!"); } catch (TencentCloudSDKException e) { log.error(e.getMessage(), e); // prd: 上传发票为假发票时,提示:该发票有疑问,请联系财务人员 String message = e.getMessage(); // ppExt: 已经是新版本接口, 过滤提示 if (message.contains("温馨提示")) { message = message.split("温馨提示")[0]; } if (message.contains("发票不存在")) { message = "有疑问,请联系财务人员"; } McException.exceptionAccess(serial + message); } } })); return McR.success(); } @Autowired private YDService ydService; /** * 发票状态更新: 服务注册 */ @PostMapping("invoice-up") McR invoice_va(HttpServletRequest request) { Map data = UtilServlet.getParamMap(request); log.info("发票状态更新: 服务注册, {}", data); String compId = UtilMap.getString(data, "compId"); String status = UtilMap.getString(data, "status"); // 读取关联表单 List associationForm = (List) JSON.parse(UtilMap.getString(data, "multiAssociation")); List formInstanceIds = new ArrayList<>(); for (String record : associationForm) { // 解析关联表单 List associationData = (List) JSON.parse(record); formInstanceIds.addAll(associationData.stream().map(form -> UtilMap.getString(form, "instanceId")).collect(Collectors.toList())); } // 宜搭批量更新 Map update = UtilMap.map(compId, status); if (compId.equals("selectField_liihyrt6")) { update.put("radioField_liw7rb2q", "否"); // 提交后, 更新是否退回标识为否 } // prd 9.10 更新报销单, 关联到发票:: ppExt 宜搭服务注册, 提交规则系统默认字段 [详见 YDService] ydService.operateData2(data, update, YDParam.builder() .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY") .formInstanceIdList(formInstanceIds) .updateFormDataJson(JSON.toJSONString(update)) .build(), YDConf.FORM_OPERATION.multi_update); return McR.success(); } /** * 发票状态更新: 退回提交 */ @PostMapping("invoice-zy") McR invoice_zy(@RequestBody Map data) { log.info("发票状态更新: 退回提交, {}", data); List pre_ids = (List) data.get("pre_ids"); // 释放修改前 List cur_ids = (List) data.get("cur_ids"); // 占用修改后 // [前端调用添加] 退回为监听宜搭dom事件, 先执行接口调用, 才会校验宜搭必填, 过滤无效调用 if (cur_ids.size() == 0) { return McR.success(); } Map pre_update = (Map) data.get("pre_update"); Map cur_update = (Map) data.get("cur_update"); // 宜搭批量更新 ydClient.operateData(YDParam.builder() .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY") .formInstanceIdList(pre_ids) .updateFormDataJson(JSON.toJSONString(pre_update)) .build(), YDConf.FORM_OPERATION.multi_update); ydClient.operateData(YDParam.builder() .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY") .formInstanceIdList(cur_ids) .updateFormDataJson(JSON.toJSONString(cur_update)) .build(), YDConf.FORM_OPERATION.multi_update); return McR.success(); } @PostMapping("test") McR test() { // List process = (List) ydClient.queryData(YDParam.builder() // .formUuid("W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY") // .formInstId("FINST-NGA66WA1FV4EB7QJC3OATA3EV8MK35Z9COEMLFR22") // .build(), YDConf.FORM_QUERY.retrieve_id).getData(); List process = (List) ydClient.queryData(YDParam.builder() .formUuid("FORM-0IA66C71F6NBAETREO8DE9SSN43D3YIZ0AYILC") .searchFieldJson(JSON.toJSONString(UtilMap.map("textField_lmewsobs", "Y16668919W4E4FHQ6123ADDHB8XK3S709YEMLXWF"))) .build(), YDConf.FORM_QUERY.retrieve_search_form).getData(); return McR.success(); } }