把票据图片交给视觉模型后,最容易犯的错误是把识别出的金额直接报销。图像理解擅长找候选字段,却可能受模糊、反光和版式影响。安全做法是让模型只"提取",让 Java 用 BigDecimal、币种白名单和人工阈值决定能否进入下一步。你将得到一个无需密钥也能读懂和编译的最小门禁。
官方模型文档把图像能力与模型可用性分开呈现,实际模型 ID、文件大小和账户权限需在调用前核验。Java 没有必要依赖不确定的 SDK:如服务只提供 REST,用 Java 17 的 HttpClient 即可,并给请求设超时。
环境与代码
安装 Java 17+,保存为 ReceiptGate.java,运行 javac ReceiptGate.java && java ReceiptGate。没有外部依赖;真实 API 请求可使用 HttpClient,将 OPENAI_API_KEY 通过 System.getenv 读取,绝不硬编码。以下候选 JSON 用教学数据模拟,因此不会上传票据。
java
import java.math.BigDecimal;
import java.util.Map;
public class ReceiptGate {
static final BigDecimal LIMIT = new BigDecimal("1000.00");
static final Map<String, String> ALLOWED = Map.of("CNY", "人民币", "USD", "美元");
static Result review(String merchant, String currency, String amount) {
if (merchant == null || merchant.isBlank()) return new Result(false, "缺少商户");
if (!ALLOWED.containsKey(currency)) return new Result(false, "不支持的币种");
try {
BigDecimal value = new BigDecimal(amount);
if (value.signum() <= 0) return new Result(false, "金额必须大于零");
if (value.compareTo(LIMIT) > 0) return new Result(false, "超过自动队列上限");
return new Result(true, "进入人工复核:" + ALLOWED.get(currency));
} catch (NumberFormatException e) {
return new Result(false, "模型金额格式无效");
}
}
record Result(boolean ok, String message) {}
public static void main(String[] args) {
Result r = review("示例超市", "CNY", "128.50");
System.out.println("ok=" + r.ok() + ", " + r.message());
// 真实 HTTP 调用应使用 HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10))
// 并为 request.timeout(Duration.ofSeconds(20));响应仍必须经过 review。
}
}
预期输出为 ok=true, 进入人工复核:人民币。为何使用 BigDecimal 而不是 double?货币比较需要避免二进制浮点误差。常见失败包括:图片不清导致小数点错位------限制自动阈值并保留原图供复核;模型返回"¥128.5"------要求结构化数字字段,解析失败时拒绝;请求超时或限流------连接超时 10 秒、请求超时 20 秒,重试前检查请求是否已入队;同一票据重复提交------生产中以图片哈希和发票号做幂等键。
它适合低风险票据预分流,不适合自动付款、税务认定或伪造判断。工程化还应增加版式置信度、原图授权与保存期限、审计记录及人工纠错回流。5 分钟实践:把上限改为从环境变量读取,并为"1000.01"写一个拒绝测试。
除了金额,你会如何证明一张票据确实属于本次报销?
关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。
本文首发于 java4u.cn,转载请注明出处。