先说个实际场景。公司上了泛微E9之后,OA里的审批流程越建越多,同时金蝶、CRM、人事系统各自都有待办,业务人员每天要切换五六个系统去看"我有哪些事没处理"。领导最后拍板:所有待办必须汇总到一个入口,统一展示、统一跳转、统一处理。于是"泛微E9统一待办接口"就成了整个集成方案的核心。
这篇文章我不打算写成官方文档的复读机,而是把我实际对接E9统一待办时踩过的坑、理清的接口逻辑、以及最终沉淀下来的调用方式完整讲一遍。适合正在做E9二次开发、OA与ERP集成、或者准备搭建企业统一待办门户的实施工程师和开发同学参考。文章里涉及的接口路径和参数,不同小版本会有些差异,但整体思路和数据模型在E9里是通用的,你拿到自己环境的接口文档后按这个框架去套,基本不会跑偏。
1. 先搞清楚:统一待办的“待办”到底存在哪里
很多第一次做E9集成的同学上来就找接口,结果绕了一大圈发现最关键的其实是理解E9的数据模型。待办不是凭空生成的,它是工作流引擎在流程流转过程中写到数据库里的状态记录。理解这一点,后面不管是调接口还是自己写SQL,心里都有底。
1.1 E9工作流的核心表与状态流转
E9的工作流引擎里,三张表是绕不开的:
workflow_requestbase:流程请求主表,一条记录代表一个流程实例。核心字段包括requestid(流程实例ID)、workflowid(流程模板ID)、requestname(流程标题)、creater(发起人ID)、createdate(发起日期)、status(流程状态)、currentnodeid(当前节点ID)、isfinish(是否已完成)。workflow_currentoperator:当前操作者表,这个是待办的直接来源。每条记录表示"某个节点上的某个人有一条待办",核心字段有userid(待办人ID)、requestid、workflowid、nodeid、isremark(是否待批)、isreject(是否驳回)、isover(是否已处理)、receivedate(接收时间)。workflow_requestlog:流程操作日志表,记录每一步的审批意见、操作人、操作时间,主要用于追溯和状态判断。
待办的产生逻辑其实很朴素:当流程流转到某个节点时,E9会向workflow_currentoperator表写入对应处理人的记录,这条记录的isover字段为0,表示还没处理,这个人登录OA后在"待办事项"里就能看到。处理完之后,isover被置为1,同时流程引擎会判断是进入下一节点、驳回还是结束,并再次写新的workflow_currentoperator记录。
所以"统一待办接口"本质上做的事情,就是从这些表里把isover=0的记录捞出来,再关联上流程标题、发起人、紧急程度等业务信息,组装成前端能直接展示的待办列表。
1.2 为什么不建议直接查库做对接
我之前见过不少团队图省事,绕开接口直接对数据库跑SQL查询待办。短期内确实能出数据,但很快会碰到几个硬问题。
第一是权限。E9的权限模型很细,同一个流程节点,不同分部、不同部门的人看到的数据范围不一样。直接查库你很难把"这个人只能看到他自己相关的那部分待办"这个规则写清楚,一旦写错就是越权,在OA系统里这是大忌。
第二是缓存。E9对流程数据有缓存机制,你直接查库拿到的数据可能是旧的。我就遇到过明明流程已经走完了,直连数据库查待办还能查出这条记录,但通过官方接口查就正常,因为接口会走E9的缓存刷新和状态过滤逻辑。
第三是状态判断的复杂性。一个"待办"不只是简单的isover=0,还要排除流程已撤销、流程已归档、当前节点已跳转等异常状态。这些边界逻辑官方接口都封装好了,自己写SQL很容易漏掉一两个条件,导致待办列表出现"幽灵数据"。
所以我的建议很明确:能调接口就调接口,接口满足不了再考虑自建查询,而且自建时必须把权限和状态过滤做到位。这一点后面第5节会展开讲。
2. 对接前必须确认的三件事:版本、鉴权、返回结构
E9的接口体系说复杂也复杂,说简单也简单。复杂在于不同版本、不同部署方式下接口地址会有变化;简单在于整体思路是固定的——先鉴权拿身份,再带身份调业务接口。我每次做新项目的对接,第一件事永远是先确认这三件事。
2.1 先确认E9版本和接口形态
E9从早期版本到现在迭代了很多次,接口的开放程度和路径都有变化。一般登录OA后台,在"系统信息"里能看到具体版本号。我建议你在对接之前先确认三件事:
- 版本号是9.0.几,因为部分接口在特定小版本才开放。
- 部署容器是Resin还是Tomcat,这会影响接口对外路径的上下文。
- 是否启用了独立的集成平台模块,E9的集成接口很多依赖
/api/前缀,而这个前缀在没开启集成模块时可能不生效。
E9对外提供接口的形态主要有四种:RESTful API、WebService、数据库视图(只读)、Java集成SDK。做统一待办,绝大多数情况下用的是RESTful API,这也是我这篇文章重点讲的形态。
2.2 鉴权方式:Token怎么拿
E9的统一待办接口不是裸奔的,调用前要先获取访问凭证。常见的做法是走E9的集成认证接口,用系统账号换一个token,后续的待办接口请求都带上这个token。
我以RESTful接口为例,典型的认证请求长这样:
POST /api/ec/dev/auth/applytoken Content-Type: application/json { "appid": "your_app_id", "appsecret": "your_app_secret" }正常情况下,接口会返回类似下面的结果:
{ "code": "0", "message": "success", "data": { "token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } }注意几个细节。第一,appid和appsecret一般需要OA管理员在集成平台里提前申请,不是你随便编的。第二,token是有有效期的,常见的是2小时,过期后要重新申请,所以调用方要做好token的缓存和自动刷新,不要每个请求都去申请一次。第三,有些环境里还会要求把token放在请求头的token字段而不是Authorization里,这个以你们环境的接口文档为准。
2.3 待办接口的通用返回结构
E9接口的返回结构整体比较统一,我调过的几个待办相关接口基本都是这个套路:
{ "code": "0", "message": "success", "data": { "list": [], "total": 0, "pageindex": 1, "pagesize": 20 } }code为"0"通常表示成功,其他值对应不同错误码;data.list才是待办数据的数组;total是符合条件的总条数,用于分页。分页参数一般叫pageindex和pagesize,有的是从0开始,有的是从1开始,这个很坑,我建议你在写代码之前先手动调一次接口,确认下标从几开始,否则翻页会漏数据。
时间格式也要注意。E9接口返回的时间有的版本是yyyy-MM-dd HH:mm:ss,有的版本是带T的ISO格式。如果你要做前端展示排序,最好在接口层统一转成标准格式,不要在前端每个页面单独处理。
3. 待办列表接口的请求构造与返回解析
确认完版本和鉴权方式,就可以正式开始调统一待办接口了。这一步我建议分三个阶段走:先手工把接口调通,再封装成统一的服务层,最后再接到前端页面。很多人一上来就写代码,结果参数错了排查半天,不如先用Postman或Apifox把接口调通,看清楚返回的字段再动手。
3.1 请求URL和请求头怎么构造
待办列表接口的路径一般是/api/workflow/pa/todolist或者类似的名字,不同的集成方式会有差异。我在项目实施中常用的请求示例(Java HttpClient)如下:
HttpClient client = HttpClient.newHttpClient(); String url = "http://your-oa-server/api/workflow/pa/todolist?pageindex=1&pagesize=20"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Content-Type", "application/json") .header("token", accessToken) .GET() .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());这里有个重要参数:查询哪个人的待办。大部分统一待办场景下,当前登录用户就是待办人,接口可以通过token自动识别,也可以在请求参数里显式传loginid或userid。如果你的统一门户是代用户查询,那必须传对应的loginid,而且后端要校验这个用户有没有权限查别人的待办,不然就是越权。
3.2 返回字段里哪些是真正用得上的
接口返回的list数组里,字段通常会比较多,但真正做统一待办展示时,核心字段就下面这几个:
| 字段 | 含义 | 使用场景 |
|---|---|---|
requestid | 流程实例ID | 拼接详情地址、调处理接口 |
workflowid | 流程模板ID | 判断流程类型、拼接详情地址 |
nodeid | 当前节点ID | 判断当前审批到哪一步 |
requestname | 流程标题 | 待办列表主标题展示 |
creater | 发起人姓名 | 展示谁发起的 |
createdate | 发起时间 | 列表排序、展示 |
urgentlevel | 紧急程度 | 高亮显示、排序 |
isremark | 是否待批 | 区分审批和知会 |
node name | 节点名称 | 展示当前在哪个人/哪个节点 |
这里特别提醒一下isremark这个字段。在E9里,isremark=1表示这条待办是"知会"性质,不需要真正审批,只是通知你看一下;isremark=0才是需要你操作的审批待办。做统一待办时,一定要在列表里把这两个类型分开展示,不然用户把知会当成审批去点,流程操作会出问题。
3.3 分页与性能:数据量大了怎么办
待办列表接口默认分页大小一般是20条,这个对于日常使用没问题,但如果做数据同步或者批量处理,就要拉全量数据。这种情况下我建议用"游标式"循环拉取,不要一次性把页码调得很大。
所谓游标式,就是每次只取一页,记录当前最大的requestid或者时间戳,下一页用这个值做条件去查。这样即使待办数量很多,也不会因为深翻页导致接口响应越来越慢。另外,在做定时同步任务时,尽量选择业务低峰期执行,并且控制并发数,不然会拖慢OA服务器。
4. 流程ID的获取:从列表到详情的钥匙
"泛微获取流程id"这个词在搜索热度里一直很高,因为它确实是整个待办跳转逻辑里最关键的一环。我在这里把流程ID相关的概念一次说清楚。
4.1 requestid和workflowid是两码事
很多初学者会混淆这两个ID,其实它们的区别很简单:
requestid是流程实例ID,每次发起一条流程就会生成一个新的requestid,它是唯一的,代表"这一条具体的申请"。workflowid是流程模板ID,同一个审批流程的所有实例共享同一个workflowid,代表"这是哪一种申请"。
举个例子,你公司有一个"请假申请"流程,模板ID是12345,员工A今天提交一条请假,生成的流程实例ID是100001;员工B明天也提交一条,流程实例ID是100002。这两条流程的workflowid都是12345,但requestid不同。
获取待办列表时,接口返回的数组里这两个字段都有,直接用就行。如果是要根据某个业务单据反查流程ID,那通常的做法是:在流程表单里加一个业务单号字段,然后通过这个字段关联查询workflow_requestbase表拿到requestid。
4.2 通过流程ID拼接详情页地址
拿到requestid之后,最常用的操作是拼接流程详情页地址,让用户点击待办直接跳转。E9的流程详情页地址格式大致如下:
http://your-oa-server/wui/index.html#/workflow/RequestView?requestid=100001&workflowid=12345注意这里的workflowid参数在部分版本里是可以省略的,但我建议还是带上,因为有些页面在缺少workflowid时无法正确渲染流程模板信息。跳转时如果是三方系统嵌入OA,建议用target=_blank方式打开新标签页,避免在iframe里出现登录态丢失的问题。
4.3 其他拿到流程ID的途径
除了待办接口,实际项目中还有两个场景经常需要获取流程ID:
一是从系统消息或邮件通知里带链接跳转。E9的工作流通知邮件里一般会带有requestid参数,从链接里解析出来就行。
二是数据库关联查询。比如外部系统只传了一个"单号",需要查出对应的requestid,常见SQL类似:
SELECT requestid, workflowid, requestname FROM workflow_requestbase WHERE requestname LIKE '%业务单号%' OR EXISTS (SELECT 1 FROM formtable_main_xx f WHERE f.requestid = workflow_requestbase.requestid AND f.billno = '业务单号')formtable_main_xx是指你表单对应的业务表,每张自定义表单在数据库里都有一张独立的表,编号xx和表单ID对应。这种查询方式适合数据核对,不建议作为高频接口调用方式。
5. 自建待办接口:什么时候需要,代码骨架怎么写
前面说了优先调官方接口,但现实中"官方接口不够用"的情况太常见了。比如你们要做多系统待办聚合,需要同时返回OA待办、金蝶待办、CRM待办,并且按紧急程度混排;又比如需要过滤掉某些特定流程的待办,只展示业务部门关心的部分。这时候就需要在E9里自建一个自定义接口,自己控制查询逻辑和返回结构。
5.1 自建接口前先想清楚三个问题
动手写代码之前,先问自己三个问题:
- 这个接口给谁用?只给统一门户用,还是也会被其他系统调用?这决定了要不要做独立的鉴权。
- 数据范围怎么定?是按照登录人自动过滤,还是允许传
loginid查别人?我强烈建议默认按登录人过滤,跨人查询必须加权限判断。 - 返回字段怎么定义?统一待办前端需要哪些字段,一次性设计好,不要等前端开发到一半再改。
这三个问题想清楚,代码写起来就很快,否则就是反复改接口的节奏。
5.2 一个可直接参考的Java接口骨架
在E9里自建接口,通常是在E9的Java工程里新增一个RestController,代码骨架大致如下:
@RestController @RequestMapping("/api/custom/todo") public class CustomTodoController { @Autowired private CustomTodoService todoService; /** * 统一待办查询接口 * @param loginid 查询目标用户的登录ID * @param pageIndex 页码 * @param pageSize 每页大小 */ @GetMapping("/list") public Map<String, Object> list(@RequestParam(required = false) String loginid, @RequestParam(defaultValue = "1") int pageIndex, @RequestParam(defaultValue = "20") int pageSize) { String currentLoginId = getCurrentLoginId(); // 如果传了loginid且不是当前登录人,必须做权限校验 if (StringUtils.isNotBlank(loginid) && !currentLoginId.equals(loginid)) { if (!hasPermissionToQueryOtherTodo(currentLoginId)) { return Result.error("无权限查询其他用户的待办"); } currentLoginId = loginid; } List<TodoItemVO> todos = todoService.queryTodoList(currentLoginId, pageIndex, pageSize); int total = todoService.countTodoList(currentLoginId); Map<String, Object> data = new HashMap<>(); data.put("list", todos); data.put("total", total); data.put("pageindex", pageIndex); data.put("pagesize", pageSize); return Result.success(data); } private String getCurrentLoginId() { // 从当前请求的token/session中解析登录人 // E9中有对应的工具类,实际开发时按自己工程的写法来 return RequestUtil.getLoginIdFromRequest(); } private boolean hasPermissionToQueryOtherTodo(String loginid) { // 这里做管理员/指定角色判断 return true; } }核心的查询逻辑在CustomTodoService.queryTodoList里。底层查询我推荐用workflow_currentoperator关联workflow_requestbase的方式,SQL的大致套路如下:
SELECT wr.requestid, wr.workflowid, wr.requestname, wr.creater, wr.createdate, wc.nodeid, wc.isremark, wb.workflowname FROM workflow_currentoperator wc INNER JOIN workflow_requestbase wr ON wc.requestid = wr.requestid LEFT JOIN workflow_base wb ON wr.workflowid = wb.workflowid WHERE wc.userid = ? AND wc.isover = 0 AND wr.status NOT IN (3, 4) AND wr.isfinish = 0 AND wc.isremark = 0 ORDER BY wr.createdate DESCwc.userid需要用当前登录人的ID,而不是登录名,所以要先通过hrmresource表根据loginid查出id。wr.status NOT IN (3, 4)是排除已撤销、已归档等无效状态,具体值在不同版本可能有差异,建议在测试环境先观察真实数据再定。wc.isremark = 0是只取待审批记录,如果需要包含知会,就把这个条件去掉,在前端分开展示。
5.3 自建接口最容易翻车的权限问题
自建接口最怕的就是越权。官方接口里的权限逻辑是黑盒,你自建接口以后,这道门就变成你自己守了。
我之前见有人图方便,在SQL里直接写死wc.userid = 1,接口只要有人调,返回的全是管理员账号的待办。后果就是普通员工在统一门户里看到了别人的审批单。这种问题在OA系统里属于严重事故级别,轻则被通报,重则会牵扯到流程数据泄露的责任。
所以我建议自建接口时加一道硬性校验:如果请求参数里的loginid和当前登录解析出来的loginid不一致,就必须判断调用者是否属于"待办查询管理员"角色,没有权限直接拒绝。这个逻辑不要在网关层做,要在接口层做,因为自建接口有可能绕开网关直接被内部系统调用。
6. 与外部业务系统打通:以泛微单点登录金蝶为例
统一待办做到后面,一定会遇到和外部系统打通的需求。热搜词里"泛微oa系统单点登录金蝶"出现频率很高,我就拿这个场景拆解一下。目标是:用户在泛微统一待办里看到金蝶的待办单据,点击之后免登录直接跳转到金蝶对应页面进行业务处理。
6.1 典型的集成链路
整体上分三步:
- 金蝶把它的待办数据推送给泛微(或者泛微定时拉取金蝶待办接口)。
- 泛微统一待办门户展示汇总后的待办列表。
- 用户点击某一条金蝶待办时,泛微拼接一个带登录票据的跳转地址,金蝶校验票据后自动登录并跳到对应单据页面。
核心难点在第1步和第3步。第3步的单点登录,很多金蝶版本支持通过ticket或token做免登。泛微侧生成一个临时票据,拼到金蝶的登录接口地址上,金蝶验证通过后建立会话并跳转。这里的票据必须设置有效期,比如5分钟,防止URL被别人拿走乱用。
6.2 待办状态同步的两种模式
外部系统的待办进入泛微统一待办后,最关键的问题是状态怎么同步:如果用户已经在金蝶里处理了这条待办,泛微这边的列表不能还显示未处理。
第一种是接口实时同步。用户点击跳转前,泛微调一次金蝶的接口确认单据状态;用户处理完回跳时,金蝶再通知泛微标记完成。这种模式体验最好,但对双方接口的健壮性要求高,任何一方接口抖动都会影响体验。
第二种是定时轮询。泛微每隔5分钟或10分钟拉一次金蝶的待办状态,批量更新本地数据。这种实现简单,但会有窗口期,用户可能处理完了,列表里还显示未处理,一般配合"已处理过一段时间后自动消失"的规则来缓解。
我做过的项目,如果金蝶那边有现成的待办查询接口和回调接口,我优先用实时同步;如果对方接口能力有限,就退而求其次用定时轮询,并在前端明确标注同步时间,避免用户误以为数据是实时的而产生投诉。
6.3 外部系统集成时的一个隐蔽坑:编码与账号映射
外部系统集成时,最容易出问题的不是接口而是账号映射。金蝶里的用户编码和泛微的loginid往往不一致,比如泛微里是zhangsan,金蝶里是00321。这个映射关系如果没建好,待办数据推过来后根本没法匹配到正确的待办人。
我的做法是单独建一张账号映射表,字段包括泛微loginid、金蝶用户编码、用户姓名、状态。在待办同步任务启动前,先把映射表核对清楚。不要试图在代码里硬编码这种映射关系,后续人员变动会让你改代码改到怀疑人生。
7. 移动端适配与高频问题排查
统一待办接口开发完,一定会遇到移动端的适配问题。E9自带的移动端和PC端在待办展示上体验还算接近,但如果是你们自己开发的小程序或者App,就会碰到一些接口层的小坑,我在这里集中说一下。
7.1 移动端待办展示的几个差异点
首先是字段长度。流程标题在PC端宽度充足,但在手机上最好截断,建议接口里直接返回一个shorttitle字段,控制在20个字符以内,避免前端每个页面都写截断逻辑。
其次是跳转方式。PC端通过浏览器地址跳转详情页没问题,但移动端可能是H5环境或者原生WebView,跳转前要确认有没有对应的移动端详情页路由。有些版本的移动端详情页路径和PC端完全不同,强行用PC端地址在手机里打开,体验很差。
最后是附件处理。移动端打开待办详情时经常要看附件,附件地址如果是content://这类本地协议,在外面集成时是不能用的,要把附件下载后转成可访问的HTTP地址,或者通过泛微自带的移动端附件预览接口处理。这个点很容易被忽略,上线前一定要拿真机实测一遍。
7.2 高频问题排查清单
我把做对接时最常遇到的问题整理成一个清单,按这个顺序查,基本能覆盖90%的情况:
| 现象 | 排查方向 |
|---|---|
| 接口返回401或token无效 | 先看token是否过期,再确认appid和appsecret配置是否正确 |
| 待办列表为空 | 确认查询的userid对应的用户在当前测试流程节点上是否真的有待办,流程是否已归档 |
| 有重复待办 | 检查是否同时走了缓存查询和数据库查询,或者是否在workflow_currentoperator中同一节点生成了多条记录 |
| 已处理的待办仍然显示 | 确认isover字段是否真的更新为1,是否走的是自定义SQL查询漏了过滤条件 |
| 接口响应慢 | 确认是否在业务高峰期拉全量数据,是否分页参数设置过大,建议加索引或用官方接口 |
| 中文乱码 | 统一请求和响应的编码为UTF-8,注意Postman测试时不会暴露,但Java调用时会在Header处丢编码参数 |
最后一个乱码问题特别容易栽跟头。我在E9上对接统一待办时,用Postman调接口一切正常,换成Java代码调用后返回的中文全是乱码。排查了半天,发现是Java HttpClient在POST请求的时候没有显式指定Content-Type里的charset=UTF-8,服务端按默认编码解析请求体导致的。这个问题在POST接口里尤其明显,GET接口因为参数在URL上,反而不太会遇到。
做完这些排查,统一待办接口基本就稳定了。最后说一句我个人做集成的体会:接口对接本身不难,难点永远在数据和权限的边界上。你花一小时把接口调通,可能就要花一整天把各种极端情况处理干净——比如转办、加签、代理审批这些流程特性,在统一待办里都要有对应的展示和处理逻辑。先把这些业务规则梳理清楚,再动手写代码,比什么技巧都管用。