SpringBootOCR 发票录入识别出的字段怎样变成可用的财务数据发票识别接口返回了票号、销方和金额表单里的格子也填上了。这时能否直接保存再自动参与报销、认证和记账企业软件里的难点往往就在这段距离**提取出了信息不等于确认了业务事实。**一张影像可以识别多次一次识别可能只有部分字段可用财务还可能改正票号、调整金额。若把识别成功、发票保存和后续使用合成一个状态出了问题就很难解释究竟是哪一步通过了。我们沿RuoYi Office的进项录入实现拆开影像、识别记录、可编辑表单和正式发票四类对象。重点是这些对象怎样交接以及哪些判断必须留在业务层。下文用真实前端配合本地隔离的示例数据展示操作文件接收、OCR响应和保存响应由mock提供字段回填由原有组件执行。它用于说明交接过程不是识别准确率测评也不表示真实财务接口已完成保存验证。▲ 识别负责提取表单负责承接财务服务负责保存规则。右侧为历史真实进项编辑页仅展示功能布局不表示其中字段由本次OCR产生。把影像、识别和保存拆成三个动作进项发票录入页上方是影像区下方是票面字段。文件上传成功后页面保留规范的文件地址再用可访问地址做预览私有文件会尝试获取预签名地址。存储地址和临时预览地址各有用途不能因为浏览器暂时打不开就认定对象文件不存在。OCR组件支持图片和PDF选择前端限制单文件20MB这些是入口约束并不表示每一种识别服务对所有格式、分页方式都有相同支持。接入时仍要按具体服务验证。▲ 示例数据影像标注“非真实发票”文件接收由本地mock处理。此时影像可见票号和金额仍为空说明文件准备与字段回填是两个动作。完整框架保留整屏小字可点开放大。这套交互有三个独立提交点上传文件调用识别接口确认保存发票。取消录入弹窗不意味着已经上传的文件和已经产生的识别记录都被撤销识别返回了字段也不会自动生成正式进项单。这样的分工保留了人工核对空间也带来一个工程要求清理孤立文件、追踪识别任务与保存单据应分别设计不能把弹窗关闭当成分布式回滚。先留下识别尝试再讨论成功和失败财务OCR服务先校验文件地址、选择识别客户端再插入一条状态为0的记录之后才调用外部识别。下面保留这条主线省略方法外围的异常分支。InvoiceOcrClientclientgetClient();FinanceOcrRecordDOrecordnewFinanceOcrRecordDO();record.setFileUrl(reqVO.getFileUrl());record.setBizType(reqVO.getBizType());record.setProvider(client.getProvider());record.setModelId(ocrProperties.getModelId());record.setStatus(0);ocrRecordMapper.insert(record);InvoiceOcrResultresultclient.recognize(reqVO.getFileUrl());FinanceOcrRecognizeRespVOrespVOBeanUtils.toBean(result,FinanceOcrRecognizeRespVO.class);respVO.setRecordId(record.getId());// 随后更新成功状态及识别结果再返回响应记录状态区分处理中0、成功1、失败2。成功时写入实际提供商、模型信息、耗时、置信度摘要、原始结果与解析结果捕获异常后写入失败状态、阶段和错误摘要。文件地址为空、提供商不支持等在插入前被拒绝的请求不会走同一条留痕路径。**这里的成功是客户端返回并被服务接受不是发票真实性认证也不是财务校验通过。**例如通用字段映射可以解析一份JSON但这份JSON仍可能缺票号不能用“解析没有抛异常”代替字段完整性判断。已有一条标记成功的历史模型OCR记录结构化结果中只有提供商、模型、耗时等信息没有票号和价税合计。单凭这条记录无法还原当时的服务版本和响应处理过程但它说明了一个业务判断成功标签与关键字段齐全必须分别核对。当前更新代码将原始结果截到60000字符错误摘要截到500字符解析结果另存。它为排查保留了上下文但不能承诺保存了任意长度的完整服务响应。还有一个容易忽略的故障窗口请求在记录插入之后遇到进程中断可能来不及进入异常处理。我们在已有开发数据里确实查到处理中记录但没有据此断言原因。若要自动超时收敛应另外设计任务状态与补偿不能因为有catch就认定每条处理中记录最终都会转成失败。不同识别器交付同一份字段契约财务层按配置中的provider从客户端实现中选择一个统一返回票号、日期、购销方、金额与明细等字段。模型OCR、多模态识别和专业发票API不是自动依次尝试的三个兜底节点当前选择路径没有实现“第一个失败就切下一个”。以模型客户端为例返回内容既可以直接是票面对象也可以放在invoice节点里客户端将其归一为同一个结果类型。privateInvoiceOcrResultparse(StringrawResult){if(StrUtil.isBlank(rawResult)){throwexception(INVOICE_OCR_RECOGNIZE_FAIL);}try{varrootJSONUtil.parseObj(rawResult);varinvoiceroot.getJSONObject(invoice);InvoiceOcrResultresultinvoicenull?JSONUtil.toBean(root,InvoiceOcrResult.class):JSONUtil.toBean(invoice,InvoiceOcrResult.class);result.setConfidenceSummary(root.getStr(confidenceSummary));returnresult;}catch(Exceptionex){// 省略现有错误日志throwexception(INVOICE_OCR_RECOGNIZE_FAIL);}}这种统一解决的是上层不必理解每家服务的响应形状。专业API客户端目前仍是通用映射骨架不同厂商的嵌套字段、错误码和凭据协议需要按实际接口适配不能理解成配置一个厂商名称就已完成接入。业务规则也不宜全压给识别器。服务可以提示疑似字段缺失、票面金额不一致正式保存时仍需独立检查因为调用保存接口的未必是这张页面人工修改后的值也可能与原始识别值不同。回填要有明确范围不能把整个响应灌进表单当前进项弹窗接到识别结果后将来源标记为20再显式选择九类票面字段回填。购方信息、商品明细、置信度摘要与recordId没有被整包写入进项主表。▲ 示例数据点击OCR识别后模拟响应进入原有回填函数不含税400.00、税额52.00、合计452.00显示在对应格子里。未预填这些表单输出也没有重绘系统界面此图验证前端映射不证明真实OCR已读对票面。functionhandleRecognized(res:FinanceOcrApi.OcrRecognizeRespVO){recognizedBySourceocrSource;constinvoiceDateres.invoiceDatedayjs(res.invoiceDate).isValid()?dayjs(res.invoiceDate).valueOf():undefined;formApi.setValues({invoiceType:res.invoiceType,invoiceCode:res.invoiceCode,invoiceNo:res.invoiceNo,invoiceDate,sellerName:res.sellerName,sellerTaxNo:res.sellerTaxNo,amount:res.amount,taxAmount:res.taxAmount,priceTaxAmount:res.priceTaxAmount,});}显式映射的好处是字段归属清楚购方税号不会因为属性相似而误写进销方税号识别明细也不会悄悄变成正式业务明细。代价是接口新增字段时需要明确决定页面是否接纳而不是期待它自动生效。日期解析失败时回填undefined票号和税号保持字符串不应转成数字。尤其是长票号前导零与每一位数字都有业务意义。这段逻辑还提示了两个需要谨慎处理的边界。第一回填没有逐字段判断“结果非空才覆盖”识别不完整时不应承诺能保留用户全部旧输入。第二页面日期使用毫秒时间戳而当前后端保存VO对日期声明了字符串格式前端API类型也写成string这处契约差异需要通过实际请求核对不能因为页面能显示日期就宣布提交已验证。保存时先明确哪些规则已经执行弹窗确认时先做表单校验再检查影像地址最后调用进项保存接口。当前前端把发票号码和价税合计列为必填但保存VO没有为这两个字段声明同样的必填注解。因此页面提示存在不等于直接调用接口也有完全相同的门禁。后端创建进项时有自己的业务约束账套必须提供并启用非空票号要判重生成业务单号初始化未认证与待报销状态。来源没有传入时默认为手工录入。privateLongcreateInputInvoice(FinanceInputInvoiceSaveReqVOsaveReqVO){validateBook(saveReqVO.getBookId());validateInvoiceNoUnique(saveReqVO.getInvoiceNo(),null);FinanceInputInvoiceDOinvoiceBeanUtils.toBean(saveReqVO,FinanceInputInvoiceDO.class);invoice.setNo(noRedisDAO.generate(FinanceNoRedisDAO.INPUT_INVOICE_NO_PREFIX));invoice.setCertStatus(FinanceInputCertStatusEnum.UNCERTIFIED.getStatus());invoice.setWalletStatus(FinanceWalletStatusEnum.WAIT.getStatus());if(invoice.getSource()null){invoice.setSource(FinanceInputSourceEnum.MANUAL.getSource());}if(invoice.getOwnerUserId()null){invoice.setOwnerUserId(SecurityFrameworkUtils.getLoginUserId());}fillTotalsFromItems(saveReqVO,invoice);inputInvoiceMapper.insert(invoice);saveItems(invoice.getId(),saveReqVO.getItems());returninvoice.getId();}保存入口使用事务组织主表和明细。票号判重是先查询已有数量再写入当前查询方法本身按票号和排除ID构造条件没有显式追加账套条件。实际范围还要结合运行时租户、数据权限等拦截器确认这一检查也不能单独证明已解决并发重复写入。金额处理同样要看实际分支。有明细时服务汇总明细金额、税额和价税合计某行价税合计为空时才用该行不含税金额加税额补出。没有明细时不含税与税额缺失补零价税合计缺失才计算已经传入的三个金额不会在这里强制校验勾稽相等。这意味着“用BigDecimal汇总”与“严格验证票面金额一致”是两件事。若业务需要把金额不符作为拒绝保存的条件应在服务端增加明确规则并确定小数精度、尾差容忍与明细优先级这是设计建议不是当前实现已经完成的承诺。识别留痕和单据追溯还差一条明确关系▲ 实线仅表示进项主表与明细的业务关联。识别记录虽保存文件地址当前进项表和保存请求没有ocrRecordId不能画出已实现的一对多审计外键。当前识别响应有recordId进项保存请求却没有接收对应关联字段。source20只表示录入来源标签不能据此唯一找到某一次识别也不能确认人工改过哪些格子。文件地址可以作为排查线索但同一影像可能重识别地址也可能经过规范化或迁移仅靠URL相等不能建立可靠审计关系。需要完整追溯时可以增加识别记录关联、确认版本与差异记录并在保存时校验其租户、文件归属和可用状态。这些都应作为独立设计落实。▲ 识别请求先完成自己的状态记录再返回页面正式进项在另一次保存中创建。取消表单不会自动回滚已经发生的识别。来源、认证和使用状态分别回答不同问题正式发票保存后后续业务读取的是已确认的发票资料。来源回答“怎样录入”认证状态回答“是否标记认证”使用状态回答“是否已被业务引用”。识别器不应把这三件事一起改掉。从创建服务的初始化规则也可以看到来源可以是OCR认证状态仍从未认证开始使用状态仍从待报销开始。只有后续业务执行对应操作这些状态才各自变化不能让识别结果顺带替用户完成认证或引用。▲ 示例数据确认后本地mock仅把提交值放入代理内存并返回DEMO-JXFP-001原有列表随后展示452.00。这是模拟保存后的界面联动未写数据库不能据此宣称后端金额、日期和判重规则已经实测通过。当前列表还会根据待报销和引用情况控制编辑入口更新服务也会检查发票是否已经被使用。这类约束保护的是下游引用数据的稳定性不应因为再次识别得到了不同金额就越过业务规则覆盖已经使用的票。把OCR接进企业管理软件真正要接上的不是一个按钮而是四份清楚的契约影像怎样长期访问识别记录怎样解释人工确认怎样保存后续业务怎样引用。提取、确认和使用各自留下可解释的事实识别能力才有稳定的业务落点。