ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot3+Vue3 进项发票池:OCR 识别、票面预览与报销占用怎么落地

SpringBoot3+Vue3 进项发票池:OCR 识别、票面预览与报销占用怎么落地 SpringBoot3Vue3 进项发票池OCR 识别、票面预览与报销占用怎么落地文档地址https://ruoyioffice.com源码1·GitHubhttps://github.com/yuqing2026/ruoyi-office源码2·GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office源码3·Giteehttps://gitee.com/yqzy1688/ruoyi-office微信17156169080备注「RuoYi Office」同一张专票出现在两张报销里财务月底才发现。差的不是认真是票没有先入池再占用。RuoYi Office 把影像识别成票号和价税进项落自己的表票池做统一台账报销挂票时行锁写入占用已被别的单锁住就拦。OCR 只是连接器占用才是账。▲ 票面 → Vue3 上传 / OCR 回填票号税号 → SpringBoot3 occupy 行锁 → 报销单上锁。第二单挂同一张票会被拦驳回 release 后票夹回待报销引言票若只活在报销明细行里重复报销拦不住费用预算管的是额度公式上一篇已经写过。发票管的是另一件事这一张纸被哪张单锁住。两套账对成同一张表财务会同时骂「超预算」和「重复报销」却对不上号。错法看起来省事三个月后报销单手填票号不用票夹同一张专票挂两单查验对不上OCR 成功才允许保存显得智能识别失败全员无法报黄牛票更没法录票池和进项一张大宽表少一张表销项购方、进项销方字段互相污染占用只改一个「已用」标记少字段驳回后不知道释放给谁金额也对不回票面正确的形状是影像先上传OCR 能回填就回填失败改手工进项进finance_input_invoice票池把进项和销项在内存里合并给台账报销或应付提交时occupy驳回release。认证状态、票夹状态、占用单据是三列不是一列。连接性也要画清Vue3 负责上传影像和回填表单OCR 只是把票号、税号、价税填进字段SpringBoot3 的occupy才上锁。识别通了但没行锁重复报销仍拦不住锁上了但驳回不release票会永远躺在已锁定。本文不讲预算可用公式也不讲开票申请怎么走审批。只讲进项票怎么从影像变成可占用的池内对象。一、先给出能点的定义1.1 进项发票是什么进项发票是「票面要素 影像 认证 票夹状态 占用指针」的一张主数据。票面要素至少有票种、代码、号码、开票日、销方名称税号、金额、税额、价税合计。影像必传否则保存直接警告。录入来源手工 10OCR 20。OCR 成功会把来源写成 20方便事后解释「这几个字段是识别出来的」。识别失败仍可手工改不要把来源锁死成「必须机器说了算」。1.2 票池不是第三张业务表销项、进项分表存储。票池服务按方向过滤后在内存里合并、按创建时间倒序、再分页。V1 台账数据量可控不必先上宽表或搜索引擎。票夹是票池的个人视图只看当前用户待报销、未占用的进项。报销单从票夹勾票不要从全公司票池随便捞。1.3 三种票夹状态占用另写指针| walletStatus | 名称 | 谁改 ||:—|:—|:—|:—|| 10 | 待报销 | 录入默认release 后回到这里 || 20 | 已报销 | 报销办结后 || 30 | 已锁定 | occupy 成功 |占用指针occupySourceType报销单 1 / 应付单 2、occupySourceId、occupyAmount。已被其他来源占用isOccupiedByOther为真编辑按钮消失再 occupy 抛「发票已被占用」。认证状态是税务侧待认证 / 已认证 / 不可抵扣。它不替代票夹状态。已认证仍可以待报销已锁定也可以尚未认证。混成一列抵扣台账和报销台账会一起错。二、系统设计子模块功能面向谁OCR 服务上传后识别落处理记录录票人进项服务保存、认证、占用、释放财务 / 报销票池服务进项 销项合并分页财务台账票面预览影像 价税审核人报销挂票从票夹勾选并 occupy员工核心决策决策点方案理由OCR 可失败识别记录失败表单仍可手改黄牛票、折叠票不能锁死录入进项销项分表票池只读合并购方销方语义相反占用行锁selectByIdForUpdate两张报销同时勾同一张占用金额默认价税合计显式传入不得超票面防止拆票超占未占用才能改票wallet待报销且无占用指针锁住的票不能再改税号OCR 提供商由 Spring 注入的InvoiceOcrClient列表按配置选取。换厂商只换客户端不换占用逻辑。识别先插一条 status0 记录成功改 1 并截断原始 JSON失败写原因。连接器挂了票还在账还在。三、PC录入、预览、票池进项列表在/finance/invoice/input-invoice。工具栏「录入发票」打开表单弹层不是跳到另一张空白详情。已占用的行没有编辑和删除只剩认证类操作待认证时。▲ 录入弹层票种、代码、号码、开票日、销方、价税。没有影像不能保存OCR 组件接受 jpg / png / pdf上限 20MB。上传走统一文件服务私有桶预览用预签名 URL。点识别后回填票号和金额回填错了允许改不要做成只读。▲ 上传影像后识别。成功回填字段来源记成 OCR失败仍可手工录入票面预览用在报销详情和录票处左边影像右边价税和销方。审核人要对的是「图上的数」和「单上的数」不是再看一张发票列表。▲ 预览认影像和价税合计。列表页最多当入口不靠表格证明票是真的票池/finance/invoice/invoice-pool可切「全票池 / 个人票夹」。全票池是财务台账票夹是报销勾选源。这一张是本篇唯一允许的列表。▲ 台账看出方向、票号、占用金额、占用单号。占用列为空才是还没被锁四、报销怎么挂这张票报销明细不是再手填一组票面而是引用进项 id。提交时对每张票occupy(invoiceId, REIMBURSE, reimburseId, amount)。金额空则用价税合计。超过票面抛超占。应付单也可以占用sourceType2。采购票进成本时走应付差旅票进费用时走报销。同一张进项不能同时被两者锁。驳回、取消、删报销走release(sourceType, sourceId)只处理已锁定的行票夹回到待报销占用三字段清空。不要在 release 里改认证状态税务侧没变。预算占用和发票占用不要对账。预算问「这个费用类型还有没有额度」发票问「这张纸是不是已经被别的单拿走」。一张招待费报销可能同时占预算明细一行占进项票一张。失败原因要分开提示不要合成一句「提交失败」。五、后端核心实现5.1 OCR先落处理中再回填publicFinanceOcrRecognizeRespVOrecognize(FinanceOcrRecognizeReqVOreqVO){if(StrUtil.isBlank(reqVO.getFileUrl())){throwexception(INVOICE_OCR_FILE_EMPTY);}InvoiceOcrClientclientgetClient();FinanceOcrRecordDOrecordnewFinanceOcrRecordDO();record.setFileUrl(reqVO.getFileUrl());record.setBizType(reqVO.getBizType());record.setProvider(client.getProvider());record.setStatus(0);ocrRecordMapper.insert(record);InvoiceOcrResultresultclient.recognize(reqVO.getFileUrl());// 成功status1原始 JSON 截断后入库返回回填字段returnBeanUtils.toBean(result,FinanceOcrRecognizeRespVO.class);}前端handleRecognized只setValues票种、代码、号码、日期、销方、三项金额。税号识别错了录票人改完再保存。不要在 OCR 成功后立刻 occupy。5.2 占用行锁 他人占用判断publicvoidoccupy(LonginvoiceId,IntegersourceType,LongsourceId,BigDecimalamount){FinanceInputInvoiceDOinvoiceinputInvoiceMapper.selectByIdForUpdate(invoiceId);if(invoicenull){throwexception(INPUT_INVOICE_NOT_EXISTS);}if(isOccupiedByOther(invoice,sourceType,sourceId)){throwexception(INPUT_INVOICE_OCCUPIED);}BigDecimaloccupyAmountamount!null?amount:nullToZero(invoice.getPriceTaxAmount());if(occupyAmount.compareTo(nullToZero(invoice.getPriceTaxAmount()))0){throwexception(INPUT_INVOICE_OCCUPY_AMOUNT_EXCEED);}updateObj.setWalletStatus(FinanceWalletStatusEnum.LOCKED.getStatus());updateObj.setOccupySourceType(sourceType);updateObj.setOccupySourceId(sourceId);updateObj.setOccupyAmount(occupyAmount);inputInvoiceMapper.updateById(updateObj);}同一张报销重复提交应视为本单占用不要当成他人占用。isOccupiedByOther排除「来源类型 来源 id 都是自己」的情况。连点提交靠这层不靠按钮禁用。5.3 票池分表查询内存合并publicListFinanceInvoicePoolRespVOgetInvoicePoolList(FinanceInvoicePoolPageReqVOreqVO){ListFinanceInvoicePoolRespVOresultnewArrayList();if(!Objects.equals(reqVO.getDirection(),DIRECTION_INPUT)){result.addAll(queryOutput(reqVO));}if(!Objects.equals(reqVO.getDirection(),DIRECTION_OUTPUT)){result.addAll(queryInput(reqVO));}result.sort(Comparator.comparing(FinanceInvoicePoolRespVO::getCreateTime,Comparator.nullsLast(Comparator.reverseOrder())));returnresult;}进项行会再补占用视图占用金额、占用单号。台账上看见单号才能从票跳回报销而不是只看见「已锁定」。六、和预算强控差在哪预算占用发票占用对象编制明细行一张进项票公式预算调整结转−占用−已发生占用金额 ≤ 价税合计策略强控 / 预警 / 不控没有预警档占不了就抛错来源申请 / 报销 / 付款报销 / 应付释放流水记负数清空指针票夹回待报销不要把发票占用金额累加进预算已发生。票是合规凭证预算是盘子。一张票报销 500预算占 500发票占 500对的是两本账不是一个数字写两次。开票申请是销项侧的审批单流程 keyfinance-invoice-apply-audit和进项池不是一张单。客户说「发票模块」先问他是开出去还是收进来。七、数据结构finance_input_invoice编号、代码、号码、票种、开票日、销方名称税号、购方名称税号、金额、税额、价税合计、认证状态、来源、供应商、归属人、票夹状态、占用类型 / 单据 / 金额、影像、备注。finance_input_invoice_item明细行税率和货物名称。OCR 能回填多少写多少不够再手工。finance_ocr_record文件地址、业务类型、提供商、状态、原始结果、解析结果。它不是票不能拿记录 id 去 occupy。票池没有自己的表。占用视图是查询拼出来的FinanceInvoiceOccupyView。不要为了台账再复制一份票面复制一次就和进项表开始漂移。编辑门禁walletStatus 待报销且没有占用指针。已锁定、已报销、或占用单号还在表单不开放。八、技术亮点设计要点实现方式价值OCR 可失败记录失败 表单手改识别挂了也能录来源分 10 / 20保存时带 source事后知道谁填的进项销项分表票池内存合并字段语义不打架占用行锁FOR UPDATE防两单抢一张占用排除本单isOccupiedByOther连点不误伤超占校验金额 ≤ 价税合计不能拆出比票面大的数票夹三态待报销 / 已锁定 / 已报销和认证状态拆开释放不改认证只清占用指针税务侧不被报销驳回带跑影像必传无 URL 不保存审核能对图预览预签名私有桶临时地址不把对象存成永久公网票夹与票池分视图wallet-page / page员工只能勾自己的待报销和预算分账不同服务不同异常超预算和票被占提示分开九、快速体验在线演示https://ruoyioffice.com/web/账号 admin / admin123打开/finance/invoice/input-invoice点「录入发票」。上传一张票面影像点识别看票号和价税是否回填。保存后在列表点编辑仅待报销且未占用核对影像还在。打开/finance/invoice/invoice-pool切全票池找刚录的进项和占用列。打开一张财务报销详情从票夹勾票提交后回票池看占用单号。若可造数同一张票挂第二张报销应看到已被占用。驳回第一张报销票夹应回到待报销编辑按钮回来。演示站公共数据只看、少改。编号前缀以页面为准认的是「一张未占用进项 报销挂票后出现占用单号」不是死背 id。源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-office GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office Giteehttps://gitee.com/yqzy1688/ruoyi-office十、结语进项发票的难度不在会不会调用识别接口而在票有没有先成为池内对象以及占用有没有行锁。RuoYi Office 用 OCR 回填、分表入池、票面预览、报销 / 应付占用把「手填票号」收成可对账的动作。识别失败可以手改占用失败必须抛错两件事不要做成同一个开关。读者能带走的三句OCR 是连接器票池是只读台账占用看行锁。预算管额度发票管这一张纸两本账分开问。你们现在重复报销是月底对票还是提交就被拦评论区对照一下票夹三态比空泛「上了发票模块」有用。相关阅读同目录财务预算强控讲占用公式OA 出差报销讲双单联动。那些文章不替代本页的进项池。要把票真正管住录入、预览、挂票、释放必须认同一张进项 id。想要体验 RuoYi Office 的强大功能在线演示https://ruoyioffice.com/web/账号 admin / admin123源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-office GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office Giteehttps://gitee.com/yqzy1688/ruoyi-office技术咨询添加微信17156169080备注「RuoYi Office」⭐如果觉得不错请给个 Star 支持一下
返回列表