简介:面向需要对接泛微E9统一待办中心的开发人员,这份PDF文档完整阐述了基于WebService的待办集成方案。内容涵盖services.xml接口配置、receiveTodoRequestByMap方法定义、Map结构参数说明(如syscode、flowid、receivets等),并给出SOAP请求XML及返回值示例,可帮助异构系统快速接入统一待办中心,实现各业务系统待办任务的集中展示。资源为1个PDF文件,大小1.6MB,便于随时查阅。已有4130人学习下载,适合OA集成开发、系统对接及流程整合场景参考。
1. 泛微E9统一待办中心的集成价值与适用场景
企业上完泛微E9之后,待办事项往往还散落在SAP、HR、ERP和自研系统里,门户端的统一待办中心如果只展示OA内部流程,价值就砍掉了一大半。泛微E9给出的方案是一组WebService接口,让异构系统把待办数据主动推送进来,由E9统一聚合、统一跳转、统一办结。这里的关键认知是:统一待办中心的本质不是页面,而是接口,页面只是接口数据的消费者。这套接口文档的核心是OfsTodoDataWebService服务,提供了 Map、JSON 两种格式的接收入口,以及待办、已办、办结三态处理能力。真正值得深入研究的不是SOAP XML长什么样,而是syscode隔离、receivets防覆盖、operType状态机这三件事。适合人群:负责E9二次开发的集成工程师、企业门户拉通待办的项目实施人员,以及需要把SAP工单、HR审批接入统一待办的研发。
2. xfire 服务注册与五个核心接口的分工逻辑
2.1 services.xml 注册:三个不可改错的元素
泛微E9的WebService基于XFire框架,服务注册文件固定存放在/ecology/classbean/META-INF/xfire/services.xml。不要把这个文件理解成普通的Spring Bean配置,XFire启动时会扫描这个XML,动态生成WSDL并绑定HTTP路由。以文档给出的配置为例:
<service> <name>OfsTodoDataWebService</name> <namespace>webservices.ofs.weaver.com.cn</namespace> <serviceClass>weaver.ofs.webservices.OfsTodoDataWebService</serviceClass> <implementationClass>weaver.ofs.webservices.OfsTodoDataWebServiceImpl</implementationClass> </service>name决定了WSDL访问路径,配置完成后通过http://E9服务器IP:端口/services/OfsTodoDataWebService?wsdl即可看到服务定义;namespace必须与SOAP请求体里的xmlns保持一致,否则XFire会直接抛出命名空间不匹配异常;serviceClass是接口全限定名,implementationClass是业务实现类。
提示:如果修改了services.xml,需要重启Tomcat或对应的E9应用服务。XFire的配置加载发生在容器启动阶段,热更新在这个文件上不生效,生产环境改完务必确认服务注册日志。
2.2 五方法矩阵:按数据格式与业务动作划分
接口文档给出了五个核心方法,按传递格式分成 Map 和 JSON 两组,按业务动作分成接收、转已办、转办结、接收异构系统流程四类。把它们放到同一张表里看,分工逻辑会清晰很多:
| 方法名 | 数据格式 | 业务动作 | 典型使用场景 |
|---|---|---|---|
| receiveTodoRequestByMap | Map | 接收待办流程标准格式 | 异构系统推送新待办 |
| processDoneRequestByMap | Map | 处理待办流程变为已办 | 异构系统同步审批完成动作 |
| processOverRequestByMap | Map | 处理办结流程变为办结 | 异构系统同步流程终结动作 |
| receiveRequestInfoByMap | Map | 接收异构系统流程 | 异构系统全量推送,含已办/办结/已读状态 |
| receiveTodoRequestByJson | JSON | 接收待办流程JSON格式 | 轻量客户端、ajax后台拼接场景 |
这里需要区分processDoneRequestByMap和processOverRequestByMap:前者是把待办变成已办,流程还能继续流转,后者是流程彻底结束。如果异构系统的业务对象没有办结概念,只做挂起处理,就不要调用办结接口,否则在统一待办中心里流程会直接消失,用户点不到详情。
2.3 为什么XFire时代选择了Map而非实体对象
XFire是JAX-RPC时期的产物,对复杂对象序列化的支持远不如JAX-WS成熟。用Map<String, String>传递参数有三个现实原因:第一,所有异构语言生成的客户端都能把Map序列化成<entry>列表,不需要各自维护一套DTO类型定义;第二,新增参数不影响已有客户端,只要服务端 Map 里多取一个 key 即可,这给接口迭代留下了空间;第三,E9的服务端实现里对所有key做循环读取,代码可以写得非常统一。
当然Map也有明显代价:参数名拼写错误无法在编译期暴露,且所有值都被当成字符串处理,时间字段的格式要由双方约定。文档里的createdatetime、receivedatetime统一采用yyyy-MM-dd HH:mm:ss,如果某个系统传了带时区的ISO格式,E9侧解析会直接落库为空。
3. 手工构造SOAP请求:从curl验证到返回码语义
3.1 用curl直接触发接口,先绕过客户端代码
不少人拿到接口文档后第一件事是写Java客户端,但最稳妥的做法是先用手工SOAP请求验证服务连通性。以下是对应receiveTodoRequestByMap的最小请求体,注意SOAPAction在XFire中通常可以留空,服务端按请求体里的方法名路由:
curl -s -X POST http://192.168.1.100/services/OfsTodoDataWebService \ -H "Content-Type: text/xml; charset=utf-8" \ -d @receiveTodo.xmlreceiveTodo.xml的内容如下,in0是XFire对Map<String, String>参数的固定包装名,请求里的中文统一使用XML实体转义,文档中查看节点即“查看节点”的Unicode编码:
<?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soapenv:Body> <receiveTodoRequestByMap xmlns="webservices.ofs.weaver.com.cn"> <in0> <entry> <key xsi:type="xsd:string">syscode</key> <value xsi:type="xsd:string">0001</value> </entry> <entry> <key xsi:type="xsd:string">flowid</key> <value xsi:type="xsd:string">A00000000007</value> </entry> <entry> <key xsi:type="xsd:string">requestname</key> <value xsi:type="xsd:string">员工请假流程_0001_-1_A00000000007_luffy</value> </entry> <entry> <key xsi:type="xsd:string">workflowname</key> <value xsi:type="xsd:string">员工请假流程</value> </entry> <entry> <key xsi:type="xsd:string">nodename</key> <value xsi:type="xsd:string">查看节点</value> </entry> <entry> <key xsi:type="xsd:string">pcurl</key> <value xsi:type="xsd:string">/showtask.aspx?id=A00000000007</value> </entry> <entry> <key xsi:type="xsd:string">appurl</key> <value xsi:type="xsd:string">/showtask.aspx?id=A00000000007</value> </entry> <entry> <key xsi:type="xsd:string">creator</key> <value xsi:type="xsd:string">wld</value> </entry> <entry> <key xsi:type="xsd:string">createdatetime</key> <value xsi:type="xsd:string">2016-03-09 10:10:10</value> </entry> <entry> <key xsi:type="xsd:string">receiver</key> <value xsi:type="xsd:string">luffy</value> </entry> <entry> <key xsi:type="xsd:string">receivedatetime</key> <value xsi:type="xsd:string">2016-03-10 10:10:10</value> </entry> </in0> </receiveTodoRequestByMap> </soapenv:Body> </soapenv:Envelope>pcurl和appurl需要特别注意:文档示例里给的是相对路径/showtask.aspx?id=A00000000007,但生产环境的统一待办中心在跳转时,会根据E9的系统参数拼装完整域名。如果异构系统有自己的详情页,这里应该传完整URL,例如https://oa.example.com/thirdparty/todo/detail?id=xxx。还有receiver字段必须传E9里的登录名原值,不是显示名,也不能传工号,否则待办归属不到具体用户。
3.2 operType 与 operResult:一次调用的完整语义
响应解析不能只看成功失败,要把dataType、operType、operResult三个字段组合起来理解。文档给出的响应示例里operType=AutoNew表示服务端推断这是一条新数据并自动创建,operResult=0却表示执行失败,原因是message中提示“流程数据已存在”。
| 字段 | 取值 | 含义 |
|---|---|---|
| dataType | IsUse / OtherSys / WfType / WfData / SetParam | 数据归属类型,WfData表示流程数据 |
| operType | AutoNew / New / AutoEdit / Edit / Del / Check / Set | 服务端判定的操作类型,Auto开头是自动推断 |
| operResult | 1 / 0 | 1成功,0失败 |
| message | 文本 | 具体错误或成功信息 |
AutoNew和AutoEdit的区别值得展开:当一条 flowid 首次推入时,服务端自动走新建逻辑,返回 AutoNew;当相同的 flowid 再次推入,服务端对比已有数据后走更新逻辑,返回 AutoEdit。这也是文档示例里第二次调用返回AutoEdit且operResult=1的原因。在实际排错中,如果连续调用同一个 flowid 却一直返回 AutoNew,说明请求里的requestname或receiver发生了变化,导致唯一性匹配失败,数据被重复创建。
3.3 JSON 格式与泛微E9的ajax调用场景
receiveTodoRequestByJson是receiveTodoRequestByMap的JSON版本,适合前后端分离架构里由服务端拼装JSON直接推送,也适合在泛微E9的自定义接口里通过ajax方式调用后端逻辑。泛微ecology9的ajax调用后端接口通常走ajax.do网关或自建Action,外层系统可以把待办数据先写入中间表,再触发E9的Action调用这个JSON接口:
{ "syscode": "0001", "flowid": "A00000000008", "requestname": "采购审批_0001_-1_A00000000008_wangwu", "workflowname": "采购审批流程", "nodename": "部门经理审批", "pcurl": "/showtask.aspx?id=A00000000008", "appurl": "/showtask.aspx?id=A00000000008", "creator": "zhaoliu", "createdatetime": "2024-06-01 09:00:00", "receiver": "wangwu", "receivedatetime": "2024-06-01 09:30:00" }JSON格式不涉及XML转义,中文可以直接传输,但接口的Content-Type必须是application/json或text/json。这里埋着一个常见的坑:XFire对JSON格式的适配不是原生的,多数E9版本是通过额外的JSON插件或服务端代码里手动JSONObject.parseObject完成的,所以JSON的key顺序、字符串首尾空格都会影响解析结果。建议在调用方先做trim,并把flowid、requestname、receiver三个字段设为无法为空的强校验,任何异常都直接返回失败而不是静默丢弃。
3.4 receiveRequestInfoByMap 与 isremark 状态位
receiveRequestInfoByMap和前几个方法最大区别是多了isremark和viewtype两个状态位。isremark取值0、2、4分别代表待办、已办、办结,viewtype取值0、1代表未读、已读。这个接口适合异构系统做全量同步,比如SAP侧扫码后把单据状态推过来,用一次调用替代三次状态流转。
实际使用中有个容易混淆的点:isremark为4的办结状态和processOverRequestByMap的办结是重叠的,如果业务流程里已经用了processOverRequestByMap,再次通过receiveRequestInfoByMap推送isremark=4就会触发重复办结校验。文档中的响应示例operType=Check就是这种检测场景的返回值,说明服务端在判定数据是否允许转换,需要结合message看具体拦截原因。
4. receivets 时间戳机制:并发推送下的乱序覆盖防线
4.1 问题来源:两个线程先后调用同一流程
异构系统推送待办往往不是单线程行为。一个流程在SAP里审批通过的同时,E9的定时任务可能刚把旧的待办数据拉取进来;或者异构系统两个节点几乎同时向统一待办中心推送同一 flowid 的不同步骤数据。如果没有时间戳控制,后发请求的数据可能被先发的旧请求覆盖,用户在门户里看到的步骤永远是旧节点。
receivets字段就是用来解决这个问题的。文档说明里写得很明确:客户端使用线程调用接口时,根据此字段判断是否需要更新数据,防止后发的请求数据被之前的覆盖。不传时服务端根据当前系统时间自动生成。要注意的是,服务端不会无条件信任客户端传值,最终写入时会上抛给最终数据比较,所以这个字段必须保证在同一业务对象上是递增的。
4.2 服务端比对逻辑与时间戳判定的边界
服务端收到请求后,会拿请求里的receivets与库中已有数据的receivets做比较,新的时间戳大于旧的才允许更新,否则直接丢弃请求内容并返回操作失败或返回旧的 AutoEdit 成功。这个机制有三个边界条件需要理解到位。
同一条流程记录的判定维度是syscode + flowid + receiver + requestname,四个字段完全一致才认为是同一条数据,其中requestname参与唯一性计算是很容易被忽略的。文档示例里 requestname 的拼接规则是流程标题_syscode_(-1)_flowid_接收人,例如员工请假流程_0001_-1_A00000000007_luffy,如果异构系统修改了拼接格式,哪怕 flowid 相同也会被当成新数据,生成两条重复待办。
时间比较不是>=,而是严格的大于,相同时间戳的请求会被视作已处理,避免重复消费。这要求调用方在同一个流程实例上生成严格递增的时间戳,Python、Java 这类语言直接取毫秒即可,毫秒相同的情况加一个自增序列后缀更稳妥。
4.3 并发场景下的完整Java调用示例
以下代码展示如何在一个线程池里对同一个 flowid 发起三次请求,分别使用递增值模拟乱序场景。代码使用 JDK 原生的HttpURLConnection,不依赖CXF或Axis的stub,适合快速接入:
import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TodoPushClient { private static final String ENDPOINT = "http://192.168.1.100/services/OfsTodoDataWebService"; static String buildSoap(String flowid, String receiver, long receivets) { String requestName = "员工请假流程_0001_-1_" + flowid + "_" + receiver; return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"" + " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"" + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" + "<soapenv:Body>" + "<receiveTodoRequestByMap xmlns=\"webservices.ofs.weaver.com.cn\">" + "<in0>" + "<entry><key>syscode</key><value>0001</value></entry>" + "<entry><key>flowid</key><value>" + flowid + "</value></entry>" + "<entry><key>requestname</key><value>" + requestName + "</value></entry>" + "<entry><key>workflowname</key><value>员工请假流程</value></entry>" + "<entry><key>nodename</key><value>查看节点</value></entry>" + "<entry><key>pcurl</key><value>/showtask.aspx?id=" + flowid + "</value></entry>" + "<entry><key>appurl</key><value>/showtask.aspx?id=" + flowid + "</value></entry>" + "<entry><key>creator</key><value>wld</value></entry>" + "<entry><key>createdatetime</key><value>2024-06-01 10:00:00</value></entry>" + "<entry><key>receiver</key><value>" + receiver + "</value></entry>" + "<entry><key>receivedatetime</key><value>2024-06-01 10:30:00</value></entry>" + "<entry><key>receivets</key><value>" + receivets + "</value></entry>" + "</in0></receiveTodoRequestByMap></soapenv:Body></soapenv:Envelope>"; } static void post(String body) throws Exception { URL url = new URL(ENDPOINT); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "text/xml; charset=utf-8"); conn.setDoOutput(true); try (OutputStream os = conn.getOutputStream()) { os.write(body.getBytes(StandardCharsets.UTF_8)); } System.out.println("HTTP " + conn.getResponseCode()); conn.disconnect(); } public static void main(String[] args) throws Exception { String flowid = "A00000000007"; String receiver = "luffy"; ExecutorService pool = Executors.newFixedThreadPool(3); // 乱序推送,receivets 小的时间反而最后执行 pool.execute(() -> { try { Thread.sleep(200); post(buildSoap(flowid, receiver, 300L)); } catch (Exception e) { e.printStackTrace(); } }); pool.execute(() -> { try { Thread.sleep(50); post(buildSoap(flowid, receiver, 100L)); } catch (Exception e) { e.printStackTrace(); } }); pool.execute(() -> { try { Thread.sleep(0); post(buildSoap(flowid, receiver, 200L)); } catch (Exception e) { e.printStackTrace(); } }); pool.shutdown(); } }这段代码里receivets被显式传入,三个线程分别使用300、100、200的递增值并故意乱序执行。最终落库的节点信息取决于服务端对比后的最大值,也就是先执行完的请求不一定生效,receivets 为200的请求最后到达时,会覆盖掉中间执行的100请求,但无法覆盖最早的300请求。这演示了为什么要由调用方统一生成单调递增的时间戳,而不是用System.currentTimeMillis()直接并发生成,因为乱序执行时本地时间的先后不能代表业务数据的先后。
提示:如果业务上无法保证receivets单调递增,建议调用方增加一个同步锁或使用数据库序列生成时间戳,否则会出现“报文显示推送成功,但门户里仍是旧节点”的诡异故障。
4.4 requestname 拼接规则是重复数据的根源
文档示例中 requestname 的完整值员工请假流程_0001_-1_A00000000007_luffy已经揭示了它的构造逻辑:flowName + "_" + syscode + "_" + (-1) + "_" + flowid + "_" + receiver。这个拼接不是写死的,而是由集成方在推送前生成。拼接规则的稳定性直接决定了服务端能否正确识别同一条流程。
如果异构系统的流程标题会变化,比如“员工请假流程-张三”在审批中被修改为“员工请假流程-李四”,requestname 就会跟着变,服务端会把同一 flowid 识别成两条数据。解决方法是让 requestname 始终使用业务单据号,例如PUR-2024-0001_0001_-1_A00000000007_receiver,从源头保证拼接前缀不随标题改变。
5. 生产落地:泛微E9待办集成的坑位清单
5.1 常见报错与处理动作
集成过程中遇到最多的三类问题如下:
| 报错或现象 | 可能原因 | 处理动作 |
|---|---|---|
| 提示代码 -16 或 OA系统访问失败 | 未走E9的授权认证,或IP不在白名单 | 确认服务地址是否带 ecology 前缀,确认防火墙放行8080端口 |
| 返回 operResult=0,message 提示数据已存在 | requestname 或 receiver 与库中不一致 | 对比库中已有记录的requestname,检查拼接规则 |
| 返回 AutoNew 但门户看不到待办 | receiver 不是E9登录名,或流程类型未启用 | 用E9管理员账号在用户管理里查登录名原值 |
| 接口返回正常但页面跳转404 | pcurl 和 appurl 传的是相对路径且未配置域名 | 检查E9的系统参数中是否存在域名配置,否则改用绝对URL |
-16这类错误码在E9里经常出现在外部系统集成场景,本质是请求没有拿到合法的身份标识。E9的WebService默认不做登录态校验,但如果中间有nginx反代或安全网关拦截,就会出现访问失败。排查顺序是:先绕过网关用IP直连确认服务存活,再加Host头部测试,最后才查业务参数。
5.2 泛微e10迁移时的差异提醒
泛微E10在接口风格上做了明显演进,部分新项目使用RESTful API,不再依赖XFire的services.xml配置。如果你正在用E9的接口文档去对接E10环境,需要先确认对方提供的服务地址是services/OfsTodoDataWebService还是新版的/api/前缀。E10的待办集成更多走消息队列或OpenAPI,Map格式的SOAP推送在新版本里不一定保留。老集成迁移时,最稳妥的做法是保留E9环境作为待办汇聚层,E10侧通过OpenAPI把数据推到E9,而不是直接改报文格式。
5.3 最小验证清单与最终建议
完成联调后,建议按以下清单做一轮收尾验证:
- 推送一条新流程,确认响应
operType=AutoNew且operResult=1 - 修改节点名称后同flowid再推一次,确认返回
AutoEdit且门户展示新节点 - 用不同
receiver推同flowid,确认生成两条独立待办 - 连续推送相同请求,确认不会产生重复数据
- 调用
processDoneRequestByMap后,待办从待办列表消失且出现在已办列表 - 对比调用方日志和E9日志,确认
receivets最大值最终落库
日常维护中建议把 E9服务端OfsTodoDataWebServiceImpl的入参日志打开,XFire默认会记录SOAP报文,但实现类里新增字段时记得同步打印完整Map,方便核对异构系统实际推送的key名称。泛微E9统一待办中心的集成,核心不是把XML拼对,而是把receivets的单调性、requestname的稳定性和receiver的原值传递这三件事管住,待办数据就会稳定流转。
本文还有配套的精品资源,点击获取