视觉模型能读票据,不代表每张图片都该发给它。上传错文件、超大图片和"申请金额比识别金额高很多"才是报销自动化最常见的事故入口。答案很朴素:Java 先校验文件,再让模型抽取字段,最后用 BigDecimal 做金额比对。你会拿到一个可运行的 Java 17+ 最小程序;它只演示请求构造与本地门禁,不把模型的文字当成付款指令。
Gemini 的图像理解文档说明可将图像作为多模态输入。这里采用 REST(Representational State Transfer,基于HTTP的接口风格)而非假定某个 Java SDK 的版本;请求超时、密钥和响应状态都显式处理。示例未在本次任务中实际调用线上 API;本机的 javac 缺少 Java 运行时,未能编译,已完成静态审阅。
环境与运行
需要 JDK 17+,不需要 Maven 依赖。设置 export GEMINI_API_KEY='你的密钥',把示例保存为 ReceiptGate.java,编译运行:javac ReceiptGate.java && java ReceiptGate receipt.jpg 128.50。当前端点与模型名请以你的账号和官方图像理解文档为准。真实项目不应把原始票据长期写入日志。
java
import java.io.*;
import java.math.BigDecimal;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.time.Duration;
import java.util.Base64;
public class ReceiptGate {
static void check(Path file) throws IOException {
String n = file.getFileName().toString().toLowerCase();
if (!(n.endsWith(".jpg") || n.endsWith(".jpeg") || n.endsWith(".png")))
throw new IllegalArgumentException("只接受JPG/PNG");
if (Files.size(file) > 5 * 1024 * 1024) throw new IllegalArgumentException("图片超过5MB");
}
static boolean within(BigDecimal declared, BigDecimal extracted) {
return declared.subtract(extracted).abs().compareTo(new BigDecimal("0.01")) <= 0;
}
public static void main(String[] args) throws Exception {
if (args.length != 2) throw new IllegalArgumentException("用法: 图片路径 申报金额");
String key = System.getenv("GEMINI_API_KEY");
if (key == null || key.isBlank()) throw new IllegalStateException("缺少GEMINI_API_KEY");
Path file = Paths.get(args[0]); check(file);
String b64 = Base64.getEncoder().encodeToString(Files.readAllBytes(file));
String body = "{\"contents\":[{\"parts\":[{\"text\":\"仅返回票据总金额\"},"
+ "{\"inlineData\":{\"mimeType\":\"image/jpeg\",\"data\":\"" + b64 + "\"}}]}]}";
HttpRequest req = HttpRequest.newBuilder(URI.create(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3.8-flash:generateContent"))
.timeout(Duration.ofSeconds(25)).header("x-goog-api-key", key)
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build();
HttpResponse<String> res = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build()
.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() / 100 != 2) throw new IOException("API状态=" + res.statusCode());
System.out.println("模型原始响应请交由受控JSON解析器提取金额,HTTP=" + res.statusCode());
}
}
check 在上传前拒绝类型和大小,避免把任意文件或巨图带入模型成本;within 使用 BigDecimal,不能用 double 比较货币。请求对连接设置5秒、整体设置25秒超时;非2xx响应直接失败。为了保持示例只用 JDK,代码没有脆弱地用字符串"猜"模型金额:生产中应要求模型按 JSON Schema 返回 currency、amount 和 confidence,再用 Jackson 等成熟 JSON 库解析。
预期是成功时打印 HTTP 状态,缺密钥、文件类型不符、超过5MB或网络/API失败时抛出清晰异常。常见错误:图片实际为PNG却硬写JPEG MIME;把模型输出中的逗号金额直接转数值;将0.01容差用于不含税/含税不同口径;失败后无限重试造成重复上传。适合做"进入审批前的辅助核验",不适合自动付款或替代税务规则。
工程化时需加入病毒扫描、对象存储短期签名URL、JSON响应校验、票据哈希去重、人工复核与保留期限。5分钟实践:把 within 的容差改为按币种表配置,并为 128.50 与 128.52 写两条单元测试。
如果票据金额与员工填写金额不一致,你的流程会自动拒绝还是进入复核?
关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。
本文首发于 java4u.cn,转载请注明出处。