尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot对接GAT/1400协议:人脸车牌数据上报实战

SpringBoot对接GAT/1400协议:人脸车牌数据上报实战 简介面向需要将视图库数据接入视频专网平台的Java后端开发人员这份基于SpringBoot的轻量级GAT/1400协议对接工具聚焦人脸识别与车牌识别结果上传场景目标是让国标接入做到开箱即用。项目已完整实现在线设备注册与保活、摘要认证登录、服务时间同步、人脸与车牌检测结果上报等核心业务并针对大华、海康主流平台完成适配验证开发者无需从零编写SIP/XML信令解析可直接将识别数据标准化上传。资源包共包含33个文件其中27个Java源文件构成主体业务逻辑pom.xml与application.yml提供Maven环境与运行配置README.md补充部署步骤和接口调用说明.gitignore辅助版本管理整包仅92KB导入工程后即可快速定位核心代码规范分包便于二次开发。目前已有62人学习使用尤其适合正在搭建视图库对接模块、需要快速交付或二次集成的工程师能大幅降低国标协议接入门槛并缩短开发周期。 最近在做一个安防对接项目甲方要求前端的人脸抓拍机、车牌识别相机抓到的结构化数据必须按GAT/1400国标协议上报到视图库平台。这个需求听起来挺明确不就是按文档拼个JSON POST上去嘛实际做起来零零碎碎的坑全碰了一遍。这篇文章把思路和代码实现整理出来给后面接同样协议的兄弟留个参考。这套东西适合谁来用如果你是Java后端手里有SpringBoot项目需要把前端设备的人脸、车牌数据统一转成国家标准协议格式上报给上级平台这篇文章就是按这个场景写的。我已经把注册、心跳、校时、数据上报完整跑通了代码也是可以直接抄的写法。1. 这项目到底在做什么GAT/1400对接的前因后果1.1 一句话讲清GAT/1400GAT/1400全称是GA/T 1400.4-2017《公安视频图像信息应用系统 第4部分接口协议要求》行业里习惯叫“1400协议”或者“视图库对接协议”。它定义了一套前端设备人脸抓拍机、车牌相机、卡口设备等把结构化数据上报给视频专网平台的标准接口规范。这套协议解决的最核心问题就是让不同厂商的设备都能用同一种语言和平台沟通。以前每家设备厂商都有自己的私有协议平台方接入一个品牌就要写一套解析逻辑设备一多维护成本直接爆炸。有了GAT/1400以后平台只看协议字段不关心你前端是什么牌子人脸、车牌、人体、视频这些数据都能用统一格式往上走。1.2 整个数据链路长什么样很多刚接触的人会把GAT/1400理解成“前端设备直接和平台通信”实际项目中不完全是这么回事。我这次接的场景是前端设备人脸门禁机、车牌识别相机先把抓拍数据通过厂商私有协议推到我们的中间服务中间服务负责统一清洗、补全字段再封装成GAT/1400格式上报给视图库平台。数据链路大概是人脸门禁机抓拍到人脸 → 厂商SDK/HTTP回调推给我们 → 服务端解析出人脸特征和抓拍图片地址 → 按1400协议封装上报平台车牌识别相机识别到车辆 → 相机通过HTTP或MQTT把过车记录推给我们 → 服务端补全车牌、车身颜色、过车时间等信息 → 按1400协议封装上报平台我在这个项目里的角色相当于一个“协议转换中枢”。这个设计的好处是当前端设备品牌变了或者换了另一套私有协议只需要改接入层不影响上游1400上报逻辑。如果你的项目里前端设备本身就支持GAT/1400那就不需要中间服务设备直接配置平台地址就行更简单。1.3 为什么我选了SpringBoot而不是其他方案对接类服务最重要的是稳定和容易维护。SpringBoot在Java后端里的普及度不用多说社区资料多出了问题好查Spring定时任务做心跳、线程池做并发上报都很方便。另一个原因是团队技术栈就是Java。如果团队是Go或Python为主完全可以用其他语言实现协议本身不挑语言。选型这件事没有绝对标准关键是交付之后有人能维护。补充一点现在很多新项目已经用Spring Boot 3.x了需要注意javax包名换成了jakartaRestTemplate在3.x里的默认实现也有调整这些细节后面坑里面我会单独说。2. 动手前必须吃透的GAT/1400核心机制2.1 六步流程注册、校时、心跳、布防、上报、注销GAT/1400的对接流程可以拆成六步顺序很重要步骤接口路径作用是否必须1. 注册/VIID/Registers设备编码和密码校验上线必须2. 校时/VIID/Times同步设备和平台时间避免数据时间戳偏差建议必须3. 心跳/VIID/Keepalives周期性上报存活状态必须4. 布防/VIID/Subscribes订阅上报数据类型视平台要求5. 数据上报/VIID/Notifications推送抓拍数据必须6. 注销/VIID/Unregisters下线上报运维时用注册成功后才能继续下一步这是最基础的约束。有些平台会严格要求先校时再心跳我做的时候发现平台对不校时的设备会在日志里打警告。布防这个步骤有的平台不需要因为平台侧可以主动下发订阅设备端只要上报即可这块要跟平台负责人确认别自己按文档想当然。心跳间隔一般配置成30秒但具体以平台要求为准有的平台要求15秒有的60秒。如果超过平台设定的超时时间还没收到心跳平台会把设备标记为离线后续上报可能直接被拒。2.2 VueDataInfo 这个外包装怎么理解GAT/1400协议里有个很关键的概念——VueDataInfo。所有接口请求的Body最外层都是这个结构它就是一层“快递包装”里面包裹着真正的业务数据。外层结构大致是{ VueDataInfo: { VueData: base64编码后的业务数据, EncodeType: 1, DataEncrypt: 0, EncryptInfo: {}, DataDigest: { Algorithm: sha-256, Digest: 摘要值 }, DataSignature: { Algorithm: RSASSA-PKCS1-v1_5, Signature: 签名值 } } }VueData就是核心了它本身是一段JSON或XML再经过Base64编码。比如注册接口的VueData解码后是Register对象数据上报里的VueData解码后是Notify对象。DataDigest是对VueData做SHA-256摘要平台校验数据完整性DataEncrypt决定业务数据是否加密DataSignature是数字签名用来做身份验证。刚开始接触这个结构的人容易被绕晕其实拆开理解就是body里真正有意义的是那串Base64其他的都是为了保证传输过程中的完整性和安全性。2.3 人脸和车牌数据长什么样数据上报里最关键的是Object数组里面可以放Face对象也可以放MotorVehicle对象甚至可以混合打包多条数据。人脸对象里常用的字段有这些{ Face: { FaceID: 设备编码时间戳生成的唯一ID, InfoKind: 1, SourceID: 来源标识, DeviceID: 设备编码, LocationMarkTime: 定位标记时间, AppearTime: 出现时间, DisappearTime: 消失时间, FaceRect: [100, 200, 300, 400], ImageID: 人脸图片ID, FeatureID: 人脸特征ID, FeatureValue: 人脸特征值 } }车牌对象常用的字段{ MotorVehicle: { MotorVehicleID: 车辆记录唯一ID, InfoKind: 1, SourceID: 来源标识, DeviceID: 设备编码, LocationMarkTime: 过车时间, AppearTime: 过车时间, DisappearTime: 离场时间, PlateNo: 京A12345, PlateColor: 1, VehicleColor: 2, VehicleBrand: 品牌, VehicleModel: 车型, ImageID1: 车辆图片ID } }字段具体含义以你对接的平台提供的《接入手册》为准不同省份、不同平台会有细微差异。比如PlateColor的枚举值有的平台用数字有的平台用字符串这些拿到文档第一件事就是核对。3. SpringBoot工程里怎么一步步落地3.1 工程与依赖搭建老规矩先建一个独立的SpringBoot模块或者直接在你的后端服务里加一个gat1400的子包。我用的是Gradle构建Maven同理核心依赖就这些implementation org.springframework.boot:spring-boot-starter-web implementation com.google.code.gson:gson:2.10.1 implementation org.apache.httpcomponents.client5:httpclient5:5.3.1SpringBoot自带的RestTemplate可以解决普通HTTP请求但GAT/1400的Digest认证支持得不好所以我直接引入了Apache HttpClient 5把HttpClient作为RestTemplate的底层实现这样既能用RestTemplate的API又能让Digest认证交给HttpClient处理。配置类先写出来Data ConfigurationProperties(prefix gat1400) Component public class Gat1400Properties { /** 平台地址如 http://192.168.1.100:9200 */ private String serverUrl; /** 本机设备编码 */ private String deviceId; /** 接入账号 */ private String username; /** 接入密码 */ private String password; /** 协议版本 */ private String protocolVersion GA/T1400.4-2017; /** 心跳间隔秒数 */ private Integer keepaliveInterval 30; /** 平台VIID编码注册响应里会返回 */ private String targetViid; }接入平台给的测试环境一般会提供这些参数平台地址、设备编码、接入账号和密码。TargetVIID是平台在注册响应里返回的注意要动态保存。3.2 注册、心跳、校时的具体实现先把一个重用的方法抽出来。所有接口都要走同一套“拼VueDataInfo”的逻辑private VueDataInfo buildVueDataInfo(Object bizData) { String json new Gson().toJson(bizData); String vueData Base64.getEncoder() .encodeToString(json.getBytes(StandardCharsets.UTF_8)); VueDataInfo wrapper new VueDataInfo(); wrapper.setVueData(vueData); wrapper.setEncodeType(1); wrapper.setDataEncrypt(0); DigestInfo digest new DigestInfo(); digest.setAlgorithm(sha-256); digest.setDigest(Sha256Utils.hex(vueData)); wrapper.setDigest(digest); return wrapper; }Sha256Utils就是一个SHA-256的工具类返回十六进制字符串代码很常规网上随便都能找到不单独贴了。注册接口实现public boolean register() { Register register new Register(); register.setDeviceId(props.getDeviceId()); register.setProtocolVersion(props.getProtocolVersion()); register.setLocalTime(TimeUtils.now()); HttpResponse response post(/VIID/Registers, buildVueDataInfo(register)); // 解析响应中的 ResponseStatus.StatusCode0 表示成功 boolean success parseStatusCode(response) 0; if (success) { // 保存平台返回的 TargetVIID String targetViid parseTargetViid(response); props.setTargetViid(targetViid); } return success; }这里用HttpClient直接发请求POST方法统一带上Digest认证头。HttpClient的Digest认证是内置能力只需要设置CredentialsProviderprivate void addAuth(HttpClientBuilder builder) { CredentialsProvider provider new BasicCredentialsProvider(); provider.setCredentials( new AuthScope(null, -1), new UsernamePasswordCredentials(props.getUsername(), props.getPassword()) ); builder.setDefaultCredentialsProvider(provider); }然后设置一个POST请求后自动重试的机制。因为Digest认证流程是第一次请求不带认证信息服务端返回401并带出realm和nonce客户端拿到nonce后再带Authorization头重发。HttpClient默认会在收到401后自动用CredentialsProvider重新请求这个机制是RestTemplate自己搞不定的。心跳用Spring的定时任务实现项目启动注册成功后自动开启Scheduled(fixedDelayString ${gat1400.keepalive-interval:30}000) public void keepalive() { if (!registered) { return; } Keepalive keepalive new Keepalive(); keepalive.setDeviceId(props.getDeviceId()); keepalive.setLocalTime(TimeUtils.now()); post(/VIID/Keepalives, buildVueDataInfo(keepalive)); }校时接口可以放在注册之后调用一次public void syncTime() { TimeRequest req new TimeRequest(); req.setDeviceId(props.getDeviceId()); req.setLocalTime(TimeUtils.now()); post(/VIID/Times, buildVueDataInfo(req)); }3.3 人脸与车牌数据上报的代码实现数据上报是最核心的接口。前端设备推过来的数据是厂商私有协议格式我们要做的是把关键字段映射成GAT/1400的标准字段然后打包上报。核心Servicepublic void reportFaceAndVehicle(ListFaceCapture faceList, ListVehicleCapture vehicleList) { if (CollectionUtils.isEmpty(faceList) CollectionUtils.isEmpty(vehicleList)) { return; } Notify notify new Notify(); notify.setNotificationId(UUID.randomUUID().toString().replace(-, )); notify.setInfoKind(0); notify.setSourceID(props.getDeviceId()); notify.setTargetVIID(props.getTargetViid()); notify.setLocalTime(TimeUtils.now()); ListNotifyObject objects new ArrayList(); for (FaceCapture face : faceList) { objects.add(NotifyObject.builder().face(FaceMapper.toGat1400(face, props.getDeviceId())).build()); } for (VehicleCapture vehicle : vehicleList) { objects.add(NotifyObject.builder() .motorVehicle(MotorVehicleMapper.toGat1400(vehicle, props.getDeviceId())) .build()); } notify.setObjectList(objects); post(/VIID/Notifications, buildVueDataInfo(notify)); }一个人脸抓拍数据映射成1400格式核心代码是public static Face toGat1400(FaceCapture source, String deviceId) { Face face new Face(); face.setFaceId(source.getCaptureId()); face.setInfoKind(1); face.setSourceID(source.getSourceId()); face.setDeviceID(deviceId); face.setLocationMarkTime(source.getCaptureTime()); face.setAppearTime(source.getAppearTime()); face.setDisappearTime(source.getDisappearTime()); // 人脸坐标左边距、上边距、宽、高 face.setFaceRect(new int[]{ source.getLeft(), source.getTop(), source.getWidth(), source.getHeight() }); face.setImageID(source.getImageId()); face.setFeatureID(source.getFeatureId()); face.setFeatureValue(source.getFeatureValue()); return face; }车牌数据的映射类似只是字段更多一些。需要注意的是车牌号、车身颜色这些值如果前端设备返回的是内部编码必须做一层枚举转换把厂商的编码映射成GAT/1400标准枚举否则平台那边看到的颜色和品牌是乱的。图片这一块前端设备可能只传图片URL而平台要求传Base64内容或者至少图片ID要能通过其他接口拉取。如果平台要求内容那你还需要在服务端下载图片转Base64再塞进去这会导致报文体积暴增要注意后面说的超时问题。4. 那些不踩一遍根本想不到的坑4.1 注册成功之前什么都别做我一开始图省事注册还没返回成功就先把一条人脸数据上报出去了结果平台直接打回错误码提示“设备未注册或已离线”。后来才意识到GAT/1400平台对设备状态是有严格校验的平台收到上报数据后会先查这个设备是不是在线状态不是注册过就行。解决的方案很朴素在项目启动后先调用注册接口确认注册成功再把“registered”标志位置为true。心跳定时任务、数据上报方法入口都检查这个标志没注册就返回失败并告警。4.2 时间格式不统一平台不认你的时间戳GAT/1400协议里的时间是“yyyy-MM-dd HH:mm:ss”格式注意是空格分隔不是“T”。我的设备厂商推过来的时间是ISO8601格式比如“2024-09-01T12:30:00”直接塞进去上报平台解析失败状态码返回的是签名或数据异常排查了很久才发现是时间格式的问题。统一封装一个时间转换工具所有接口都走这个入口public static String now() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); } public static String format(LocalDateTime time) { return time.format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); }4.3 Digest认证第一次请求要先拿nonce别用RestTemplate硬怼这是整个项目里折腾最久的一个问题。一开始我直接用Spring的RestTemplate发POST带了错误的Authorization头自己拼的平台一直返回401。后来抓包发现Digest认证需要先发一次不带认证的请求从401响应头里拿realm和nonce然后第二次请求才带Authorization。换成Apache HttpClient后这个问题直接消失因为HttpClient原生支持Digest认证的401重新协商机制。如果你们团队不愿意引HttpClient也可以手动实现两段式请求先发一次拿nonce再带认证头发第二次但代码维护起来比较麻烦我建议直接用HttpClient。4.4 图片Base64塞满整个报文上报超时被平台丢弃人脸图片和车牌图片都是以Base64形式放在上报数据里的一张图片动辄几百KB一次上报几条数据报文就能到几MB。平台一般有超时限制和报文大小限制太大会直接超时。两种方案我都实测过第一种是控制单次上报次数比如一次最多20条数据超过就分批第二种是跟平台确认是否支持传图片ID或URL提供图片下载接口让平台来拉取这种方案对服务端压力最小。如果平台要求必须有图片内容那就做一个“库表暂存异步批量上报”的机制避免阻塞业务接口。4.5 SpringBoot版本升级带来的隐形影响项目里如果用Spring Boot 3.x要记得原来的javax.servlet、javax.annotation全部换成jakarta前缀尤其涉及自定义过滤器和拦截器的时候。另外Spring Boot 3.x里RestTemplate默认不是用SimpleClientHttpRequestFactory配置超时的方式略有变化这些细节不处理测试环境一切正常上线就出幺蛾子。5. 再做一步数据完整性校验与日志留痕数据上报这种对接最怕平台说“没收到”或者“收到但数据异常”。我从一开始就要求在数据库里记录每次上报的请求体和平台响应表结构至少包含上报时间、业务数据类型、上报状态、平台状态码、失败原因、数据唯一ID。这样做的价值在项目联调阶段体现得特别明显。前端设备传过来一条数据库里能看到这条数据有没有进到我们的服务上报后能看到平台返回的是什么错误码。如果没有这一步出了问题只能靠猜。日志里也要打清楚日志级别至少INFO包含设备编码、上报类型和平台返回码线上排查问题时逐条对着看能省一大半时间。最后再分享一个小建议不管平台文档写得多清楚拿到对接文档后先做一份“字段映射表”把前端厂商字段和1400协议字段一行行列出来跟平台负责人逐项核对。大多数对接问题都出在字段理解不一致上这一步做扎实了联调会顺畅很多。本文还有配套的精品资源点击获取
返回列表