做EBS集成的同学对采购订单审批肯定不陌生。标准功能里,审批人每天登录EBS工作台,从一堆待办通知里逐个点开、审批、写意见,流程虽然完整,但一旦公司上了OA、企业微信、钉钉或者供应商协同平台,业务方就会立刻找你:能不能让我们在OA里直接审批采购订单?能不能把待办自动推过来?审批完能不能把结果回传给ERP?这个时候,你需要开发和部署一套Oracle EBS采购订单审批和转发API,也就是常说的审批API。
这篇文章基于我最近完整落地的一个EBS采购订单移动审批集成项目,把审批API的需求拆解、技术选型、核心存储过程实现、中间件封装、测试排错思路完整梳理一遍。内容偏实战,适合正在被同样需求折磨的EBS开发、ERP集成顾问、项目经理参考,也适合想了解企业里“审批API到底是怎么通”的初学者。
1. 项目背景与需求拆解
1.1 为什么需要审批API
EBS采购订单审批,本质上不是一张表加一个状态字段那么简单。它背后是一整套工作流引擎在驱动:采购员创建订单,系统根据审批规则生成待办通知,审批人在通知上点击“批准”后,Workflow引擎自动判断是否还需要下一级审批,直到全部审批完成,订单状态才变成“已批准”。
原生模式下,所有审批动作都只能在EBS的审批工作台里操作。这意味着审批人必须能访问局域网,必须有EBS账号,必须记得每天去待办列表里翻一翻。很多企业里实际业务人员的习惯是“OA弹个窗我才知道有事情要做”,你让他在两个系统之间来回切换,审批时效一定起不来。
所以,企业真正需要的不是一个“审批按钮”,而是把审批动作能力开放成接口:外部业务系统可以查询当前用户的审批待办,可以执行同意、拒绝、退回、转发等操作,然后把结果同步回EBS。这就是审批API的存在意义。
1.2 核心需求清单
我在项目初期和业务方、IT方来回对了好几轮需求,最后确定下来的核心功能可以归纳成五块:
- 待办查询:根据工号查询当前登录用户在EBS中的采购订单待办列表,包括订单编号、供应商、金额、提交人、当前节点、发起时间。
- 审批处理:对待办执行同意(APPROVE)或拒绝(REJECT),必须能附带审批意见。
- 审批转发:原审批人因出差、离职、业务调整等原因,把待办转发给其他有权限的用户,并保留转发记录。
- 结果回写:审批动作完成后,将EBS侧的最新审批状态回传给调用方,方便OA刷新界面。
- 审计留痕:每一次API调用都记录操作人、目标订单、动作、结果、耗时,方便内控和追溯。
注意,这里还有一个很容易被忽略的需求:幂等性。外部系统因为网络超时可能会自动重发请求,同一笔审批如果被重复执行,要么报错,要么返回“已处理”结果,绝不能把一笔订单从“已批准”状态再往下推进一遍。
1.3 典型业务场景还原
某个制造企业(这里就叫A公司吧)的场景比较典型。A公司上了OA审批中心,希望采购订单审批在OA里完成。一笔采购订单提交后,走“项目负责人 → 部门经理 → 财务 → 总经理”四级审批。总经理出差,需要把待办转发给副总代审。同时财务在OA里看到审批关联的合同信息不完整,直接点了“拒绝”并填写意见,要求采购员修改后重新提交。
没有API的时候,这四步审批里只要有一级人不在电脑前,整个链条就卡着。有了转发API后,总经理在手机OA上看到待办,一键转发给副总,副总实时收到消息,直接审批通过。整个链路在半天内走完。这就是审批API最直接的价值。
2. 技术方案选型与架构设计
2.1 三条技术路线的对比
接EBS审批,业内常见做法大致分三类,我在这里用表格做个横向对比,方便不同体量项目的同学对号入座。
| 方案 | 实现方式 | 开发成本 | 安全性 | 灵活性 | 维护难度 |
|---|---|---|---|---|---|
| 方案A | 开放数据库包,外部系统直连数据库调用 | 低 | 低,账号密码直接落到外部系统,审计困难 | 低,外部系统需要直连数据库网络 | 中 |
| 方案B | 基于EBS自带的REST服务扩展 | 高,需要熟悉EBS的ADF/BC4J框架和定制发布流程 | 较高,走EBS应用服务器认证 | 中,受原生框架限制较多 | 高 |
| 方案C | 独立中间件 + 数据库存储过程包 | 中,需要开发一个轻量级Java服务 | 高,认证和审计可以统一管控 | 高,接口字段和协议完全自定义 | 中 |
我在这个项目里选的是方案C。理由很简单:EBS在绝大多数企业里属于核心交易系统,数据库连接不能直接暴露给外部应用;而EBS原生REST服务定制审批流程需要改CUST扩展点,开发和版本升级成本都偏高,团队里如果没有熟悉Oracle ADF的人,后续维护会很痛苦。
2.2 为什么采用“中间件 + 存储过程包”组合
先理解一下EBS审批的数据结构。审批人看到的一条待办,实际对应的是工作流引擎里的一个通知(Notification),存在于WF_NOTIFICATION表。审批动作的本质,是调用标准工作流API更新这个通知的状态,并触发引擎向下一个节点推进。
所以这里有一条红线:绝不能绕过工作流引擎直接改表。有些人图省事,直接UPDATE WF_NOTIFICATION把状态改成“已批准”,看起来待办消失了,但工作流活动实例的状态没变、审计信息缺失,后续整个审批链路的计数全乱,最后订单状态和工作流状态对不上,排查起来会非常痛苦。
方案C里,Java中间件只负责通信协议转换和业务编排,核心审批动作放在数据库包中完成。数据库包内部调用的是EBS标准工作流API,由工作流引擎自己去推进节点。这样的层级关系是:
外部系统(OA/钉钉/移动App) → 中间件API服务 → 数据库存储过程包 → EBS工作流引擎 → 采购订单审批数据
这样做的好处是:中间件可以统一处理认证、参数校验、接口文档、日志;数据库包可以保证审批逻辑不依赖中间件,即使以后中间件重构了,审批核心逻辑依然跑在数据库层,不会丢。
2.3 架构中的身份映射设计
EBS的审批人和外部系统用户默认不是一套账号体系。API接收到的是OA工号(比如10086),数据库里查的却是FND_USER里的USER_NAME(比如ZHANGSAN)。所以中间件必须维护一张映射表,或者调用方直接传EBS账号。
我当时做的是双模式:优先按工号查映射表,映射不到就直接按USER_NAME匹配。这种设计灵活性高,但要注意,映射表本身也要有维护界面,否则上线后人员变动全靠写SQL,迟早出问题。
3. 核心API的实现与实操步骤
3.1 接口契约设计
接口设计上不要贪多,我最终精简成三个核心接口。
| 接口 | 方法 | 路径 | 核心参数 |
|---|---|---|---|
| 待办查询 | GET | /api/po/notifications | approver(审批人工号), pageSize, pageNum |
| 审批处理 | POST | /api/po/approve | notificationId, action, comment, operator |
| 审批转发 | POST | /api/po/forward | notificationId, fromUser, toUser, comment, operator |
统一响应结构我习惯这样做:
{ "code": "200", "message": "success", "data": { "notificationId": 123456, "poHeaderId": 7890, "poNum": "PO-2024-00123", "status": "APPROVED" } }状态码不能只用HTTP状态码。我自定义了业务码:200正常,400参数错误,404待办不存在,409订单已被处理(幂等冲突),500系统异常。这样排查问题时,只看返回码就能定位大部分问题。
3.2 数据库包主体结构设计
我在EBS数据库层创建一个独立的Package,命名上加上公司标识防止和标准对象冲突,比如XXAPP_PO_APRV_PKG。包内拆分四个过程:
get_todo_list:查待办列表submit_approval:审批通过/拒绝forward_notification:转发待办post_process_audit:写审计日志
这里有一个很重要的设计原则:数据库包只接收解析后的业务参数,不负责外部协议解析和鉴权。鉴权在中间件完成,数据库包内部只做业务合法性校验。
3.3 审批主逻辑实现
审批是核心中的核心。网上很多例子里直接UPDATE状态,那是错误的。我的做法是先锁定通知行,再调用EBS标准API。
PROCEDURE submit_approval( p_notification_id IN NUMBER, p_action IN VARCHAR2, -- APPROVE / REJECT p_comment IN VARCHAR2, p_operator IN VARCHAR2, x_return_status OUT VARCHAR2 ) IS l_nid NUMBER; l_status VARCHAR2(30); BEGIN x_return_status := 'S'; -- 第一步:锁定当前通知,防止并发重复审批 SELECT notification_id INTO l_nid FROM wf_notifications WHERE notification_id = p_notification_id AND status = 'OPEN' FOR UPDATE NOWAIT; -- 第二步:调用EBS标准工作流API完成审批 fnd_wf_notification.set_approval_status( p_notification_id => l_nid, p_status => p_action, p_comment => p_comment, p_responder => p_operator ); COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; x_return_status := 'E'; -- 记录错误日志到自定义日志表 END submit_approval;这套逻辑里的关键点,第一个是FOR UPDATE NOWAIT。两套系统同时调用同一个接口时,这个锁能保证只有一笔审批能成功,另一笔要么等锁,要么直接报“数据被锁定”。第二个是调用标准API而不是直接改表,把动作交给EBS工作流引擎去处理,后续任务自动推进。第三个是事务处理,审批成功才提交,失败必须回滚,绝不能让外部系统拿到一个假成功。
实际操作里,参数p_responder在部分EBS版本中叫法略有不同,甚至某些功能顾问会建议同步更新接口人字段。你在自己环境联调时以标准API签名为准,核心思路是一致的。
3.4 转发逻辑实现
转发API的业务逻辑比审批稍微绕一点,因为它涉及审批责任转移。我实现时分成三步。
第一步,校验被转发人是否存在且在该PO的审批规则范围内。否则会出现把单子转给一个根本没有审批权限的人,待办发出去了却没人能处理的情况。
第二步,调用标准工作流API执行转发。
PROCEDURE forward_notification( p_notification_id IN NUMBER, p_from_user IN VARCHAR2, p_to_user IN VARCHAR2, p_comment IN VARCHAR2, x_return_status OUT VARCHAR2 ) IS BEGIN x_return_status := 'S'; -- 校验目标用户存在且有效 SELECT user_id INTO l_user_id FROM fnd_user WHERE user_name = p_to_user AND end_date IS NULL; -- 调用EBS标准转发API fnd_wf_notification.set_forwarder( p_notification_id => p_notification_id, p_forwarder => p_from_user, p_forwarded_to => p_to_user, p_forwarded_note => p_comment ); COMMIT; END forward_notification;第三步,写转发历史。标准API本身会记录部分日志,但为了业务追溯,我在自定义表里记录了转发前后处理人、转发时间、转发原因,方便后来审计。
这里有一个我在实施中踩过的坑:转发不是改字段那么简单。有些开发图省事,直接通过UPDATE把WF_NOTIFICATION里的收件人改了,原审批人列表里确实看不到待办了,但工作流内部的“通知用户”节点没有重新分配,被转发人也收不到新的待办,结果审批单就凭空消失了。所以转发必须走标准API。
3.5 中间件封装细节
数据库包实现后,我用Spring Boot做了一个轻量级中间件,核心工作只有四件事:认证、参数校验、调用数据库包、返回结果。
认证用的是JWT。OA系统审批时携带token,中间件校验token后从上下文里取出用户身份,再和请求体里的operator做比对,防止一名普通员工冒充总经理审批。这一步不能省。
调用数据库包的代码长这样:
@PostMapping("/api/po/approve") public ApiResponse approve(@RequestBody ApproveRequest request) { // 1. 鉴权 String operator = JwtUtil.parseToken(request.getToken()); if (!operator.equals(request.getOperator())) { return ApiResponse.error(403, "非法操作"); } // 2. 构造存储过程调用 String sql = "{call XXAPP_PO_APRV_PKG.submit_approval(?, ?, ?, ?, ?)}"; try (Connection conn = dataSource.getConnection(); CallableStatement cs = conn.prepareCall(sql)) { cs.setLong(1, request.getNotificationId()); cs.setString(2, request.getAction()); cs.setString(3, request.getComment()); cs.setString(4, request.getOperator()); cs.registerOutParameter(5, Types.VARCHAR); cs.execute(); String status = cs.getString(5); if ("E".equals(status)) { return ApiResponse.error(500, "EBS处理失败"); } return ApiResponse.ok(); } catch (SQLException e) { log.error("审批调用异常", e); return ApiResponse.error(500, "系统异常"); } }有个细节值得提一下:数据库连接必须和EBS业务库的字符集保持一致。EBS常用ZHS16GBK或AL32UTF8,如果中间件连接串里的字符集和数据库不一致,审批意见里的中文会出现乱码,这种问题从参数上看永远没问题,但一写库就坏,很隐蔽。
3.6 审计日志与幂等控制
审计日志我建了一张独立表,字段包括:接口调用ID、调用的系统标识、操作人工号、PO头ID、通知ID、动作类型、审批意见、结果状态、异常信息、开始时间、结束时间、耗时毫秒数。每笔调用都记录,不仅是内控需要,出问题排查时也全靠这张表。
幂等控制我设计得比较轻量:在审批逻辑执行前,先查询该通知的当前状态,如果不是OPEN,直接返回409“已处理”。加上数据库层对通知行的锁定,基本能保证并发和重试场景下不会二次审批。
4. 部署、测试与常见问题排查
4.1 环境准备与部署步骤
整个系统部署涉及三层环境:EBS数据库层、EBS应用层、中间件。
数据库层需要做的操作:以系统管理账号登录数据库,创建自定义Package,授权给中间件使用的数据库账号,执行授权脚本让中间件账号可以调用包。中间件账号不建议直接用APPS账号,而是单独建一个只拥有包执行权限的最小权限账号。
中间件层配置比较简单:准备一台Linux/Windows服务器,安装JDK 8+,部署Spring Boot的jar包,修改application.yml里的数据源指向EBS数据库,配置好JWT密钥。如果是走HTTPS,还要在中间件前面加一层Nginx,配置SSL证书和反向代理,并把请求体大小限制调大一些,防止审批意见太长被Nginx拦截。
4.2 测试方案设计
测试环节,我建议不要只测“通不通”,而是按照业务矩阵来覆盖。
| 用例编号 | 场景 | 操作 | 预期结果 |
|---|---|---|---|
| T01 | 正常审批 | 合法审批人提交APPROVE | 待办消失,流程推进 |
| T02 | 拒绝审批 | 合法审批人提交REJECT并填意见 | 流程退回,单据状态更新 |
| T03 | 转发给有效用户 | 原审批人转发给副手 | 新审批人看到待办,原审批人看不到 |
| T04 | 转发给无效用户 | 转发给已离职/锁定的用户 | 返回400,待办不丢失 |
| T05 | 重复审批 | 对已完成的待办再次提交APPROVE | 返回409,不产生二次推进 |
| T06 | 非本人审批 | A用户审批B用户的待办 | 返回403,拒绝处理 |
| T07 | 并发审批 | 两线程同时提交同一待办 | 一笔成功,一笔报“数据被锁定” |
| T08 | 中文审批意见 | 输入包含生僻字的长文本 | 数据库存储无乱码 |
这套矩阵跑下来,大部分常规问题都能暴露出来。
4.3 高频问题排查速查表
上线期间我整理了一个排查表,遇到问题直接对照,效率高很多。
| 症状 | 可能原因 | 解决思路 |
|---|---|---|
| 调用审批API报ORA-20001 | 自定义校验抛错,常见于订单状态不允许当前动作 | 查询订单当前审批状态,和调用方确认是否单据已变 |
| 提示待办不存在 | 传入的notificationId不属于当前审批人,或通知已被处理 | 联查WF_NOTIFICATION和审计日志确认当前状态 |
| 中文乱码 | 中间件连接串字符集与EBS不一致 | 在JDBC连接URL中显式指定字符集,并重启验证 |
| 审批成功但流程不往下走 | 工作流后台进程未及时推进 | 检查WF后台进程状态,必要时手动激活 |
| 调用方显示操作超时 | 中间件到数据库的网络抖动或锁等待 | 数据库包中尽量避免长事务,中间件设置合理超时和重试 |
| 字段值莫名被覆盖 | 多来源更新同一单据 | 禁止非标准API直接更新业务表,统一走包入口 |
4.4 一次线上排错实战记录
A公司上线第二周,OA反馈一个问题:审批人在OA里点了“同意”,界面提示成功,但采购员在EBS里看到订单还停在“审批中”,过了十几分钟才变成“已批准”。
我第一时间查了审计日志,发现API调用确实成功,数据库包返回正常。再查工作流后台进程,发现EBS的Workflow Background Process定时轮询间隔比较长,通知状态虽然改成完成态了,但引擎没有立刻被唤醒。
处理办法有两个。一是把工作流后台进程的轮询频率调短,这个需要EBS系统管理员配合评估对数据库性能的影响。二是更稳妥的做法——在审批包中调用标准API后,主动触发一次工作流推进动作,缩短等待窗口。我们最终在数据库包里补了一个唤醒动作,问题基本消失。
这个案例给我的教训是:API接口返回成功,不代表EBS端业务动作已经全部完成。工作流引擎有自己的异步机制,中间件侧的“成功”和业务侧的“完成”之间可能存在时间窗口。和OA联调时必须把这种异步特征讲清楚,否则OA那边会认为接口返回成功就该立刻看到结果。
5. 实操心得与扩展建议
5.1 最关键的三个坑
这套项目做完,我最想提醒后人的三件事:
第一,不要图省事绕过工作流引擎。审批状态和业务状态是两套体系,表面上改字段能解决眼前问题,后面流程计数和审计记录全对不上,排查成本远高于开发成本。所有审批动作必须通过标准工作流API。
第二,测试阶段一定要验证“离职用户”和“跨部门转发”这类边缘场景。EBS的FND_USER里很多账号是有有效期的,OA侧拿到的是一个已经失效的账号,转发时如果不做校验,接口会报错或者待办卡死。
第三,字符集和时区这种基础配置问题,越早确认越好。我见过太多项目联调几天后发现乱码和日期错位,最后定位到只是JDBC连接串少了一个参数。
5.2 性能与超时优化
采购订单审批有个特点:单笔调用非常轻量,但月底、年底会有集中审批的峰值。优化上我建议注意两点。
数据库包内尽量避免动态SQL,能一次查出来的不要拆成多次查询。审批待办查询接口要重点关注,如果使用方是OA首页那种频繁轮询的页面,建议在中间件层加缓存,5秒到10秒的缓存对业务影响基本为零。
中间件的HTTP连接池和数据库连接池参数也要匹配好。连接池太小,峰值时请求排队;连接池太大,数据库会话数爆掉。我当时调整到数据库单例空闲连接不超过20,事务提交后立即归还连接,这套配置在峰值时比较稳。
5.3 从审批API到流程自动化的扩展
审批API做出来后,很多延伸需求会自然浮出来。我这里简单提两个方向,后续项目可能会用到。
一个是审批超时自动提醒和升级。外部系统可以每天定时调用待办查询API,把超过24小时未审批的待办列表拉出来,推送给上级或启动催促流程。这个实现起来很简单,但业务价值很高。
另一个是审批意见的沉淀和分析。审批人拒绝订单时填写的意见,其实是很好的采购业务数据。通过API把意见存下来之后,可以按月统计拒绝原因分布,是价格问题、供应商问题还是需求描述不清,这对采购团队改进流程有实际帮助。
6. 写在最后的项目复盘
这套审批API上线运行到现在,稳定处理了几千笔采购订单审批,OA侧的审批人基本不再需要登录EBS工作台。我个人最深的体会是,这种集成类项目,技术实现本身不是最大的难点,最难的是把EBS的工作流机制讲清楚,让项目组里每一个人都理解“为什么不能直接改待办表”这件事。
如果你正在做类似的项目,我的建议是先花时间吃透EBS标准工作流的数据结构和处理逻辑,再动手写代码。接口契约、数据库包、中间件封装其实都是流水线工作,真正决定项目成败的,是那些藏在细节里的设计决策——锁、幂等、字符集、异步推进。把这些想清楚,这个项目就已经成功了一大半。