
做AGV仓储项目这几年被同行问得最多的问题是WMS里已经生成了搬运任务怎么让现场的海康AGV真正跑起来很多人第一次接触Java集成海康RCS系统AGV任务下发时以为就是调两个HTTP接口的事真正接入后才发现认证鉴权、任务状态机、轮询限流、车辆调度逻辑每个环节都有看不见的坑。这篇把从零对接海康RCS的关键代码、设计思路和踩坑经过完整整理出来给准备接RCS的Java团队做个参考也顺便聊聊哪些事不该我们来管。1. 集成之前先把AGV调度这件事的边界搞清楚1.1 业务系统和RCS到底谁说了算一句话回答业务系统只负责“产生搬运需求”RCS负责“怎么搬、哪台车搬、走哪条路”。我在项目里见过不少团队试图绕过RCS直接去控制AGV结果无一例外都碰壁了因为硬件层、调度层的逻辑太复杂。一个完整AGV搬运链条通常是这样的层级承担系统核心职责业务层WMS/MES/ERP生成搬运指令比如“货架A搬到拣选台B”集成层Java集成服务把业务指令翻译成RCS能识别的任务格式并跟踪状态调度层海康RCS路径规划、车辆分配、交通管制、充电调度、任务拆分执行层AGV本体接收RCS指令执行行走、举升、放货等机械动作从这张表能看到RCS是调度大脑。它内部集成了路径规划算法、多车防碰撞逻辑和任务优先级仲裁这些在对接时完全不用我们关心——哪怕热搜里那些AGV路径规划、A*算法的帖子再热闹对做Java集成的人来说它们属于RCS的“黑盒能力”。我们真正要做的只有三件事下发任务、查询状态、处理异常。很多初学者会纠结要不要学习海康RCS内部的A*算法或者AGV协同调度我的建议是可以当兴趣了解但别陷进去。集成层和调度层的关注点是两套东西把RCS当作一个带鉴权的远程服务来对接反而更清爽。1.2 一个合格的Java集成层该管哪些事结合我几个落地项目来看Java集成层的核心职责可以拆成五块任务转换把WMS的搬运单转成RCS的任务批次TaskBatch和任务单元TaskUnit字段映射往往是最容易出错的地方。接口调用封装登录鉴权、任务下发、状态查询、任务取消等HTTP接口处理超时和重试。状态同步RCS的任务状态变化会发生在服务端集成层必须通过轮询或回调把状态同步回业务库供WMS/前端展示。可靠性兜底网络抖动、服务重启、RCS短暂不可用都要有补偿机制不能让搬运任务凭空丢失。数据留存每次下发都会产生一个任务单据完整留在本地数据库方便以后对账排查。同时要明确哪些事不该做不要直接操作数据库去改RCS里的站点配置不要绕过RCS去给AGV下发底层运动指令。集成层是“翻译器”和“监控器”不是“调度器”。明白这个边界后面写代码就有章法了。2. 环境准备与接口摸底先别急着写代码2.1 技术栈选型与依赖配置海康RCS官方一般不提供开箱即用的Java SDK大多数项目都是自己写HTTP客户端封装。这里先给一套我验证过的技术组合JDK 8或11都可以生产环境以稳定为主Spring Boot 2.x是主流选择HTTP客户端首选OkHttp 3.x/4.x连接池、超时、重试都好控制也可以用Apache HttpClient但代码会啰嗦一些JSON序列化用Jackson或Fastjson看团队习惯我用的Jackson定时任务用Spring自带的Scheduled就够了高并发场景再考虑分布式调度平台。一个参照的pom依赖配置dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency这里有个容易被忽略的细节不要图省事用Hutool的HttpUtil虽然写起来快但每次请求都会新建连接生产环境并发一上来就会把RCS服务端的连接数打满还会本地端口耗竭。OkHttp的ConnectionPool和Dispatcher机制更适合做这种有QPS压力的对接服务。2.2 RCS关键概念任务批次、任务单元、站点编码打开海康RCS的接口文档会被一堆名词淹没。这里先把四个绕不开的核心概念过一遍概念说明对应到代码里access_token登录后拿到的访问凭证有有效期每次请求放Header里站点StationAGV执行搬运动作的物理位置编码作为fromStation和targetStation传入任务批次TaskBatch一次下发的一组任务同一个批次共享一个批次号batchId生成后要能幂等任务单元TaskUnit批次里的具体单个搬运任务一般一个TaskUnit对应“从一个站点搬到另一个站点”站点编码这个坑特别值得提前说RCS里的站点通常是一串有规则的编码比如STATION_LEAVING_01或者P03-2F-A12不是界面上显示的中文名称。我在项目里见过有人直接传了中文名结果AGV根本找不到站点任务一直卡在等待分配。2.3 从接口清单里挑出必用的三个接口海康RCS不同版本的接口路径差异很大但功能维度基本大同小异核心就三个登录鉴权接口POST提交用户名密码或appKey/appSecret返回access_token和过期时间。任务下发接口POST提交任务批次、任务单元列表、优先级、站点信息返回任务批次在RCS侧的编号。任务状态查询接口按批次号或任务号查询任务执行状态、关联的AGV编号、站点信息。另外有条件的话一定要搞清楚有没有事件回调接口。RCS支持在任务状态变化时主动推送到你提供的回调地址。能推就尽量用推送轮询只做兜底。后面我会详细讲为什么。3. 认证封装与HTTP客户端一个能上生产的RcsClient3.1 token的生命周期管理RCS的access_token通常有两小时左右的有效期它的生命周期管理直接决定集成层稳不稳定。最忌讳的写法是每个请求前无条件登录一次这样不仅慢还可能把RCS端的老token顶掉导致别的线程突然401。我的做法是单独封装一个RcsClient组件用volatile变量缓存token到期前5分钟主动刷新刷新动作加锁保证只有一个线程去调登录接口Component public class RcsClient { private final OkHttpClient httpClient; private final RcsProperties properties; private volatile String accessToken; private volatile long tokenExpireAt; public RcsClient(OkHttpClient httpClient, RcsProperties properties) { this.httpClient httpClient; this.properties properties; } /** * 获取有效token若即将过期则由当前线程触发刷新 */ public String getAccessToken() { long now System.currentTimeMillis(); if (accessToken ! null now tokenExpireAt - 5 * 60 * 1000L) { return accessToken; } synchronized (this) { // 双检锁防止多个线程同时刷新 if (accessToken null || now tokenExpireAt - 5 * 60 * 1000L) { refreshToken(); } return accessToken; } } private void refreshToken() { // 调用RCS登录接口解析返回的accessToken和expiresIn // 这里用你们项目现有的JSON库解析即可 } }这种“提前刷新双检锁”的组合几乎不会出问题唯一要留意的是如果RCS登录接口连续失败要抛异常让上层感知不能静默返回旧token。3.2 统一响应体与异常处理RCS的接口返回一般是JSON不管字段叫什么有的版本是code有的是resultCode有的是status核心逻辑都是“非成功码即视为失败”。封装一个泛型响应体会让所有调用代码清爽很多public class RcsResponseT { private int code; private String msg; private T data; public boolean isSuccess() { // 注意有的RCS版本用0表示成功有的用1以文档为准 return code 0; } }3.3 HTTP客户端与连接池的坑OkHttp的配置里超时时间、连接池大小、队列策略都很关键。直接给出我生产环境在用的参数Bean public OkHttpClient rcsHttpClient() { return new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES)) .build(); }这里想特别提醒一点readTimeout不要设置成几秒钟的短超时。AGV任务的调度过程可能很慢RCS收到任务后要等待车辆分配分配不到时任务会排队。如果发下请求后RCS迟迟不给结果30秒读超时还算合理再短就会出现“RCS其实收下了任务但客户端已经超时报错”的尴尬情况。这种超时导致的任务状态不确定是对账机制要解决的头号问题。4. 任务下发主链路的代码实现4.1 任务下发DTO层的设计设计DTO之前我建议先去RCS接口文档确认每个字段的类型和是否必填尤其注意站点编码是字符串还是整型。下面是一个通用性较强的任务下发请求模型public class TaskUnitDTO { private String taskUnitGuid; // 任务单元唯一标识业务系统生成 private String taskUnitName; // 任务单元名称方便在RCS端排查 private String taskTyp; // 任务类型如搬运/举升按文档取值 private String fromStation; // 起始站点编码 private String toStation; // 目标站点编码 private Integer priority; // 优先级数值越高越优先按文档约定 private Integer vehicleNo; // 可选指定车辆编号不传则由RCS调度 } public class TaskBatchSendRequest { private String taskBatchId; // 幂等键业务系统用UUID生成 private String taskBatchName; // 任务批次名称 private ListTaskUnitDTO taskUnitList; }在真实的仓储场景里一次搬运单可能包含多个子任务。比如“从收货区搬到存储区”中间可能要求AGV先到A站点再绕到B站点放货这时候就要拆成多个TaskUnit放在同一个TaskBatch里。我的经验是能拆细的尽量拆细因为RCS的调度是按TaskUnit粒度安排的太粗的任务会让RCS没法做路径优化。4.2 下发与幂等控制任务下发的核心代码逻辑并不复杂麻烦的是让RCS知道“这条任务之前已经发过了别再建一份”。我习惯用taskBatchId做幂等键每次生成UUID。这样如果第一次调用超时、结果没收到重试时RCS能根据batchId识别出批次已存在返回已有的批次号。伪代码逻辑如下public TaskSendResult sendTask(TaskBatchSendRequest request) { String token rcsClient.getAccessToken(); String url rcsProperties.getBaseUrl() rcsProperties.getTaskSendPath(); MapString, Object payload new HashMap(); payload.put(taskBatchId, request.getTaskBatchId()); payload.put(taskBatchName, request.getTaskBatchName()); payload.put(taskUnitList, request.getTaskUnitList()); // 组装OkHttp请求Header中带上Authorization: Bearer token // 执行请求解析RcsResponse if (resp.isSuccess()) { // 返回的data里一般包含RCS侧的批次号 return toTaskSendResult(resp.getData()); } // 根据错误码区分重复提交/参数错误/服务端异常 throw new RcsException(resp.getMsg()); }这里有个非常重要的实操细节返回成功不代表AGV已经在执行了。RCS成功接收任务后还要经历站点分配、车辆分配、路径规划几个内部环节。所以下发成功之后真正的战场在状态同步。4.3 状态轮询与业务状态联动任务下发之后需要把RCS任务状态同步到本地业务表。最可靠的做法是“回调优先 轮询兜底”但如果项目初期回调还没联调好也可以先纯轮询。我的轮询实现长这样Scheduled(fixedDelay 15000) public void pollTaskStatus() { ListTaskRecord records taskMapper.findByStatus(PollingStatus.WAITING_POLL); if (records.isEmpty()) { return; } for (TaskRecord record : records) { try { TaskStatusQueryResponse rcsResp rcsClient.queryTaskStatus(record.getBatchId()); updateTaskStateMachine(record, rcsResp); } catch (Exception e) { log.error(轮询任务状态异常 batchId{}, record.getBatchId(), e); } } }状态机设计上我建议至少包含这几个状态SUBMITTED已下发未确认、PENDINGRCS已接收排队中、EXECUTINGAGV执行中、FINISHED完成、FAILED失败、CANCELED取消。本地业务表会有一列rcs_status专门存RCS返回的原始状态另有一列biz_status存业务侧最终表达的状态两者不要混。曾经有同事图省事直接用RCS状态当业务状态后来RCS版本一升级、状态值变了前端跟着崩。5. 实测中的典型故障与排查链路5.1 token并发刷新导致的偶发鉴权失败第一次写token管理时我用的是“取token前先查缓存没有就登录”的朴素逻辑。上线后出现一个诡异现象某个时间段任务下发接口偶发401看日志没有任何规律。排查链路先看RCS服务端日志发现同一秒内有多个登录请求查本地日志多个线程同时判断token过期同时调登录接口再查RCS机制确认后登录的请求会刷新服务端token之前登录返回的token立刻失效其他线程拿着已经失效的token去请求自然401。解决方案就是我前面写的volatile缓存 synchronized双检 提前5分钟刷新。改完之后再没出现过偶发401。5.2 站点名称当编码用AGV原地罚站的教训某个项目联调时现场AGV收到任务后一直不动RCS界面上任务状态是“分配车辆失败”。一开始怀疑车辆被占用后来发现传的fromStation是中文名“东门收货区”而RCS里的站点编码是STATION_DM01。排查逻辑很简单先去RCS后台查到站点管理里真正的唯一编码再去业务库里看站点表发现我们存的是展示名称。这是个很低级但非常常见的错误建议对接前先让项目组成员都明确凡是在接口里传的站点参数一律用编码不允许用名称最好在字段上就加注释或者传参前做一次编码合法性校验。5.3 轮询太猛RCS网关主动限流有段时间我们内部测试200个任务同时下发每秒查一次状态RCS接口响应开始出现HTTP 429和“too many requests”。RCS作为工厂级调度系统网关层的限流策略比大家想象得严格得多。排查后我们调整了三处轮询间隔从1秒调到15秒支持批量查询任务状态的接口尽量用批量不要一个任务一个任务地循环查上线回调推送之后轮询降级为兜底手段每30秒才查一次。这个教训让我意识到对接第三方系统时频率策略是在开发阶段就要放进设计文档里的不能等上线后再调。如果RCS文档里没有明确限制从保守的间隔开始加频比从激进开始被限流要舒服得多。5.4 下发成功但AGV不执行去RCS端看什么“接口返回200RCS也说任务创建成功但现场AGV一动不动”——这是群里聊得最多的一个问题。整理一下我在现场排查的标准顺序看任务状态RCS界面或查询接口里任务是PENDING还是EXECUTING如果一直PENDING通常是车辆分配阶段卡住了看车辆状态AGV是空闲、任务中、充电、故障还是急停有的场景AGV在充电点自动执行充电策略不响应任务优先级看站点可达性任务里的起始站和终点站是不是在同一张地图上AGV当前位置到目标站点的路径是否被障碍物或封闭区域阻断看任务参数任务类型和车型是否匹配比如这个车型不支持指定任务类型看优先级如果现场同时有其他高优任务在跑低优先级任务会被无限排队。我见过最大比例的原因是站点归属地图错误尤其改造项目里RCS里往往配置了多张地图代码里写死了一个站点编码但那个站点属于老地图新AGV根本不在那张图上跑。排查这类问题时不要纠结代码先跑现场、上RCS运维界面看效率最高。6. 性能优化与生产可观测性的一些升级做法6.1 异步化与消息队列削峰业务高峰期WMS可能同时产生上千条搬运需求。如果集成服务同步逐条调用RCS一旦RCS响应超过几百毫秒整个业务链路就会拖垮。我的做法是引入异步化业务系统只把任务落库然后发送一条MQ消息集成服务的消费端拉取消息批量调用RCS下发下发结果写回任务表供状态轮询统一处理。如果项目规模没那么大不引入MQ也可以用Spring的Async配一个独立线程池也能扛住大部分峰值。关键点是调用RCS的线程池和业务处理线程池要分离避免阻塞正常的业务请求线程。6.2 任务对账与失败补偿跟银行对账一个思路本地任务状态和RCS真实状态不可能天然一致超时、网络闪断、进程重启都会造成漂移。我设计了一个定时对账任务每30分钟扫描本地表中“未进入终态”的任务逐个向RCS查询真实状态本地显示已下发、RCS查不到说明可能因为某种原因没创建成功需要重新下发或人工介入本地显示下发中、RCS已执行完以RCS为准修正本地状态同时把成功事件推给WMS连续对账N次比如3次仍异常的任务自动标红并推送告警。对账任务还可以设置指数退避重试第一次等30秒失败后等1分钟、2分钟、4分钟最多重试5次。这套补偿机制救过我好几次尤其是半夜RCS短暂重启的场景有它兜底第二天早上打开工单列表根本不用人工补数据。6.3 监控指标与告警阈值对接RCS这类第三方调度系统最怕的不是出问题而是出了问题没人第一时间知道。我整理了核心监控指标指标说明建议告警阈值下发接口成功率RCS返回成功码的比例低于99%告警RCS平均响应时间任务下发接口的耗时超过2000ms告警轮询限流次数收到的429/限流码次数5分钟内大于10告警任务完成周期从下达到FINISHED的平均耗时超过30分钟告警本地积压任务数未进入终态的任务总量大于50告警token刷新失败数登录接口异常次数一票否则告警用Spring Boot Actuator Micrometer Prometheus Grafana基本零成本就能搭起来。在真正做工业项目时这个告警面板的价值远大于那些花哨的展示页面——AGV停在产线中央的时候早一分钟发现问题就是早一分钟止损。做这类集成最有价值的经验往往不在代码本身而在于你怎么把一个外部调度系统当作一个有脾气的在线服务来对待token要想着提前刷新任务要想着定期对账失败要想着重试补偿。把这几点做到位海康RCS的对接就成功了大半。如果你们团队正准备做类似的项目先把边界理清、接口摸底摸透再动手编码能少走很多弯路。