
简介基于Java的称重过磅系统设计源码集成海康威视与芊熠摄像头功能面向Java后端开发人员及仓库过磅管理信息化项目实践。系统实现称重数据记录、实时视频监控与车牌自动识别海康威视摄像头用于过磅过程录像监督芊熠摄像头专注车牌识别并与称重数据关联数据统一保存于服务器支持后台编辑和查看仓库货物及过磅信息整体采用模块化分层设计。压缩包共483个文件主要包含219个Java源文件、44个JavaScript脚本、39个XML配置、38个CSS样式、33个动态链接库、26个界面布局文件等包体大小约41.25MB已有732人学习浏览。源码按业务模块清晰划分实体定义、数据访问、界面交互与服务实现等层次分明便于理解称重业务与持久化流程适合作为Java桌面应用和第三方摄像头对接的完整参考也可直接用于过磅系统的二次开发与功能扩展。1. 基于Java的称重过磅系统:先看清它解决的是什么问题地磅过磅看着简单:车开上去,仪表读数,记个重量。但真去做工厂计量项目的人都知道,难点不在仪表,在怎么证明这个重量是真的。换司机、压边、倒换皮重、事后扯皮——这些场景都指向同一个需求:称重记录必须和图像、车牌绑在一起。基于Java的称重过磅系统设计源码,集成海康威视和芊熠摄像头功能,做的就是这件事:Java后端同时对接称重仪表、海康威视抓拍镜头和芊熠车牌识别镜头,把毛重、皮重、净重、车牌、抓拍图片、过磅时间串成一条不可抵赖的计量记录。适合三类人:接工厂地磅项目的Java工程师、做计量信息化改造的集成商、想把手动过磅改成自动过磅的企业IT。这套源码不是把摄像头视频流接到网页上看画面那么简单,核心在于触发、绑定和落库的连贯性。2. 系统架构与数据流:把仪表、海康威视、芊熠放进同一个闭环这类称重过磅系统最基本的架构原则是事件驱动,而不是轮询。重量读数、车牌识别、抓拍图片这三个事件本身没有任何顺序保证,如果让它们各自去数据库里写一行,最终得到的记录必然是一堆残片。常见的做法是把整个系统拆成三层:硬件采集层、业务处理层、数据存储层。采集层只负责把原始数据变成事件,业务层负责把事件合并为过磅记录,存储层负责把记录落到库。2.1 一次完整过磅流程的三个关键触发点车辆上磅之后,系统会在极短的时间里收到三类事件。第一类是重量落定事件。车辆刚上磅时重量不断跳变,采集模块会持续读取仪表帧,直到连续多次读数落在一个很小的区间内,才判定车辆停稳,把当前重量作为一个有效稳定值发给业务服务。第二类是车牌识别事件。芊熠摄像头在车辆进入识别区域后,经过内部算法输出车牌号,通过HTTP回调把结果送到Java服务。第三类是抓拍事件。业务服务在收到重量落定事件后,触发海康威视摄像头执行抓拍,保存一张现场照片。这三个事件之间没有固定先后顺序。有时候车牌识别先到,重量后到;有时候抓拍已经完成,但重量还在小幅跳变。因此业务服务里需要一个“过磅会话”的概念:先到的事件把字段填进会话,后续事件逐步补全,直到毛重、净重、车牌号、图片路径四个字段都齐全,这条记录才允许进入正式表。这个设计从源头上避免了一个常见问题——把重量、车牌、图片直接写成一张表的三列,并发场景下会插出大量半截记录。2.2 模块划分:硬件采集、业务服务与数据存储围绕上面的流程,源码通常拆成三个独立模块,各自维护一套接口。硬件采集模块封装串口读取和仪表协议解析,对外只暴露“当前重量”和“重量落定”两个方法。图像识别模块封装海康威视SDK登录、抓拍,以及芊熠摄像头的HTTP回调解析,对外暴露“触发抓拍”和“车牌到达”两个事件。业务服务模块是核心,它负责状态机流转、净重计算和记录落库。下面是一张简化的模块划分表,方便在接手源码时快速定位:模块关键技术职责称重采集jSerialComm、串口读取读取仪表重量帧,判断车辆是否落定图像采集海康威视SDK、芊熠HTTP回调抓拍图片、识别车牌号业务处理Spring Boot、状态机事件绑定、净重计算、数据校验数据存储MySQL、本地磁盘过磅记录、抓拍图片文件这样划分还有一个好处:硬件更换成本被隔离在采集模块内部。比如今天用耀华仪表,明天换托利多仪表,只需要改称重采集模块里的协议解析,业务服务和数据库表结构完全不用动。2.3 为什么选Java而不选C#或Python选Java做这类系统,最直接的理由是海康威视官方SDK提供了Java版本,底层通过JNA调用本地动态库,Java工程师不需要为了一个地磅项目去学C。芊熠摄像头对接走HTTP上报,Java后端天然就是干这个的。相比C#在工控机上部署要装.NET Framework,Python在Windows服务化管理又麻烦,Java在这种常年无人维护的工控环境里反而是最省心的选择。另一个现实原因是团队可持续维护。企业计量系统的生命周期通常超过五年,中间会经历好几轮维护人员更替。Java开发者的供给量大,后续移交不会找不到人。不过Java也不是没有代价,海康SDK的JNA回调需要小心管理堆外内存,串口读取在高版本JDK上偶尔会碰到兼容性问题。这些坑放到第5章细说,但架构层面一定要预留日志开关,否则现场出问题时,你只能面对一个黑匣子。3. 称重仪表数据接入:串口读取、落定判断与落库实现称重仪表是整个系统里最“硬”的部分,数据来源是一个物理串口。多数地磅仪表通过RS232接口与工控机连接,少数老旧现场会用RS485。RS232接线简单,但距离受限,超过15米建议转RS485。实机上需要确认清楚,不然会浪费大半天在信号干扰上。3.1 串口通信配置:jSerialComm的初始化参数地磅仪表最常见的串口参数是波特率9600、数据位8、停止位1、无校验、无流控,也就是常说的“9600, 8, N, 1”。不同厂家的仪表可能用4800或19200,但数据结构基本一致。这里用jSerialComm作为串口库,原因是它在Windows和Linux上都能跑,而且不依赖额外本地包,RXTX虽然老牌,但在新JDK上编译经常出问题。import com.fazecast.jSerialComm.SerialPort; public class ScaleSerialReader { private SerialPort port; public void open(String portName, int baudRate) { port SerialPort.getCommPort(portName); port.setBaudRate(baudRate); // 地磅仪表一般为9600或4800 port.setNumDataBits(8); port.setNumStopBits(SerialPort.ONE_STOP_BIT); port.setParity(SerialPort.NO_PARITY); port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED); port.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 200, 300); if (!port.openPort()) { throw new IllegalStateException(串口打开失败: portName); } } }参数说明:波特率是串口通信的“节奏”,双方不一致时读到的全是乱码。数据位、停止位、校验位组合为8/N/1时,绝大多数仪表都能正常通信。超时参数里,200是单次读的超时毫秒数,300是两次读之间的间隔,这样能避免主线程长时间卡死在read()上。USB转串口线在现场很常见,虚拟出的COM口编号可能不固定,建议把串口号放到配置文件里,而不是写死在代码中。3.2 重量帧解析:ASCII协议、落定标志与负值处理仪表返回的数据是ASCII文本帧,但具体格式各家有差异。常见格式是“ST,GS,001234.5kg”,其中ST表示帧头,GS表示当前重量已经落定,后面是重量值。另一种常见格式是不带单位,直接输出“ST,GS,001234.5”。解析时不要写死单位,用提取数字的方式处理更稳妥。// 读取串口数据并解析重量帧,帧格式参考:ST,GS,001234.5 public void parseFrame(byte[] buffer) { String line new String(buffer, StandardCharsets.US_ASCII).trim(); // 帧头为ST,GS表示落定,数字部分从第四位到末尾或单位前 if (line.startsWith(ST) line.contains(GS)) { String weightStr line.substring(4).replace(, ).replace( , ); BigDecimal weight new BigDecimal(weightStr); this.currentWeight weight.setScale(2, RoundingMode.HALF_UP); this.isSettled true; } else { // 帧头不完整或不含GS,说明仪表还在等待数据落定 this.isSettled false; } }这段代码有两个边界要注意。第一个是负数,空车回零时可能出现“-000000.5”,如果直接去掉所有加号和空格,负号会被保留,但前提是解析逻辑必须放在“/-”处理之后。第二个是连续帧粘连,仪表在持续发送时,一次read()可能读进来两帧甚至三帧,字符串会变成“ST,GS,001234.5ST,GS,001235.0”。解决办法是按“ST”做切分,或者每次只截取固定长度,否则BigDecimal解析会直接抛异常。抓到这个问题的程序员大多已经过了第一关。3.3 重量落定判断与过磅记录落库车辆从开上地磅到完全停稳,重量读数会经历一段衰减过程。如果仪表刚跳到1000kg就立刻记录,两秒后可能变成985kg,这个误差在地磅计量的场景下没法接受。所以业务层不能只信任一帧GS标志,还要做滑动窗口判断:连续N次读数之间的最大差值小于阈值,才认为重量真正落定。// 连续5次读数差值不超过10kg时,认为重量已落定 private final LinkedListBigDecimal recentWeights new LinkedList(); private static final BigDecimal THRESHOLD new BigDecimal(10); private static final int SAMPLE_COUNT 5; public boolean settleCheck(BigDecimal newWeight) { recentWeights.addLast(newWeight); if (recentWeights.size() SAMPLE_COUNT) { recentWeights.removeFirst(); } if (recentWeights.size() SAMPLE_COUNT) { return false; } BigDecimal max recentWeights.stream().max(BigDecimal::compareTo).get(); BigDecimal min recentWeights.stream().min(BigDecimal::compareTo).get(); return max.subtract(min).compareTo(THRESHOLD) 0; }SAMPLE_COUNT和THRESHOLD这两个参数必须根据现场地磅量程调整。150吨的大地磅,10kg阈值可能太敏感,放宽到30kg很正常;小台秤1kg就够。代码里还有一个隐含问题:recentWeights这个队列不是线程安全的,如果串口读取线程和业务查询线程同时操作它,会出现并发修改异常。需要用synchronized修饰settleCheck方法,或者改用ConcurrentLinkedDeque。落库表结构方面,我的常用设计如下:CREATE TABLE weigh_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, plate_no VARCHAR(20) COMMENT 车牌号, gross_weight DECIMAL(10,2) COMMENT 毛重(kg), tare_weight DECIMAL(10,2) COMMENT 皮重(kg), net_weight DECIMAL(10,2) COMMENT 净重(kg), image_path VARCHAR(255) COMMENT 海康抓拍图片路径, plate_path VARCHAR(255) COMMENT 芊熠识别小图路径, weigh_time DATETIME COMMENT 过磅时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );重量字段必须用DECIMAL,不要用FLOAT或DOUBLE,计量数据不允许浮点数误差。weigh_time和plate_no要建联合索引,因为日后的统计报表、对账查询基本都是按时间段加车牌来做。4. 集成海康威视与芊熠摄像头:抓拍、车牌识别与业务绑定摄像头部分是这个项目最容易被误解的地方。很多人以为把视频流接进来能看监控就行,但实际业务要的是“抓拍一张照片、识别一个车牌号、跟重量记录绑死”。所以海康威视和芊熠的接入方式完全不同:海康走SDK做抓拍,芊熠走HTTP回调做车牌识别。4.1 海康威视SDK登录与抓拍:JNA接口的调用细节海康威视摄像头有两种对接方式。一种是RTSP地址取视频流,适合做网页实时预览;另一种是官方设备网络SDK,用来做登录、抓拍、报警布防。这个项目里推荐用SDK,因为RTSP取流之后要在Java端做图像转码,工作量大而且容易花屏,SDK直接给出JPEG文件,省时间也省内存。海康官方提供的Java SDK本质是用JNA封装C动态库,使用时需要把HCNetSDK.jar放入工程,把HCNetSDK.dll及配套依赖放到运行目录。新手经常碰到UnsatisfiedLinkError,原因大多是依赖库不全,或者动态库路径没有加入java.library.path。登录代码的核心如下:// 海康威视SDK登录,使用JNA调用HCNetSDK HCNetSDK hcNetSDK HCNetSDK.INSTANCE; NET_DVR_USER_LOGIN_INFO loginInfo new NET_DVR_USER_LOGIN_INFO(); loginInfo.wPort (short) 8000; // 海康标准SDK端口 loginInfo.sDeviceAddress cameraIp.getBytes(); loginInfo.sUserName username.getBytes(); loginInfo.sPassword password.getBytes(); NET_DVR_DEVICEINFO_V40 deviceInfo new NET_DVR_DEVICEINFO_V40(); boolean initSuccess hcNetSDK.NET_DVR_Init(); if (!initSuccess) { throw new RuntimeException(SDK初始化失败); } int userId hcNetSDK.NET_DVR_Login_V40(loginInfo, deviceInfo); if (userId 0) { int errorCode hcNetSDK.NET_DVR_GetLastError(); throw new RuntimeException(登录失败,错误码: errorCode); }NET_DVR_Init在JVM生命周期里只需要调用一次,千万不要设计成每次登录都初始化,否则句柄泄漏会直接拖垮系统。登录端口默认8000,但部分录像机或定制固件可能改过端口,以设备网络参数页为准。登录返回的userId大于0才算成功,后续所有抓拍调用都要带着这个userId。抓拍接口本身并不复杂:// 触发抓拍,保存为JPEG文件 NET_DVR_JPEGPARA jpegPara new NET_DVR_JPEGPARA(); jpegPara.wPicSize (short) 0; // 0表示设备默认分辨率 jpegPara.wPicQuality (short) 0; // 0表示最佳画质 String savePath D:/weigh/captures/20240512_143000.jpg; boolean capture hcNetSDK.NET_DVR_CaptureJPEG(userId, 1, jpegPara, savePath); if (!capture) { int err hcNetSDK.NET_DVR_GetLastError(); System.err.println(抓拍失败,错误码: err); }注意通道号,枪机一般是1,球机会有多个通道,接错通道会抓到另一路画面。保存路径最好全英文,不要放在中文目录下,部分海康固件在中文路径上会返回失败。文件名用时间戳加业务序列号,排错时一眼能看出是哪一次过磅。抓拍还有一种更高级的姿势:用报警布防,当检测到车辆进入监控区域时自动抓拍。这需要调用NET_DVR_SetDVRMessageCallBack,回调方法在JNA里直接操作堆外内存有风险。常规做法是把回调里的图像字节复制到一个普通byte[]数组,交给独立线程池处理,回调方法本身不碰数据库。4.2 芊熠摄像头车牌识别对接:HTTP回调与字段解析芊熠摄像头的牌识功能是设备端自带的,不需要Java侧跑模型。摄像头识别到车牌后,主动把结果POST到后台地址。你需要做的就是在后台管理页填一个上报地址,然后写一个接收回调的Spring Boot接口。// 接收芊熠识别结果回调 RestController RequestMapping(/api/plate) public class PlateCallbackController { private final PlateEventBuffer buffer; // 摄像头POST的JSON示例:{plateNo:苏A12345,confidence:92,channel:1} PostMapping(/callback) public MapString, Object receive(RequestBody MapString, Object payload) { String plateNo String.valueOf(payload.getOrDefault(plateNo, )); int confidence Integer.parseInt(String.valueOf(payload.getOrDefault(confidence, 0))); String channel String.valueOf(payload.getOrDefault(channel, 1)); if (plateNo.length() 6 confidence 85) { buffer.add(new PlateEvent(channel, plateNo, LocalDateTime.now())); } return Map.of(code, 0, msg, ok); } }字段名在不同型号的芊熠摄像头里可能是“plateNo”“carPlate”“license”几种,联调时先用摄像头后台的调试工具看原始报文,再按实际字段解析。置信度阈值85是经验值,现场光线差时可以降到70,但太低了会把遮挡严重的车牌误识别成别的好牌,后续对账麻烦事更多。上报策略也要在摄像头后台设好。推荐选“识别到立即上报,内容变化才上报”,不要选定时上报,否则同一辆车在画面里停留三分钟,后端会收到几十条重复车牌,内存和时间片都被浪费。回调地址结尾加一个token参数,例如http://192.168.1.100:8080/api/plate/callback?tokenabc123,防止别人扫到地址后随便刷接口。4.3 把抓拍图片、车牌号与重量记录绑定成一条过磅数据异步事件的绑定是这个系统里最容易发生数据错乱的地方。车牌识别结果可能比重量落定事件早到3秒,也可能晚到5秒,如果业务逻辑是“先来先组合”,两条相邻车辆的数据就会被串到同一条记录里。所以我习惯用状态机加时间窗口来处理合并。// 过磅会话状态机:逐步合并异步到达的事件字段 public class WeighSession { private String plateNo; private BigDecimal grossWeight; private String imagePath; private LocalDateTime settleTime; private LocalDateTime lastUpdateTime; public boolean isComplete() { return plateNo ! null grossWeight ! null imagePath ! null; } public WeighRecord toRecord() { WeighRecord record new WeighRecord(); record.setPlateNo(plateNo); record.setGrossWeight(grossWeight); record.setImagePath(imagePath); record.setWeighTime(settleTime); return record; } public void updateLastTime() { this.lastUpdateTime LocalDateTime.now(); } }后台要有一个定时任务,每隔两秒扫描当前活跃的过磅会话,超过10秒仍未补全字段的会话直接关闭,释放内存。时间窗口的长短根据现场车流速度调整,普通料场8到10秒够用,高峰期排队紧凑的厂区可能要放宽到20秒。关闭会话之后如果还有迟到事件到达,那就不会再被合并,宁可不记录也不记录错。抓拍的触发时机建议放在重量落定之后,而不是车牌识别之后。原因很简单:车都停稳了再拍,照片里车牌和车身的相对位置是最标准的。如果车牌识别先触发抓拍,车还没停稳,等于拍了一张运动模糊的废图。5. 过磅系统集成中的常见问题与排查:五类翻车现场与解决以下五个问题是我做地磅项目时在不同现场遇到过的真实情况,按出现频率排序。每一条都按“现象→原因→解决”的口径写,方便你直接对照排查。5.1 现象:串口读到乱码或重量跳变串口调试助手能正常看到文本帧,但Java程序读出来是乱码;或者重量值偶尔从1000kg跳到0又跳回来。原因多半是串口参数不匹配,少部分是USB转串口线芯片质量差导致数据错位。仪表说明书标的波特率是9600,但程序里设成4800,读出来的字节自然不对。另一种情况是工控机和仪表之间地电位不一致,RS232在这种状态下会出现间歇性误码。解决方法是先用串口调试工具按9600,8,N,1参数打开,观察是否有完整文本帧。如果没输出,按4800、19200各试一轮。如果调试工具读正常但Java乱码,对比SerialPort初始化参数是否每项都设置到位,尤其是有没有漏掉setNumStopBits和setParity。干扰问题则把串口线换成带磁环的屏蔽线,或者加一个USB转串口隔离器,成本不高但效果立竿见影。5.2 现象:海康威视SDK登录失败,错误码看不懂调用NET_DVR_Login_V40返回-1,日志打印出错误码,但查手册找不到对应含义。高频错误码其实就几个:7代表网络不通,23代表设备地址错误,27代表用户名或密码错误。出现27的另一个常见原因是摄像头Web管理页里关闭了SDK访问权限,现在很多海康设备默认只开放ONVIF,自有SDK协议需要手动打开。解决步骤是:先用ping确认摄像头IP通不通,再登录摄像头Web管理页,在网络设置的高级选项里找到“集成协议”,打开SDK开关,给操作员账号分配SDK访问权限。然后重新调用登录接口,看错误码是否变化。还有一个容易忽略的点:工控机上装了多个网卡时,Java进程可能走了错误的出口网卡,导致访问不到摄像头所在的独立网段。可以在JVM启动参数里加上-Djava.net.preferIPv4Stacktrue,必要时指定bind地址。5.3 现象:芊熠识别结果回调不到或延迟严重摄像头画面里能看到车牌识别框,但后台接口一直收不到回调;或者收到了,但时间滞后好几秒。原因通常有三个:上报地址填错、上报策略设成定时上报、摄像头与Java服务之间的网络被防火墙拦截。先用Postman或浏览器直接请求回调地址,确认接口本身能通。然后在摄像头后台把上报地址改成正确的服务器IP加端口,注意不要漏掉端口号。如果摄像头后台有“测试上报”按钮,点一下看日志输出是否正常。上报策略改成“识别到立即上报且变化才上报”,这样既减少延迟,也不会产生重复数据。排错时在回调接口里加一行完整报文日志,最容易看出是哪里的字段名对不上。5.4 现象:图片落盘成功,但业务表里对应记录缺失本地磁盘上明明有抓拍图片,数据库里image_path却是空的;或者image_path有值,但点击图片404。根本原因是抓拍图片的IO操作和数据库事务没有放在同一个生命周期里。NET_DVR_CaptureJPEG写文件是独立动作,如果随后业务事务因为重量数据校验失败回滚,图片已经生成但数据库没记录。反过来,如果图片路径存的是相对路径,应用服务器换目录之后,文件路径全部失效。解决套路是:图片先落盘,再提交数据库事务,事务失败后由定时任务清理孤儿图片。路径一律用绝对物理路径加URL映射,比如静态资源映射/weigh/xxx.jpg指向D:/weigh/captures/xxx.jpg,数据库只存映射后的相对路径。这样既保证文件可访问,又不至于把整台服务器的磁盘路径暴露给前端。5.5 现象:JVM内存缓慢上涨,摄像头SDK疑似泄漏系统运行三到五天后,内存占用持续上升,GC越来越频繁,最终OutOfMemoryError导致服务崩溃。这类问题大多出在海康SDK的JNA回调上。回调过程中分配的ByteBuffer属于堆外内存,如果只引用没有释放,或者对象被Spring容器长期持有,GCRoot一直可达,内存就只增不减。解决方式是回调方法里尽量不用ByteBuffer.allocateDirect,改用普通byte[]接收再复制到业务队列。回调线程本身只做“取数据、放队列”两件事,不让它碰数据库或业务逻辑。同时给活跃过磅会话的列表加上上限判断,超过阈值强制关闭最早的会话,避免异常过磅导致队列持续堆积。如果现场有条件,用Java Flight Recorder抓一段内存分配火焰图,能定位到具体是哪个方法在持续分配,但最实用的原则还是“即取即销,不积攒”。6. 进阶:验证过磅系统并发能力与数据完整性的方法系统开发完成不代表可以上线,先在测试环境跑通完整的事件合并流程才是正经事。我会用模拟器代替真实硬件,随机产生重量、车牌、抓拍事件,让它们按照真实的异步节奏进入系统,然后检查最终落库的记录数量和数据完整性。// 并发模拟200次过磅,验证事务绑定逻辑 ExecutorService pool Executors.newFixedThreadPool(4); for (int i 0; i 200; i) { final int seq i; pool.submit(() - { weighService.onSettled(BigDecimal.valueOf(5000 seq)); plateService.onPlateRecognized(苏A String.format(%05d, seq)); captureService.onCaptureSaved(/weigh/captures/ seq .jpg); return null; }); } pool.shutdown(); pool.awaitTermination(5, TimeUnit.MINUTES);这只是一个演示思路,真实模拟器还要加入随机延迟。比如重量事件在200到500毫秒之后到达,车牌事件在1到3秒之后到达,抓拍事件可能早于车牌也可能晚于车牌,这样才逼得状态机处理真正不按顺序的事件。模拟结束之后,执行下面这条SQL,直接看数据完整性:SELECT COUNT(*) AS total, SUM(CASE WHEN plate_no IS NULL OR plate_no THEN 1 ELSE 0 END) AS missing_plate, SUM(CASE WHEN image_path IS NULL OR image_path THEN 1 ELSE 0 END) AS missing_image FROM weigh_record;total应该等于200,missing_plate和missing_image都应该为0。只要有一个字段大于0,就说明会话合并逻辑或者事件回调链路里有数据丢失,直接去查对应的过磅会话是被谁关掉的。我习惯在每个工厂上线前再补一道脚本检查,扫描图片目录,把所有数据库里引用的路径映射到磁盘,核对文件是否存在且大小不为0。这是因为有一次某项目上线前,我发现有10条记录没有图片,当时觉得靠重量和车牌也能对账,结果月底客户拿着无图记录来扯皮,说少了一车货,查了三天才查出是海康抓拍回调在某个并发瞬间丢了图片路径。从那以后,任何一条记录缺少任一字段就不允许出报表,这条规则已经写进所有我经手的过磅系统里。希望帮到你。本文还有配套的精品资源点击获取