简介:这是一套基于Java开发的称重过磅系统源码,面向仓储物流、工矿企业及需要车辆过磅管理的开发者与项目团队,解决称重数据记录、视频监控与车牌识别一体化管理的需求。资源包共483个文件,约41.25MB,以219个Java源文件为核心,配合44个JavaScript、39个XML配置、38个CSS样式及26个FXML界面文件构建前后端逻辑,另含33个DLL库、5个JAR包、2个SQL脚本等运行依赖,模块划分清晰,涵盖业务逻辑、数据模型、前端交互与数据库持久化等层次。系统集成海康威视摄像头实现过磅实时监控与拍照,芊熠摄像头负责车牌自动识别,所有称重与视频数据均保存至服务器,支持后台编辑与查询仓库货物及过磅信息。目前已有729人学习下载,适合作为Java桌面端与硬件集成项目的参考范例,便于快速理解摄像头对接、数据持久化与模块化架构设计。
1. 称重过磅系统为什么要同时接海康威视和芊熠两路相机
做过地磅项目的人都知道,车牌识别和抓拍是整套系统里最容易翻车的一环。一个典型的称重过磅场景是这样的:货车压上地磅,红外对射触发到位信号,仪表把重量通过串口推上来,同时两路相机分别从车头、车尾方向抓拍车牌和车厢照片,最后把重量、车牌、图片、时间戳一起落库。这套流程里,Java 负责的是业务编排、数据一致性、报表导出和界面呈现,而相机 SDK 负责的是取流、抓拍和车牌识别。
标题里同时出现海康威视和芊熠,不是堆料,而是现场真实需求:海康威视网络摄像头负责广角监控和录像回溯,芊熠相机负责车牌识别和特写抓拍,两者互补。很多新手一上来就想用一套 SDK 打通所有相机,结果发现海康的 SDK 是 C++ 动态库加 JNA 封装,芊熠走的是另一套 HTTP 或 SDK 接口,硬凑在一起只会让代码变成黑匣子。这篇笔记就按我实际落地的顺序,把选型、对接、参数、避坑和验证讲清楚,适合正在做 Java 课程设计、毕业设计或者中小型物流园地磅项目的工程师参考。
2. 系统骨架与两路相机的接入选型
2.1 Java 侧的技术栈与模块划分
称重过磅系统的 Java 侧我一般拆成四层:设备接入层、业务服务层、数据持久层、前端展示层。设备接入层负责串口读仪表、SDK 调相机、IO 板卡读红外;业务服务层负责过磅流程状态机、车牌与重量绑定、防作弊校验;持久层用 MyBatis 或 JPA 落 MySQL;前端用 Vue 或 Thymeleaf 都行,课程设计里 Thymeleaf 更省事。
选型上,JDK 用 8 或 11 最稳,海康威视 SDK 的 JNA 封装在 JDK 17 上偶发加载失败,报“源发行版 17 需要目标发行版 17”这类编译警告时别慌,那是 maven-compiler-plugin 的 source/target 没对齐,不是 SDK 本身的问题。串口通信用 jSerialComm 或 RXTX,前者跨平台更省心。相机接入统一抽象成一个CameraService接口,海康和芊熠各自实现,业务层只依赖接口,这样后面换相机不用动主流程。
public interface CameraService { // 初始化设备,返回设备句柄或会话ID boolean init(String ip, int port, String user, String password); // 抓拍一张,返回本地图片路径 String capture(String savePath); // 车牌识别,返回车牌号,识别不到返回 null String recognizePlate(); // 释放资源,必须在 finally 或销毁钩子里调用 void release(); }这段接口定义是整个接入层的契约。init里海康走NET_DVR_Login_V40,芊熠走 HTTP 登录或 SDK 初始化;capture海康用NET_DVR_CaptureJPEGPicture,芊熠用抓拍接口;recognizePlate只有芊熠或带识别功能的海康相机才实现,普通海康监控相机返回 null 即可。参数上 IP、端口、用户名密码都从配置文件读,不要硬编码,现场改 IP 是家常便饭。
2.2 海康威视 SDK 在 Java 里的加载方式
海康威视官方没有纯 Java SDK,社区和项目里常见做法是用 JNA 或 JNI 封装 C++ 的 HCNetSDK.dll / libhcnetsdk.so。JNA 上手快,不用写 C++ 胶水层,适合课程设计和中小项目。核心步骤是:把 SDK 的动态库和依赖库放到resources或系统库路径,Java 里用Native.load加载,然后按官方文档定义函数签名。
import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Pointer; public interface HCNetSDK extends Library { HCNetSDK INSTANCE = Native.load("HCNetSDK", HCNetSDK.class); boolean NET_DVR_Init(); int NET_DVR_Login_V40(LoginParam param, Pointer deviceInfo); boolean NET_DVR_CaptureJPEGPicture(int userId, int channel, JPEGParam param, String filePath); boolean NET_DVR_Logout(int userId); boolean NET_DVR_Cleanup(); }逻辑说明:Native.load会按系统去java.library.path找HCNetSDK.dll或libhcnetsdk.so,找不到就抛UnsatisfiedLinkError。LoginParam和JPEGParam是结构体,字段顺序必须和 C++ 头文件完全一致,错一个字节就登录失败。参数上userId是登录返回的句柄,channel一般是 1,filePath要传绝对路径且目录必须存在。注意NET_DVR_Init全局只需调一次,NET_DVR_Cleanup在应用关闭时调,重复调用会出玄学问题。
2.3 芊熠相机的接入路径与协议选择
芊熠相机通常提供 HTTP 接口或独立 SDK,做车牌识别时我优先用 HTTP,因为调试直观、跨平台、不依赖动态库。典型流程是:先登录拿 token,再调抓拍识别接口,返回 JSON 里带车牌号、置信度、图片 base64 或 URL。如果项目要求高频抓拍或离线运行,再考虑用芊熠 SDK。
public class QianyiCamera implements CameraService { private String baseUrl; private String token; @Override public boolean init(String ip, int port, String user, String password) { this.baseUrl = "http://" + ip + ":" + port; // 登录接口,返回 token,具体路径以设备文档为准 String resp = HttpUtil.post(baseUrl + "/login", "{\"user\":\"" + user + "\",\"pwd\":\"" + password + "\"}"); this.token = JsonUtil.get(resp, "token"); return token != null; } @Override public String recognizePlate() { String resp = HttpUtil.get(baseUrl + "/snap?token=" + token); return JsonUtil.get(resp, "plateNo"); } }逻辑说明:init里拼登录 URL,拿到 token 存成员变量,后续请求带上。recognizePlate调抓拍识别接口,解析plateNo。参数上超时时间建议设 3 到 5 秒,太短容易误判失败,太长会卡住过磅流程。如果设备返回的是 base64 图片,记得解码后存本地,别直接塞数据库,图片路径入库、文件落盘是更稳的做法。
3. 过磅流程编排与数据一致性保障
3.1 从红外触发到落库的完整状态机
过磅流程最怕的是“重量到了、车牌没识别到”或者“图片抓了、重量没稳定”。我一般用一个显式状态机来管:IDLE → WAIT_STABLE → CAPTURING → BINDING → DONE。红外触发进入 WAIT_STABLE,仪表重量连续 N 次波动小于阈值才进 CAPTURING,同时调两路相机抓拍,都返回后进 BINDING,把车牌、重量、图片路径、时间戳组装成一条过磅记录,最后 DONE 并落库。
public enum WeighState { IDLE, WAIT_STABLE, CAPTURING, BINDING, DONE } public class WeighFlow { private WeighState state = WeighState.IDLE; private BigDecimal lastWeight; private int stableCount; public synchronized void onWeight(BigDecimal weight) { if (state == WeighState.IDLE) return; if (state == WeighState.WAIT_STABLE) { // 波动小于 20kg 视为稳定,连续 3 次进抓拍 if (lastWeight != null && weight.subtract(lastWeight).abs().compareTo(new BigDecimal("20")) < 0) { stableCount++; if (stableCount >= 3) state = WeighState.CAPTURING; } else { stableCount = 0; } lastWeight = weight; } } }逻辑说明:onWeight由串口线程回调,synchronized保证状态不被并发改乱。稳定阈值 20kg 和连续次数 3 是可调参数,地磅越大阈值越大,比如 100 吨地磅可以放到 50kg。进 CAPTURING 后要异步调两路相机,用CountDownLatch等两路都完成再进 BINDING,避免一路慢一路快导致数据错位。
3.2 重量、车牌、图片三者的绑定时机
绑定时机是数据一致性的核心。常见错误是抓到车牌就先入库,重量后到再 update,结果中途断电就留下脏数据。我的做法是:三样都拿到才组装完整记录,一次 insert,事务提交。如果某一路相机超时,记录进“待补录”表,人工确认后再补,不阻塞下一辆车。
@Transactional public void bindAndSave(WeighContext ctx) { // 三要素齐全才落库 if (ctx.getWeight() == null || ctx.getPlate() == null || ctx.getImagePaths().isEmpty()) { pendingMapper.insert(ctx.toPending()); return; } WeighRecord record = new WeighRecord(); record.setPlate(ctx.getPlate()); record.setWeight(ctx.getWeight()); record.setImagePaths(String.join(",", ctx.getImagePaths())); record.setWeighTime(new Date()); recordMapper.insert(record); }逻辑说明:@Transactional保证 insert 原子性。pendingMapper是待补录表,字段和主表一致加一个处理状态。参数上imagePaths用逗号分隔存路径,别存 base64,否则单条记录能撑到几 MB,查询和备份都痛苦。如果业务要求绝对不丢,可以在 insert 前先写本地日志文件,作为后悔药用。
3.3 用本地消息表兜住相机超时
相机 SDK 调用是外部依赖,超时、断网、重启都可能发生。我一般加一张本地消息表,每次过磅先写一条“待处理”消息,相机和仪表都成功后更新为“已完成”,定时任务扫描超过 5 分钟还是“待处理”的记录,触发补抓或告警。这样即使相机临时挂了,数据也不会凭空消失。
CREATE TABLE weigh_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate VARCHAR(16), weight DECIMAL(10,2), status TINYINT DEFAULT 0, -- 0待处理 1已完成 2失败 retry_count INT DEFAULT 0, create_time DATETIME, update_time DATETIME );逻辑说明:status用 0/1/2 三态,retry_count控制重试次数,超过 3 次转人工。定时任务用@Scheduled每 30 秒扫一次,update_time超过阈值且 status=0 的捞出来重试。参数上重试间隔别太短,相机重启要时间,30 秒到 1 分钟比较合理。
4. 海康与芊熠对接中的避坑与排查
4.1 动态库加载失败:UnsatisfiedLinkError 的三种成因
现象:Java 启动时报UnsatisfiedLinkError: Unable to load library 'HCNetSDK'。原因通常有三种:一是动态库没放到java.library.path覆盖的目录;二是位数不匹配,JDK 64 位配了 32 位 dll;三是依赖库缺失,海康 SDK 除了主库还要HCCore.dll、PlayCtrl.dll等一串。解决:先打印System.getProperty("java.library.path")确认搜索路径,再把 SDK 所有库放同一目录,用-Djava.library.path=xxx显式指定,最后用Dependency Walker或ldd查依赖是否齐全。
4.2 登录返回失败但错误码是 0
现象:NET_DVR_Login_V40返回 -1,但NET_DVR_GetLastError拿到 0。原因多半是结构体字段对齐问题,JNA 默认按平台对齐,C++ 头文件里如果有#pragma pack(1)就必须在 Java 结构体上加@Structure.FieldOrder并设置对齐。解决:对照头文件逐个字段核对类型和顺序,char[]数组长度要一致,指针字段用Pointer,必要时用Memory手动分配。
4.3 车牌识别率低:先查补光和曝光再怪算法
现象:白天识别正常,晚上或逆光时车牌识别率骤降。原因不一定是算法差,更多是补光灯没同步、曝光时间固定、相机角度偏。解决:海康相机在 Web 端调曝光模式和宽动态,芊熠相机调补光触发延时,让抓拍瞬间补光已亮。参数上快门建议 1/500 到 1/1000,增益别拉太高否则噪点淹没车牌。这一条是血泪经验,很多项目返工都栽在光上。
4.4 两路相机时间不同步导致图片对不上
现象:过磅记录里海康图片和芊熠图片时间差几秒,回溯时对不上车。原因:两台相机各自 NTP,或者根本没配时间同步。解决:在 Java 侧抓拍完成后统一打时间戳入库,不依赖相机自带时间;同时给两台相机配同一 NTP 源,偏差控制在 1 秒内。如果现场没有 NTP,就在系统启动时用 Java 给相机下发一次时间。
4.5 串口读数抖动被误判为稳定
现象:车辆还没停稳就触发抓拍,重量是动态值。原因:稳定判定只看波动阈值,没考虑持续时间。解决:加一个最小稳定时长,比如连续 3 次且总时长超过 1.5 秒才进抓拍;同时红外信号要等车完全上磅再触发,必要时加地感线圈辅助。参数上稳定时长和阈值要现场标定,别照搬。
5. 报表导出与现场验证的进阶技巧
报表这块,很多课程设计要求导出 Word 或 Excel,Java 里常用 POI 或 EasyExcel。POI 生成 Word 图表比较麻烦,我的习惯是重量趋势用 Excel 折线图,过磅明细用 Word 表格,图片以缩略图嵌入。EasyExcel 写大数据量更省内存,POI 的 XWPF 适合 Word 模板填充。导出时注意图片路径要转成绝对路径再读,否则打包成 jar 后相对路径会失效。
验证整套系统是否可靠,我一般做三件事。第一,用模拟器造重量和红外信号,跑 100 次过磅看有没有丢记录;第二,拔掉一路相机网线,看本地消息表是否兜住并补录;第三,把系统时间往前调一天再调回来,看时间戳和 NTP 同步逻辑是否正常。这三步能覆盖大部分现场故障。
// 用 EasyExcel 导出过磅明细,图片列写路径即可 List<WeighRecord> list = recordMapper.selectByDate(new Date()); EasyExcel.write("weigh_report.xlsx", WeighRecord.class) .sheet("过磅明细") .doWrite(list);逻辑说明:EasyExcel.write第一个参数是输出路径,第二个是实体类,字段上加@ExcelProperty控制表头。参数上大数据量时分批查,别一次select *全捞出来。图片列如果要在 Excel 里显示缩略图,需要自定义 Converter,课程设计里写路径通常就够。
最后说个习惯:每次现场调试前,先把两台相机的 IP、端口、账号、SDK 版本、动态库路径记在一个device.md里,出问题时对着查,比翻代码快得多。相机对接这行,玄学不少,但把加载路径、结构体对齐、补光和时间同步这四件事做扎实,八成问题都能提前避开。希望帮到你。
本文还有配套的精品资源,点击获取