ARTICLE DETAIL

资讯详情

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

SAP RAP集成BTP Workflow实现采购申请审批的完整实践指南

SAP RAP集成BTP Workflow实现采购申请审批的完整实践指南 最近在帮客户做S/4HANA项目里的采购申请审批需求很典型用户在Fiori里录入采购申请保存后自动发起审批审批人通过或拒绝后状态回到申请单。表面上看是保存后发个工作流这么简单真正动手才发现在RAP模型里集成SAP BTP WorkflowCPWF这件事从BTP侧配置、RAP BO建模、触发时机设计到审批结果回写每一步都有不少讲究。这篇实践总结会以采购申请审批为案例完整梳理RAP与CPWF集成开发的思路、代码和排错经验给正在做RAP全栈开发、需要集成云工作流的朋友做个参考。1. 先把场景讲清楚RAP里的审批需求到底长什么样1.1 我踩过的第一个坑直接在保存逻辑里调API需求提出的时候客户的原话是单据保存后自动发起审批。大多数ABAP开发者的第一反应都是去RAP的behavior implementation里找个方法比如在create的determination里直接调用HTTP创建CPWF的workflow instance。我第一版也是这么干的。问题很快暴露出来单据在Fiori界面上保存用户点击保存按钮RAP会走完整个transaction sequence包括finalize、modify、save、cleanup。我把HTTP调用放在determination里等于把一次外部系统的远程调用包进了主事务里结果就是——外部工作流实例创建成功了但ABAP侧因为某个校验在后续阶段失败整个BO保存回滚。用户看到的是保存失败的红叉但工作流审批任务已经躺在审批人的待办列表里了。这是典型的分布式事务陷阱。CPWF的API调用不是ABAP数据库操作它不会跟着回滚。你在业务事务里调外部API就要接受外部已发生但本地未保存这种不一致窗口。更麻烦的是如果用户不明所以重新保存一次工作流实例就会重复创建同一张单据出现两条审批任务客户会以为系统抽风了。所以第一版方案在测试阶段就被我叫停了。核心问题不是要不要调API而是在哪里调、怎么控制幂等、怎么保证状态一致。这也是我后来把方案从自动触发改成显式action触发状态机管理的起因。1.2 方案选型为什么选CPWF而不是经典工作流需求里的工作流引擎选型当时有两条技术路线。一条是传统的SAP Workflow用SWF类型的任务配置和WF_BATCH后台执行另一条就是SAP BTP上的Workflow服务也就是常说的CPWF。我做了一个简单的对比对比维度经典SAP WorkflowCPWFBTP Workflow运行环境本地S/4HANA或Suite系统BTP云环境独立于业务系统建模方式ABAP Workbench里的SWDD开发人员主导可视化流程编辑器业务人员可参与扩展性受限于本地系统资源和连接云原生弹性伸缩跨系统集成方便与RAP集成需要额外RFC/BAPI封装比较生硬基于HTTP/RESTRAP可直接调用审批任务体验依赖SAP GUI或Fiori旧版任务收件箱BTP Workflow的收件箱响应式UI从RAP全栈开发的视角看CPWF是更自然的选择。RAP本身倡导的就是云优先、API优先服务通过Service Binding暴露成OData。CPWF也完全走REST API两边在集成哲学上是一致的。而且CPWF的可视化建模器允许业务分析师自己调整审批链路不用每次改流程都找开发改代码。不过选型也不是只看技术上的优雅。CPWF作为BTP服务意味着有额外的订阅成本和网络通信开销。对本地部署的S/4HANA客户如果只是想做个简单的两级审批传统SAP Workflow可能更省事。但当业务要求多级审批、动态会签、跨系统通知时CPWF的灵活性和独立部署优势就体现出来了。我们选CPWF主要是考虑到客户后续还要接入移动审批和企业微信通知不希望把流程逻辑绑死在业务系统里。2. BTP侧配置与通信基础这一步做错后面全是泪2.1 创建Workflow实例并准备服务密钥CPWF作为BTP上的托管服务第一步就是在BTP Cockpit里把它激活。路径一般是Your Global Account → Your Subaccount → Services → Service Marketplace搜索Workflow创建一个workflow实例。实例创建时的plan建议选standard这是功能最完整的版本。创建完成之后进入实例详情页面创建一个服务密钥Service Key。服务密钥里最重要的三样东西是endpointAPI的base URL、client ID、client secret。这三样是后续所有ABAP代码访问CPWF的通行证。实际操作里很多人会忽略一个细节Workflow服务需要在BTP子账户里先完成授权Entitlement分配。如果你在Service Marketplace里搜不到Workflow或者创建实例时提示没有可用plan先去Entitlements页面给子账户分配Workflow服务的standard计划。这个步骤不做后面全是卡在第一步。2.2 用Communication Arrangement打通ABAP到BTP Workflow的信任服务密钥准备好了接下来是ABAP侧如何访问CPWF的问题。在BTP ABAP Environment或者嵌入了RAP开发环境的S/4HANA Cloud上标准做法不是直接在代码里写死URL和账号密码而是通过Communication Arrangement这套机制来管理通信信任。具体分三步走。第一步创建一个Communication Scenario。在ABAP开发环境里用事务码COMM_SCN或者直接在ADT里创建Communication Scenario填上场景名称、场景ID然后添加入站/出站服务。这里我们要配置的是出站HTTP服务目的地是CPWF的REST API。第二步创建Communication System。这里配置的是对端系统的信息包括系统ID、主机名等。关键的是在出站用户Outbound Users里配置一个用户这个用户就是用来做OAuth客户端认证的。把服务密钥里的client ID和client secret填进来认证方法选OAuth 2.0 Client Credentials。第三步创建Communication Arrangement。把Scenario和System关联起来系统会自动生成一个Arrangement ID。这个ID会在代码里作为Destination名称使用非常重要建议用有业务含义的名字比如CPWF_APPROVAL。有些朋友可能习惯直接创建Destination在BTP Cockpit的Connectivity → Destinations里然后在ABAP代码里用cl_http_destination_providercreate_by_url直接拿destination。这样也能通但不太符合SAP云环境的推荐做法。Communication Arrangement的好处是——它是SAP云平台统一的service consumption机制权限管理、密钥轮换、审计都有规范路径后续运维不会变成一团乱麻。代码侧标准的取destination方式是DATA(lo_destination) cl_http_destination_providercreate_by_comm_arrangement( comm_scenario ZCPWF_SCN service_id ZCPWF_OUTBOUND comm_system_id CPWF_SYSTEM ).这里service_id是在Communication Scenario里定义的出站服务的ID。拿到destination之后就可以用cl_web_http_client_manager来创建HTTP客户端了。2.3 验证通信链路先跑通一个GET再说在开始写RAP行为代码之前我强烈建议先做一个最小化的通信验证。在ABAP里写个简单的report调用CPWF的一个只读API比如查询工作流定义列表。这一步能在开发早期把90%的通信配置问题暴露出来而不是等到整个RAP代码写完了再去调试。验证代码的核心逻辑是DATA(lo_client) cl_web_http_client_managercreate_by_http_destination( io_destination lo_destination ). DATA(lo_request) lo_client-get_http_request( ). lo_request-set_uri_path( /v1/definitions ). DATA(lo_response) lo_client-execute( if_web_http_clientget ).如果HTTP response code是200返回了工作流定义列表的JSON说明Communication Arrangement、OAuth认证、网络链路全部通畅。如果返回401或者403直接去检查Communication Arrangement里的出站用户认证配置以及服务密钥是否还有效。这一步排查成本远比在RAP代码里打半天断点低。3. RAP BO建模把审批状态设计成BO的一等公民3.1 数据结构与行为定义参考RAP里的工作流集成不能只想着发请求这一个动作审批状态、工作流实例ID、错误信息这些都是数据都必须建模到BO里。我设计的采购申请主表包含这样几个核心字段字段类型用途request_idCHAR(10)采购申请唯一编号BO的根实体主键statusCHAR(2)审批状态初始/待审批/通过/拒绝/失败workflow_instance_idCHAR(36)CPWF创建后返回的全局唯一实例IDerror_messageCHAR(200)触发工作流失败时的错误信息approverCHAR(40)审批通过/拒绝时的审批人approval_timeTIMESTAMP审批完成时间Behavior Definition的核心部分长这样define behavior for ZRAP_REQ_H alias ReqHeader persistent table zrap_req_h lock master authorization master( instance ) { field ( numbering : managed, readonly : after ) request_id; field ( readonly : update ) status, create_by, create_time; action ( features : instance ) submitForApproval; action ( features : instance ) approve; action ( features : instance ) reject; action ( features : instance ) resetAfterFailure; field ( readonly : update ) workflow_instance_id, error_message, approver, approval_time; mapping for zrap_req_h { RequestId request_id; Status status; WorkflowInstanceId workflow_instance_id; ErrorMessage error_message; Approver approver; ApprovalTime approval_time; } }代码里的field ( readonly : update )这一段值得说道说道。工作流实例ID和错误信息设计成update时只读意思是在创建采购申请时由后端填充而不是让前端用户胡乱传值。这样保证了这些字段只能通过后端行为逻辑来维护数据安全性和一致性都有保障。在真正的生产项目中我建议给你的BO开启draft功能。因为审批状态这类关键信息我们不希望用户在界面上输入一半就落库了。开启draft表后前端编辑操作都发生在draft数据上只有点击保存触发最终写入时active版本才更新。工作流的发起也应该基于active数据去做避免把半成品数据推给审批人看。3.2 用action而不是自动触发把控制权握在自己手里设计触发方式的时候我考虑过两个方向一是通过determination自动触发即BO实例创建成功后RAP框架自动调用一个确定逻辑二是通过显式action触发用户在界面上点击提交审批按钮。我最后选择了显式action。原因有几个。自动触发的不可控性在前面已经说过了——主事务未提交前你不确定后续会不会失败就无法安全地调用外部API。此外自动触发很难处理暂存和提交两个业务动作的区分。用户把采购申请保存为草稿这时候不应该发起审批。只有点击提交审批这个按钮才是业务上真正把单据推入审批环节的那一刻。显式action的最大好处是触发时机完全由你控制。action的执行意味着用户明确的业务意图这时候再去创建CPWF工作流实例语义上就非常顺用户提交 → 创建审批实例 → 状态流转到待审批。同时action配合features可以做到按钮级控制。比如action ( features : instance ) submitForApproval;在behavior implementation里通过get_features方法实现METHOD get_features. READ ENTITIES OF zrap_req_h ENTITY ReqHeader FIELDS ( status ) WITH VALUE #( ( %key-request_id request_id ) ) RESULT DATA(ls_req). IF ls_req-status INITIAL. result-submitForApproval if_abap_behvfc-o-enabled. ELSE. result-submitForApproval if_abap_behvfc-o-disabled. ENDIF. ENDMETHOD.这样界面上提交审批按钮只有在初始状态时才可用审批中、已通过、已拒绝的状态下按钮都是置灰的。这比在UI层硬编码按钮显隐要稳健得多因为它是后端层面的硬控制不管谁用什么客户端调用都无法绕过。3.3 为什么我把错误处理也设计成一个action实践中我发现审批流程偶尔会触发失败。原因可能是CPWF服务暂时不可用、网络抖动、或者认证token刚好过期。如果触发失败了BO状态需要有一个落点——不能卡在待审批状态然后就没人管了。我设计了一个resetAfterFailureaction作用是让单据从失败状态回到初始状态用户可以重新提交审批。这个设计看似简单但对整个方案的优雅贡献很大。它让系统有了自愈的路径而不是一个错误状态摆在那里用户只能在后台改数据库这在云环境里几乎不可能。后面会讲到配合失败信息的存储这个action可以让用户自助解决问题极大减少运维介入。4. 优雅触发工作流的完整实现从EML到HTTP再到状态机4.1 在save sequence里发送请求让副作用安全落地前面批评了在determination里调HTTP的做法那正确的写法是什么我的经验是把HTTP调用放到RAP行为实现的action方法里但必须遵循一个原则——所有BO字段的修改通过EML语句完成让RAP框架管理持久化外部API调用放在EML修改之后作为确认动作。简单说先本地落库再外部通知。以submitForApproval为例核心流程分三步。第一步读取事务数据。通过EML读取当前BO实例的明细数据拼装工作流上下文需要的字段比如采购申请号、申请人、金额等。第二步通过EML调用一个定制的行为方法把BO状态从初始状态改成处理中。这一步会真正写入数据库并触发RAP的持久化。只要这一步成功就说明单据已经安全地进入了审批中状态。MODIFY ENTITIES OF zrap_req_h ENTITY ReqHeader UPDATE SET FIELDS WITH VALUE #( ( request_id lv_request_id status PROCESSING ) ) FAILED DATA(ls_failed) REPORTED DATA(ls_reported).第三步调用CPWF API创建工作流实例。调用成功就把workflow_instance_id和status更新到BO调用失败就把status改回失败状态并记录错误信息。这个顺序保证了业务单据的状态是可靠落库的外部系统的调用是单据进入审批中的通知而非前提。万一外部调用失败单据并非处于半空状态而是明确标记为审批发起失败用户可以查看错误信息并重新提交。这才是RAP事务边界下处理外部副作用的安全模式。4.2 幂等性设计同一请求不能开两次审批幂等是我在RAP工作流集成里最强调的一点。CPWF的POST /v1/workflow-instances接口每次调用都会创建一个新的工作流实例它天然不是幂等的。如果你的UI因为网络超时而让用户点了两次提交审批没有幂等控制的话审批人就会看到两张一模一样的审批任务。我的做法是借助BO状态机做应用层幂等。submitForApprovalaction开头强制校验当前状态必须是初始状态如果不是直接返回业务错误消息给前端。这样即使在极短的时间内收到两次请求第二次也会因为状态已变成处理中而被拒绝。这里不能只依赖UI上的按钮置灰。按钮置灰只是用户体验层面的优化后端action的状态校验才是真正的保护层。因为我们无法保证所有调用方都走Fiori界面——如果企业有API集成平台、有移动端自定义客户端它们发的请求不一定遵守前端按钮状态。此外还有一个细节CPWF API调用本身如果失败要区分是请求已到达服务器但响应超时还是请求根本没发出去。前者意味着工作流实例可能已经创建成功只是响应丢了。这时候如果直接重试就会造成重复实例。处理策略是超时后不去重试而是让单据进入失败状态由用户通过查询CPWF实例确认是否创建成功再决定是重新提交还是处理异常。虽然多了一点人工操作但避免了重复审批这种更严重的业务事故。4.3 审批结果回写用回调把状态闭环工作流实例创建成功只是整个流程的一半。审批人在CPWF收件箱里点了通过或拒绝这个结果必须回写到ABAP侧的BO状态里否则SAP端根本不知道审批结果。CPWF侧回写的最佳方式是反向HTTP调用。在CPWF流程编辑器中审批通过和拒绝的节点后面分别配置一个HTTP请求节点调用ABAP侧暴露的OData服务。这个OData服务需要支持两个关键actionapprove和reject。在RAP侧暴露回调action的方法也很直接。在Behavior Definition里定义的approve和reject两个action通过Service Definition和Service Binding暴露成OData服务。回调请求进来时OData服务的action处理逻辑会被触发我们在behavior implementation里实现状态更新METHOD approve. MODIFY ENTITIES OF zrap_req_h ENTITY ReqHeader UPDATE SET FIELDS WITH VALUE #( ( request_id lv_request_id_from_callback status APPROVED approver lv_approver approval_time lv_time ) ) FAILED DATA(ls_failed) REPORTED DATA(ls_reported). ENDMETHOD.调用回填的字段中包含审批人信息这样业务侧能追踪到这个申请是谁批的、什么时候批的对企业审计来说必不可少。回调接口的鉴权也要处理好。CPWF的HTTP节点发起回调时需要用OAuth认证来访问ABAP侧的OData服务。在ABAP侧通过Communication Arrangement配置入站认证。这里有个很容易踩的坑OData服务的CSRF保护。SAP默认开启CSRF token验证如果你在回调时不携带有效的CSRF token请求会返回403。标准做法是CPWF的HTTP节点内置了获取CSRF token的逻辑在发起正式写入请求前先发送一个GET请求拿到token然后在POST请求头里带上。这个逻辑在CPWF流程编辑器里可以配置但很多人会漏掉。4.4 状态机总览一张表说清所有流转把整个方案的前台、后台、云端串起来看状态流转是这样的当前状态触发动作目标状态对应操作INITIALsubmitForApprovalPROCESSING创建CPWF实例状态置为处理中PROCESSING审批人通过回调APPROVED回写审批人、审批时间PROCESSING审批人拒绝回调REJECTED回写拒绝原因可附加消息PROCESSING无系统检测异常FAILED工作流实例状态非正常FAILEDresetAfterFailureINITIAL清空错误信息允许重新提交这个状态机不是我在设计之初就画的而是在两次生产事故后逐渐完善的。第一次是状态卡在PROCESSING没有超时机制后来加了定时任务扫描超时实例。第二次是审批结果回写失败后单据状态永远停在PROCESSING后来设计了补偿机制。现在这个状态机已经稳定运行了三个多月了。5. 压力测试与问题排查实录5.1 认证401的常见坑做过的RAP集成CPWF项目里遇到最多的就是认证问题。总共五个常见原因我整理成一个自查表现象原因解决方案POST /v1/workflow-instances 返回401服务密钥里client secret填错或已轮换在BTP里重新生成密钥并更新Communication Arrangement401且token URL不正确Communication Arrangement里OAuth token endpoint配置指向了错误的区域确认BTP子账户region使用正确的token URL403而非401工作流服务未激活或子账户entitlement释放检查BTP子账户的workflow实例是否存在偶发401过一会又好了token过期后没有缓存刷新检查ABAP侧是否使用了Destination的token缓存机制确认没有手工绕过回调ABAP OData时403CSRF token缺失或过期在CPWF HTTP节点先GET /csrf_token再发送正式请求关于token缓存ABAP侧的Destination框架会自动处理OAuth token的获取和刷新你不需要在代码里手动获取token。但如果你出于某些原因绕过了Destination直接在代码里用cl_http_client配合手动密码验证就要自己管理token的过期时间。我在早期版本就这么干过后来发现token每隔几小时就过期单据审批发起经常失败被运维同事吐槽了一周。5.2 工作流启动了但审批人看不到实例这个问题的排查路径很有意思。工作流实例在ABAP侧显示创建成功了BO里的workflow_instance_id也有值但审批人在CPWF收件箱里就是看不到任务。最常见的原因是CPWF的visibility权限配置问题。Workflow管理的维度是人如果审批人的用户没有分配对应的流程参与者角色实例创建成功但任务不会出现在他的收件箱里。解法是在CPWF的流程定义或参与者配置里把审批人映射到正确的用户或用户组。第二种原因是审批人字段没有传正确。比如上下文里配置的是mangerEmail这个字段但流程定义里绑定的是approverEmail两个字段对不上流程引擎找不到有效审批人任务就会一直没有assignee。这种问题在联调时应尽早用管理员视角查看流程实例详情检查context里传递的字段值是否都正确。第三种原因是审批人多级配置里中间节点的责任人不明确。CPWF在某些情况下不会自动跳过空白节点如果某一级审批人没配置任务会停下来等待。这个不是错误但会造成流程看起来没响应的错觉。5.3 回调回写失败的排查路径审批结果回写失败也是个高频问题。这里的坑往往在数据格式和路径不对上。我整理了一个典型的排查路径确认CPWF的HTTP节点里URL是否指向了正确的OData服务路径。很多人在配置时写成了Service Binding的URL但没有拼上entity的action路径。确认回调请求的HTTP方法是POST。RAP的action通过OData服务调用时标准方法是POST。确认请求体是JSON格式且字段名与RAP BO的字段名对应。这里有个惯性坑RAP暴露的OData服务字段名默认是Service Binding里的名字和BO内部字段名不一定完全一致需要在Service Definition里确认。查看ABAP侧的应用日志。SE80或者ADT里的应用日志能够显示OData请求的入站记录以及抛出的异常。如果你在behavior implementation里加了RAISE EXCEPTION或APPEND message日志里都有。如果排到第4步还是没头绪我建议在behavior implementation的approve/reject方法入口打断点如果有本地调试权限的话看请求到底有没有进来。没进来说明是网络或destination配置问题进来了却失败那就是处理逻辑的问题。这个二分排查法能节省大量时间。5.4 超时与重试经验第三个让人头疼的问题是超时。CPWF的API响应时间通常在几百毫秒到几秒之间但偶尔会遇到慢请求。如果RAP的HTTP客户端没有设置超时时间默认值可能会让你的用户在Fiori界面等得很焦虑。我的建议是给HTTP客户端设置两个超时参数连接超时5秒读取超时15秒。同时配合前面说过的幂等设计——超时后不要自动重试而是把单据置为失败状态让用户确认后再决定是否重新提交。另外建议在ABAP侧给关键的工作流触发动作写日志。通过cl_bali_log创建应用日志记录每次触发的时间、响应状态码、返回的workflow instance id。这些日志不仅方便问题定位还能在月度统计里发现哪类单据总是触发失败这样的深层次问题。我之前就是因为日志发现某个用户创建的单据总是触发失败最后查出来是他的数据里有个字段的长度超了CPWF上下文接收的限制。这种问题没有日志根本无从查起。6. 后续扩展这套集成还能怎么玩方案稳定运行之后可以做一些锦上添花的扩展。第一个方向是审批超时提醒。在CPWF流程模型里加一个定时节点如果审批任务超过48小时未处理自动发邮件或者在企业微信群里提醒审批人。用CPWF做这个比在SAP侧写定时任务简单得多因为流程节点天然支持等待和计时。第二个方向是查询审批进度。通过RAP暴露一个只读接口返回BO的当前状态和workflow_instance_id。外部系统拿到workflow_instance_id后可以直接调用CPWF的实例状态查询API获取当前节点、历史记录等详细信息。这样用户在SAP界面就能看到第一级审批已通过第二级等待部门经理审批这样的细粒度进度。第三个方向是把审批结果同步给关联的BO。采购申请审批通过后可能还要联动创建采购订单或者更新某个统计报表。这时在approve回调action里继续做EML修正可以形成更完整的业务闭环。这些都是CPWF的HttpResponse节点串联起来的但前提是RAP侧的回调接口设计得足够健壮我在实践中也顺手整理了ABAP RAP集成工作流的技术笔记后续有时间会专门拆一下流程编排层的配置细节。
返回列表