ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

泛微Ecology9接口对接实战:从Token认证到Java调用全流程解析

泛微Ecology9接口对接实战:从Token认证到Java调用全流程解析 做了这么多年企业内部系统集成泛微Ecology9是我用得最多的一套OA底座也是踩坑最多、积累最厚的一套东西。很多刚接触泛微的兄弟一上来就懵因为Ecology9的接口体系不像普通互联网应用那样一个Swagger文档搞定它分好几套入口、好几套认证方式而且不同版本的路径和参数还有差异。这篇博文我就把自己从后台配置到Java代码调用全流程摸通的经验完整拆一遍重点讲清楚接口怎么申请、Token怎么拿、业务流程怎么设计、Java代码怎么写才不出幺蛾子帮你少走我走过的弯路。如果你是系统集成工程师、Java开发或者企业信息化负责人这篇内容应该能直接帮上忙。1. 泛微Ecology9接口体系概览对接前先搞清楚有什么牌可打1.1 三种主流对接方式从JSON到XML的取舍泛微Ecology9这套产品我在集成时常用的对接方式主要有三类它们各自解决不同的问题也各有各的脾气。第一类是泛微自带的Restful API走/api/ec/dev/...这类路径返回JSON格式数据开发体验最好。以我接触过的多个E9版本来看Token获取走/api/ec/dev/auth/applytoken创建流程、查询流程这些接口则分散在流程引擎、内容管理等模块下。这个体系的优点是结构清晰前后端分离Java、Python、甚至Node.js都能轻松对接缺点是不同版本、不同补丁包打完之后接口路径可能会发生变化升级时容易踩坑。第二类是WorkflowXML WebService接口地址一般是http://{host}/services/WorkflowXML这是泛微老牌工作流接口通过SOAP协议传XML字符串来创建流程、提交审批、查询待办。这套接口非常稳定我见过不少企业用了七八年都没动过但它开发体验确实难受——你要手动拼XML、解析返回的SOAP XML调试起来比Restful费劲不少。不过如果你的核心需求是“发起流程”“审批流转”“查待办”WorkflowXML依然是很靠谱的选择。第三类是Ecode平台提供的前端集成能力可以在泛微页面里写自定义脚本也可以在后端写Java插件。严格来说它不算“外部接口调用”但对于需要在OA内部做数据处理、按钮扩展、页面增强的场景Ecode往往是最高效的路径。我个人的经验是外部第三方系统对接优先考虑Restful老系统稳定优先考虑WorkflowXML定制页面和内部逻辑增强优先考虑Ecode。1.2 场景决定技术路线你的需求该走哪条路接口选型不是越新越好也不是越熟越好关键看业务场景。我整理了一张选型对照表基本上覆盖了我做集成时遇到的大多数需求类型业务场景推荐接口主要原因第三方系统发起流程创建Restful / WorkflowXML两个都能做Restful开发快WorkflowXML稳定查询流程进度、审批日志WorkflowXMLdoGetWorkflowRequestLogs接口成熟字段全读取OA表单数据、文档内容RestfulJSON结构解析方便数据层级清晰待办待阅同步到企业微信/钉钉Restful高频调用Token机制更适合连接池复用页面按钮触发调用第三方系统Ecode不需要后端单独部署直接在OA页面逻辑里对接批量导入历史数据数据库接口 / Restful数据量大时直接走数据库更可控但要求熟悉表结构打个比方Restful像是一辆配置齐全的新车开着舒服但更新换代快WorkflowXML像是一台老款越野车外表糙但皮实耐用翻山越岭从不掉链子Ecode则像是原厂改装件只有在原厂体系里才能发挥最大价值。实际项目中我经常在一个集成方案里同时用到两三种接口比如用Restful获取外部系统推送的数据用WorkflowXML创建流程再用Ecode在审批结束后触发回调这套组合在真实企业环境里非常常见。2. 后台配置与Token机制接口调用的地基工程2.1 接口密钥申请不是随便填填就完事很多新手拿到泛微Ecology9地址就直接写代码调接口结果第一步就卡住了——返回401或者appid invalid。原因是泛微的接口必须有密钥才能调而这个密钥需要在OA后台申请。登录OA系统管理员账号之后入口一般在“集成中心”或者“客户化”模块下面的“接口密钥管理”不同版本的菜单名会有差异但里面的要素大同小异。进去之后选择新增密钥需要填应用名称、联系人、授权模块这几个关键信息。这里要注意授权模块决定了你能调哪些接口比如你只想做流程创建那就勾选工作流相关模块如果你勾选范围过大安全审核时容易被卡。如果只勾选了流程模块后面想查文档数据又会报权限不足所以申请前最好列一个接口清单按清单勾权限。提交申请之后系统会生成一对密钥一个是appid一个是secret有的版本还会额外生成一个appkey。这三个词的叫法在不同补丁版本里略有差别但逻辑上appid是应用标识secret相当于密码。我见过不少内部系统在代码里硬编码了secret这是非常危险的——secret一旦泄露别人就能以你这个应用的身份读取或操作系统数据生产环境的secret一定要放到配置中心或环境变量里不要写进代码仓库。提示申请密钥的时候尽量一个外部系统申请一个独立的应用不要所有系统共用一个appid。否则后面想单独回收某个第三方系统的权限时你会发现牵一发动全身。2.2 双Token机制拆解access_token和refresh_token的恩怨申请完密钥下一步就是通过密钥换取Token。泛微Ecology9的Token机制和主流开放平台类似采用access_token refresh_token双Token设计。调用业务接口时请求头或参数里带上access_tokenaccess_token过期后用refresh_token换取新的access_token避免用户重新走密钥认证流程。我截取一个典型的获取Token响应体{ code: 0, msg: success, data: { access_token: eyJhbGciOiJIUzI1NiIs..., refresh_token: dGhpcyBpcyByZWZyZXNo..., expires_in: 3600, refresh_expires_in: 604800 } }这里expires_in是access_token的有效期单位秒常见的是3600秒1小时refresh_expires_in是refresh_token的有效期常见的是7天。很多开发者以为只需要在过期后重新获取新Token就行但从安全设计上讲正确的做法是access_token快过期时用refresh_token去换而不是重新用appid和secret去申请。这样既减少了密钥的传输频率也方便后台做统一的生命周期管理和吊销控制。不过我在实际项目里遇到过一个坑有些泛微版本对refresh_token的调用频率有限制频繁刷新可能会被风控拦截。所以代码层面必须做好缓存和并发控制——多个线程同时发现token过期、同时去刷新就会产生雪崩效应。稍后第4章我会给出一个线程安全的TokenManager方案。2.3 签名算法与安全边界防人篡改的那点事泛微Ecology9在申请Token时很多版本要求携带签名参数token。这个签名的作用是防止请求参数在传输过程中被篡改。我见过比较常见的签名算法是将appid、secret、timestamp按固定顺序拼接后做MD5具体拼接顺序和加密方式每个补丁版本可能有差异但核心思想是一致的——用只有客户端和服务端知道的secret参与计算服务端收到请求后重新计算一遍两个签名一致才会放行。实际调试时我建议先在泛微后台找一下“接口文档”或者“开发示例”页面里面有现成的签名示例。如果后台没有文档可以抓一个OA前端页面发起的请求看看它怎么生成签名照葫芦画瓢。这个办法我在好几个版本上都用过百试百灵。关于安全边界还有一个容易被忽略的点很多生产环境的泛微OA直接对外网开放了业务接口风险很大。如果条件允许建议在防火墙层面对接口路径做白名单限制只允许固定IP段的服务器访问。如果泛微必须对公网开放至少要把/api/ec/dev/这几个认证相关路径加严格访问控制并且定期轮换密钥。3. 业务流程设计从ERP发起报销流程看调用闭环3.1 真实场景ERP审批通过后自动发起OA报销流程接口调用不能只看单个请求要站在业务流程闭环的高度去设计。我这里分享一个我真实做过的案例客户公司用ERP管理采购报销审批在ERP里完成后需要自动在泛微OA里发起一条报销流程流程走完后审批结果还要回传给ERP。这个场景里泛微其实是“被调用方”ERP是“调用方”。整个链条可以拆成四段ERP提交审批通过后触发一个消息事件ERP系统的集成服务收到事件组装泛微创建流程所需的参数调用泛微Restful接口或WorkflowXML接口创建一条报销流程并拿到requestId泛微流程审批结束后再通过接口或回调把结果返回给ERP。很多开发在做第3步时只关心“流程有没有创建成功”忽略了第1步和第4步的设计结果流程是建起来了但审批到哪一步了、有没有被退回ERP那边一概不知最后还是靠人肉查OA集成就失去了意义。3.2 调用链路拆解5个环节串起一次完整请求我把一次完整的第三方发起泛微流程的调用拆成5个环节每个环节都有自己必须注意的细节环节一准备Token。在真正发起流程创建前先确认本地缓存的access_token是否还有效。无效则用refresh_token刷新刷新失败再走密钥申请逻辑。环节二组装业务数据。这一步是把ERP系统的数据映射成泛微表单字段。常见的坑是字段类型不匹配比如ERP里的金额是字符串1234.5泛微表单的小数字段类型是decimal直接传会导致流程创建失败。环节三调用创建流程接口。通过HTTP客户端把JSON请求体发送到泛微接口。请求体里一般有三个核心部分工作流标识workflowId、流程标题requestName、表单数据formData。有的场景还需要指定流程发起人creatorId这个字段在接口升级后可能有不同的传法需要以实际文档为准。环节四解析响应拿requestId。创建成功后泛微会返回业务流水号requestId这是后续追踪流程的唯一标识必须保存到ERP的业务表里。环节五回调与状态同步。泛微流程审批结束后通过Ecode写回调或者定时轮询WorkflowXML的doGetWorkflowRequestLogs查询流程状态把结果返回ERP。轮询方案实现简单但存在延迟回调方案实时性好但需要处理网络异常、重试等问题。3.3 表单字段映射接口参数与表单控件的前世今生表单字段映射是整个流程设计里最容易出问题的地方。泛微Ecology9的表单分为标准字段和自定义字段标准字段比如申请人、申请日期、部门这些对应FormData里的固定key自定义字段则是你们OA管理员在建模引擎里建的控件字段名以field加编号之类的形式命名比如field12345、field67890。我见过不少新人直接拿表单控件的中文标签去做接口参数比如把“报销金额”作为key传进去结果泛微根本识别不了。正确做法是在泛微后台的建模引擎查看每个字段的实际物理名称或者用抓包工具抓一下前端实际提交的表单结构那个才是接口能识别的字段名。字段映射还有一个细节附件的处理。如果流程表单里需要带附件一般的做法是先把附件上传到泛微文档中心拿到docid再在创建流程时把docid放进表单字段。上传接口和文档中心的关联逻辑在不同版本差异较大建议先在测试环境验证一遍再把代码固化下来。4. Java实战解析手写一个可上生产的调用客户端4.1 工程依赖与工具类准备Java对接泛微Ecology9我建议直接使用OkHttp Fastjson这套组合。OkHttp连接池管理好性能稳定Fastjson序列化反序列化方便解析泛微返回的JSON非常顺手。如果你的项目对HTTP库有统一规范用Spring的RestTemplate或HttpClient也行核心逻辑都一样。Maven依赖如下dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.32/version /dependency dependency groupIdcommons-codec/groupId artifactIdcommons-codec/artifactId version1.15/version /dependency我还习惯封装一个最简HTTP工具类把POST JSON、GET请求这些常用方法收敛起来业务代码里就不用每个地方都写一遍OkHttp的Builder了public class OkHttpUtils { private static final OkHttpClient CLIENT new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build(); public static String postJson(String url, String json) throws IOException { RequestBody body RequestBody.create(json, MediaType.parse(application/json; charsetutf-8)); Request request new Request.Builder().url(url).post(body).build(); try (Response response CLIENT.newCall(request).execute()) { return response.body() ! null ? response.body().string() : ; } } public static String get(String url) throws IOException { Request request new Request.Builder().url(url).get().build(); try (Response response CLIENT.newCall(request).execute()) { return response.body() ! null ? response.body().string() : ; } } }超时时间不要设置太短泛微有些流程操作涉及大量数据组装响应超过10秒很正常。读超时30秒是比较稳妥的起始值。4.2 获取Token代码里的细节决定成败获取Token是实现调用闭环的第一步。先看一段我自己在多个E9版本上验证过的核心代码public class TokenManager { private static volatile TokenManager instance; private final String appid; private final String secret; private final String tokenUrl; private volatile String accessToken; private volatile String refreshToken; private volatile long expireAt; private TokenManager(String appid, String secret, String tokenUrl) { this.appid appid; this.secret secret; this.tokenUrl tokenUrl; } public static TokenManager getInstance() { if (instance null) { synchronized (TokenManager.class) { if (instance null) { instance new TokenManager(your-appid, your-secret, http://oa.example.com/api/ec/dev/auth/applytoken); } } } return instance; } public String getAccessToken() throws Exception { // 提前60秒过期避免token刚好在请求过程中失效 if (accessToken null || System.currentTimeMillis() expireAt - 60000) { synchronized (this) { if (accessToken null || System.currentTimeMillis() expireAt - 60000) { doRefreshToken(); } } } return accessToken; } private void doRefreshToken() throws Exception { long timestamp System.currentTimeMillis(); // 请注意不同版本的泛微签名串拼接顺序可能不同以官方接口文档为准 String sign DigestUtils.md5Hex(appid secret timestamp); MapString, Object params new HashMap(); params.put(appid, appid); params.put(secret, secret); params.put(timestamp, timestamp); params.put(token, sign); String json OkHttpUtils.postJson(tokenUrl, JSON.toJSONString(params)); JSONObject result JSON.parseObject(json); if (result.getIntValue(code) ! 0) { throw new RuntimeException(获取Token失败: json); } JSONObject data result.getJSONObject(data); this.accessToken data.getString(access_token); this.refreshToken data.getString(refresh_token); long expireIn data.getLongValue(expires_in); this.expireAt System.currentTimeMillis() expireIn * 1000L; } }这段代码我有几个设计想法特意说明一下第一用了双重检查锁保证并发安全多个线程同时调用getAccessToken()时不会重复刷新Token——这是生产环境最容易踩的并发坑不做并发控制的话高峰期瞬间几十个线程一起去刷新泛微后台会限流反过来又拖慢业务请求。第二提前60秒过期这是一个经验值。网络请求有耗时如果token刚好在发请求的瞬间过期服务端返回401你就得重试一次白白增加耗时。提前1分钟刷新能有效规避这个问题。第三刷新Token的方法和初次申请Token的方法我合成了同一个doRefreshToken()因为很多版本的泛微认证接口并不区分首次申请和刷新统一走applytoken即可。如果你的版本区分两个接口那就在getAccessToken()里先判断refreshToken是否为空再决定走哪个接口。4.3 创建流程实例从JSON拼装到响应解析拿到Token之后接下来的核心操作就是创建流程。我以比较容易理解的创建请求接口为例写一段典型的Java代码public class WorkflowApiClient { private static final String HOST http://oa.example.com; public String createWorkflow(int workflowId, String requestName, String creatorId, MapString, Object formData) throws Exception { String url HOST /api/ec/dev/workflow/paService/grampusworkflow/createWorkflow; String token TokenManager.getInstance().getAccessToken(); MapString, Object workflowBaseInfo new HashMap(); workflowBaseInfo.put(workflowid, workflowId); workflowBaseInfo.put(requestName, requestName); workflowBaseInfo.put(creatorId, creatorId); MapString, Object payload new HashMap(); payload.put(workflowBaseInfo, workflowBaseInfo); payload.put(formData, formData); String requestJson JSON.toJSONString(payload); System.out.println(请求参数: requestJson); String responseJson OkHttpUtils.postJsonWithToken(url, requestJson, token); JSONObject result JSON.parseObject(responseJson); if (result.getIntValue(code) ! 0) { throw new RuntimeException(创建流程失败: responseJson); } // 生产环境这里的requestId请存库后续查询状态要用 String requestId result.getJSONObject(data).getString(requestId); return requestId; } }这里有几个关键点值得展开。首先是请求体结构。workflowBaseInfo承载的是流程的基础信息formData承载的是表单数据。不同版本的formData字段结构可能有差异有的版本是Map直接映射字段有的版本要求一个FormField数组。我建议大家在开发时先抓一个前端实际发起流程的请求照抄它的结构成功率会直线上升。其次是creatorId的处理。有的场景下第三方系统代发起流程但流程发起人应该是某个具体的OA用户这时要传该用户在泛微里的用户ID而不是appid对应的系统用户。如果泛微版本不允许指定发起人就需要用系统管理员的身份来调用通过onBehalfUser之类的参数指定具体以你们版本文档为准。最后是响应解析。泛微返回的requestId是流程实例的唯一标识类似数据库的主键ID。我在代码里刻意把它单独提取出来返回就是为了让调用方在业务逻辑里可以明确感知到“这条流程已经属于某个业务主数据了”。后续ERP要用这个requestId去查询审批状态、做关联归档所以无论如何都要保存下来。4.4 查询流程状态与日志闭环不能只发不管流程创建成功以后业务闭环并没有结束还需要查询流程走到哪个节点了。最方便的方式是在ERP的定时任务里调用泛微的流程日志查询接口。我在老项目中通常用WorkflowXML的doGetWorkflowRequestLogs方法这个方法传入requestId返回该流程的所有审批日志包括各个节点的审批人、审批意见、审批时间、最终状态等。这个方法的返回是SOAP XML格式解析起来有一点繁琐我贴一个简化版的调用思路public String getWorkflowLogs(String requestId) throws Exception { String wsdlUrl HOST /services/WorkflowXML; ListString requestMessage new ArrayList(); requestMessage.add(RequestRequestId requestId /RequestId/Request); // 构造SOAP请求体具体命名空间以WSDL为准 String soapBody buildSoapBody(doGetWorkflowRequestLogs, requestMessage); String responseXml OkHttpUtils.postXml(wsdlUrl, soapBody); return parseSoapResult(responseXml); }SOAP接口拼XML比较丑但在企业环境里你绕不开它。我见过有人为了不用SOAP专门去找新版Restful流程查询接口这当然可以但如果你们环境是旧版本那WorkflowXML就是唯一选择了。在解析XML时我建议直接用DocumentHelper或XPath取节点不要用正则去抠效率低还容易出错。补充一句doGetWorkflowRequestLogs返回的日志里workflowRequestStatus字段比较关键它表示流程当前状态比如0是审批中、1是已完成、2是已作废或退回等具体枚举值不同版本有差异业务判断时尽量用常量映射而不是硬编码数字。4.5 封装一个EcologyApiClient单例、重试与线程安全当你的项目里不只一个地方需要调泛微接口时散落各处写HttpClient代码就会变得很难维护。我的建议是把所有和泛微相关的调用收敛到一个EcologyApiClient类里对外只暴露业务方法内部统一处理Token、重试、异常转换。一个典型的设计可以是public class EcologyApiClient { private final TokenManager tokenManager TokenManager.getInstance(); public String createExpenseWorkflow(ExpenseData data) throws Exception { MapString, Object formData new HashMap(); formData.put(field_expense_amount, data.getAmount()); formData.put(field_expense_reason, data.getReason()); formData.put(field_applicant, data.getApplicant()); // 业务方法内部统一走重试逻辑 return retryOnTokenExpired(() - createWorkflow( data.getWorkflowId(), data.getRequestName(), data.getCreatorId(), formData)); } private T T retryOnTokenExpired(CallableT action) throws Exception { try { return action.call(); } catch (RuntimeException e) { if (e.getMessage() ! null e.getMessage().contains(token)) { tokenManager.forceRefresh(); return action.call(); } throw e; } } }这个设计的核心价值有两点一是把业务数据和接口的映射逻辑集中在Client内部上层系统不需要关心workflowBaseInfo到底怎么组二是做了Token过期自动重试当接口返回token expired或401时先强制刷新Token再重试一次对上层业务透明。这个重试逻辑对网络抖动、令牌刚好过期这类的偶发问题很有效。有一点要提醒重试逻辑要控制次数一般重试1次就够了重试太多次反而会给泛微服务端造成压力尤其在接口性能比较差的环境里。5. 常见问题与排查技巧实录5.1 高频报错速查表一眼定位问题我把这些年对接泛微Ecology9时遇到的高频报错整理成了表格照着这个表排查大部分问题能快速定位报错现象可能原因解决思路401 UnauthorizedToken缺失、过期、或者请求头里没带检查Token缓存逻辑提前刷新确认请求头key拼写正确appid invalidappid填错或者该应用没有接口权限后台核对密钥确认授权模块签名校验失败签名串拼接顺序不对或timestamp误差过大严格按照接口文档生成签名检查服务器时间同步表单字段不存在formData里的key和表单物理字段名不一致到后台建模引擎查看字段物理名或抓包确认流程创建但无权限当前账号不具备该流程的发起权限确认creatorId是否对该流程可见可发起换个授权账号测试返回code非0但HTTP是200业务逻辑错误比如workflowId不存在输出完整响应体定位业务错误码信息连接超时泛微处理慢或网络隔离适当调大超时配置检查泛微服务器负载跨域报错浏览器调用泛微接口时CORS拦截改为后端调用不要在浏览器里直接调OA接口5.2 排查三板斧日志、抓包、接口文档遇到问题不要瞎猜排接口问题我有三板斧。第一板斧是看日志。我这里说的不是单纯看控制台而是看请求参数和响应体的完整日志。我自己习惯在代码里用日志记录每次调用的URL、请求JSON、响应JSON但要注意生产环境打日志时必须脱敏绝对不能把secret、token明文打出来。建议封装工具类对敏感字段做掩码处理。第二板斧是抓包。当泛微前端页面能正常操作但你的代码就是调不通时用Fiddler或Charles抓一下前端页面实际发出的HTTP请求看看它调的URL、请求头、请求体长什么样子和你的代码对比差异。这个方法多次帮我解决了版本差异问题比看接口文档快得多。注意用真机或测试环境抓包不要在公网入口抓。第三板斧还是接口文档。泛微Ecology9后台通常会有一个“接口文档”或“开发说明”页面里面能看到当前版本支持的接口列表、请求参数说明和Java示例代码。这个文档偶尔和实际行为有出入但多数情况下还是靠谱的。判断接口文档和实际行为不一致时以抓包结果为准。5.3 我踩过的三个坑长期困扰你的往往是小地方第一个坑是关于集群部署下的Token管理。客户OA是泛微集群环境负载均衡后面有多台E9节点Token认证逻辑在各个节点之间应该是共享的但如果你在代码里把Token缓存到本地内存集群刷新的时机又没控制好就会出现部分请求token失效、部分请求正常的现象。解决办法是在TokenManager里加分布式锁或者干脆把Token缓存到Redis确保集群环境下所有请求节点拿到的是同一份Token。第二个坑是回调地址的网络隔离。我在做流程审批结束后回调外部系统时刚开始怎么也调不通最后发现是泛微服务器到外部系统的网络端口没有放通。这个问题不是代码问题但很容易让人怀疑代码写错了。做集成联调之前先检查你的目标服务器和对方服务器的网络连通性能省下大量排查时间。第三个坑是金额字段的精度问题。ERP传过来的金额是BigDecimal类型序列化后的字符串可能是1234.50泛微表单里如果没有设置对应的小数位精度流程创建时会把多余的小数位截断导致报销金额对不上。解决思路是在组装formData之前统一按表单精度格式化金额字段保留两位小数。最后分享两个小经验我自己做下来最大的体会是泛微Ecology9接口本身并不算难难的是各种版本差异和环境问题。你在写代码之前一定要先花半天时间把当前环境属于哪个版本、有哪些接口文档、签名算法是什么、字段物理名是什么这些信息沉淀下来把地基打牢后面的开发会顺畅很多。另外一个小技巧是泛微接口联调时千万不要在生产环境直接试一定要在测试环境把完整流程跑通了再切生产。测试环境的初始化数据和生产不一定完全一致你可能会遇到表单配置、人员权限、流程分类这类只在生产环境存在的问题但至少能把代码层面的问题全部过滤掉。等你切换到生产环境后剩下的就是配置同步和环境参数调整风险小得多。
返回列表