平时做SAP B1项目接触过不少中小企业客户,最常被问到的需求除了财务月结,就是"能不能让钉钉上的审批流直接进ERP"。说实话,很多公司内部跑的都是两套系统:员工日常审批在钉钉上完成,流程确实快,但单据永远只在手机里躺一个"已通过"的状态;另一边财务和采购在SAP B1里,还得手动把钉钉上的申请一笔一笔录进去。时间一长,数据对不上、单据丢单、月底结账前疯狂补录,都是常态。
这篇主要讲的就是怎么把SAP B1里的采购申请单审批,完整放到钉钉上来跑:员工在钉钉上填一张采购申请单,走完企业内部的审批流之后,系统自动把数据写到SAP B1里生成正式的Purchase Request单,同时把SAP B1的单号回推给钉钉。整个过程尽量少人工干预,审批记录和ERP单据能一一对应。
如果你正在做SAP B1的二次开发、准备接钉钉的开放平台接口,或者所在公司在纠结要不要做OA审批和ERP的集成,这篇文章可以作为一份选型和落地的参考。下面是我在实际项目里的完整方案,包括架构设计、字段映射、接口调用、踩过的坑,文末还有一份高频问题清单。
1. 为什么采购申请审批一定要打通SAP B1与钉钉
1.1 双轨办公下的数据孤岛问题
采购申请这个场景特别典型。SAP B1在中小制造和贸易企业里覆盖率很高,但绝大多数实际使用B1的公司,一线员工并不是天天坐在电脑前对着ERP客户端操作的。销售要申请样机、车间要申请辅料、行政要申请办公用品,这些场景天然发生在移动端,所以钉钉几乎是标配。
问题就出在这里:员工在钉钉上提交审批,审批通过之后,这张申请单如果没人去B1里补录,采购部门就没法在ERP里安排后续的采购订单。反之,如果采购人员在B1里手动建了单,钉钉上又显示审批已完成,两边各记各的账,月底一查,同一笔采购到底批没批、ERP里有没有建单,完全对不上。这就是典型的双系统数据割裂。
我见过最夸张的情况,是一家做电子元件的贸易公司,采购申请全靠钉钉审批通过后截图发给采购,采购再照着截图在B1里录单。截图里的品名规格和B1物料主数据不完全一致,录单的人还得一个个去核对物料编码。一个月有300多张采购申请,录错、录漏、重复录的至少占两成。
1.2 传统方案的三条路,为什么都不彻底
在正式做接口集成之前,大部分企业解决这个问题的思路无外乎三条路:
纯手工录单。审批走钉钉,录单走B1,两边靠人肉同步。优点是没有开发成本,缺点是效率低、错误率高、过程不透明。人工录单还有另外一个问题,就是延迟。审批通过后隔几天才录进系统,等于采购周期被人为拉长了。
截图/邮件审批。也有企业干脆不走钉钉审批,而是让申请人把采购需求发邮件或者直接截图给采购部门,采购在B1里建单后再用邮件回复确认。这种方式比第一种稍微集中一些,但审批痕迹分散在个人邮箱里,审计的时候非常被动。
单独开发一套审批系统。有些企业选择了自建一套独立的采购申请系统,开发周期长、成本高,而且一线员工还要多学一套系统,推广阻力很大。更麻烦的是,这套自建系统还是得和B1做接口,等于绕了一圈,工作和成本一点没少。
这三条路的共同问题,是把"审批过程"和"ERP数据"当成两件事在管。而集成的核心思路恰恰相反:审批过程可以留在钉钉,但审批的结果必须直接变成ERP里的业务单据,让钉钉成为ERP的移动端入口,而不是一个孤立的审批工具。
1.3 集成方案到底能带来什么
从项目落地后的实际效果来看,打通之后最直观的变化有几点:
- 单据生成时间从"人工补录"变成"审批通过后自动同步",采购申请到采购订单的周期能缩短1到2天;
- 物料编码、供应商编码、税率、仓库等主数据来自B1侧校验,录单错误基本消除;
- 每张审批单都能在B1里找到对应的原始申请,审计追溯时有完整的链路;
- 钉钉上完成审批的员工,能看到最终生成的B1采购申请单号,心理上的确定感完全不一样。
这里要特意说明一点,集成不是要把钉钉改造成B1,也不是要把B1的完整业务流程搬到钉钉上。整个方案的服务边界是"采购申请单"这一步,后面的采购订单、收货、发票校验仍然留在B1内部,钉钉只负责触达和审批。
2. 整体数据流:从钉钉审批发起到SAP B1采购申请落库
2.1 主数据流与单据状态定义
这个方案里最简单的理解方式是:钉钉管审批,B1管账本,中间要有一个翻译官。整体数据流大致如下:
- 申请人在钉钉的采购申请模板里填写申请单,指定审批人,提交;
- 钉钉审批流按照预设的规则进行审批(一级审批、二级审批、条件审批等);
- 审批流结束后,钉钉开放平台通过回调事件,把审批结果(同意/驳回/撤回)推送给集成中间件;
- 中间件校验审批结果,调用SAP B1的Service Layer接口,在B1里创建采购申请单;
- 创建成功后,中间件把B1生成的单据编号更新回钉钉的外部系统字段(或者单独发一条通知给申请人);
- 如果写ERP失败,中间件记录失败日志并触发重试,同时通知IT管理员介入。
整个流程里,中间件是核心。它既要接收钉钉的回调,又要调用B1的接口,还要负责状态记录、异常处理、幂等控制和数据转换。建议在数据库里维护一张单据状态表,把每条申报的运行状态记录下来。
2.2 中间件的角色:为什么需要一层翻译官
有些朋友可能第一反应是,能不能让钉钉直接调用SAP B1的Service Layer接口?技术上不是完全不行,但实际做的时候很容易被几个问题卡住:
- Service Layer的鉴权方式和企业网络环境,直接暴露给钉钉的服务器,安全性很难保障;
- 钉钉审批回调是异步、事件驱动的,如果B1接口响应超时或者不可用,钉钉不会帮你做复杂的重试处理;
- 审批结果里的数据是钉钉表单的语义,物料编码、供应商编码需要先映射成B1侧的主数据格式,这个转换逻辑放在钉钉和B1之间的独立服务里更灵活。
所以中间件最基本、最朴素的职责,是当一个"翻译官+可靠投递员"。它接收钉钉的JSON报文,解析出需要的字段,转换成Service Layer需要的JSON结构,调用B1接口,返回结果回写。中间件本身可以是一个小型的Spring Boot服务,也可以是一套Python服务,甚至用云函数实现,取决于团队熟悉什么技术栈。
我在项目里用的是Java Spring Boot + MySQL,部署在客户内网的一台Windows服务器上。选Java没有特别的宗教原因,主要是客户内部有Java维护能力,而且Spring Boot在定时任务、HTTP调用、日志管理这些方面都很成熟。如果你团队更熟Python,用FastAPI或者Flask也完全可以,核心是这层服务要足够简单、稳定、可监控。
2.3 单据状态机设计
这步容易被忽略,但非常重要。建议在中间件里维护一张pr_sync_log表,字段大致如下:
| 字段 | 说明 |
|---|---|
| id | 自增主键 |
| process_instance_id | 钉钉审批实例ID |
| form_data_json | 钉钉审批表单原始数据 |
| status | 0-待处理,1-处理中,2-成功,3-失败,4-已重试 |
| b1_doc_entry | SAP B1生成的单据内部ID |
| b1_doc_num | SAP B1生成的单据编号 |
| error_msg | 失败原因 |
| retry_count | 重试次数 |
| create_time / update_time | 创建和更新时间 |
状态流转其实就两种主线:
- 审批通过:待处理 → 处理中 → 成功(或失败 → 重试 → 成功/终态失败)
- 审批驳回/取消:直接标记为终态,不调用B1
这张表不只是日志,也是异常处理的基础。后期排查问题、统计成功率和响应耗时,全都靠它。另外,process_instance_id要加唯一索引,这是做幂等控制的第一道保障,后面会细说。
3. 钉钉侧实现:审批模板配置与回调机制
3.1 采购申请表单字段设计
钉钉这边的第一步,是在钉钉管理后台创建一张"采购申请单"审批模板。表单字段的设计要结合B1采购申请单的核心字段来定,不要原封照抄线下纸质单。
建议的表单设计如下:
| 钉钉表单字段 | 组件类型 | 说明 |
|---|---|---|
| 申请事由 | 单行输入 | 必填,作为SAP B1单据的备注/事由 |
| 供应商 | 关联审批单/下拉单选 | 建议用下拉,数据从B1供应商主数据导入 |
| 采购类别 | 下拉单选 | 区分原材料、办公用品、固定资产等 |
| 申请日期 | 日期 | 默认当天,写入B1的申请日期 |
| 需求日期 | 日期 | 必填,写入B1的需求日期 |
| 预算中心/部门 | 下拉单选 | 用于B1里按部门核算,和B1的利润中心对应 |
| 明细列表 | 明细组件 | 物料编码、物料名称、数量、含税单价、备注 |
明细组件是钉钉表单里比较常用的一个能力,审批人可以在手机上直接打开明细逐行查看。明细默认有展开收起的功能,行数多的申请单在手机上也不会太卡。
关于"物料编码"这个字段,不同企业差异很大。有的企业一线员工熟悉物料编码,可以直接填;有的企业员工只写品名规格,需要采购后补编码。我建议如果物料主数据比较规范,尽量在下拉/关联选择里限定范围,避免审批通过后写B1时找不到物料号导致失败。
3.2 审批流的组织架构匹配
钉钉审批流的配置相对简单,通常按照部门负责人、部门负责人+采购负责人、金额分级审批等策略配置即可。这里要提醒的是,钉钉审批流条件规则依赖组织架构和角色,上线前要把审批人名单重新梳理一遍。
常见的一个坑是:按角色选审批人(比如"部门主管")时,钉钉会自动识别发起人的直接上级。但如果员工在组织架构里所在的部门是空的,或者部门主管角色没有设定,审批流会报"无审批人"错误,流程会直接卡在发起环节。这类问题在集成方案里需要提前排查,不然后续流程根本走不起来。
3.3 审批结果回调:订阅事件方式
钉钉侧最关键的技术点,是审批结果如何通知到中间件。目前主流的做法是使用钉钉开放平台的"事件订阅"能力。
在钉钉开放平台的开发者后台,创建一个企业内部应用,申请以下权限:
- 审批实例的读取权限(审批实例的详情)
- 通讯录只读权限(获取用户信息,用于用户映射)
然后在"事件订阅"里,订阅这两个事件:
bpms_instance_change:审批实例状态变更,包括审批开始、审批结束等场景bpms_task_change:任务状态变更,一般用于跟踪每个审批节点的动作
订阅事件时,需要配置一个回调URL,钉钉会往这个URL推送加密的JSON报文。这里有两个容易踩的细节:
加密方式。钉钉的事件回调默认使用AES加密,需要你在开发者后台配置一个AES Key和Token。接收回调时,要先解密,再用Token做签名校验。如果加密解密配置不一致,钉钉后台会一直显示"订阅回调测试失败"。
响应超时。钉钉的回调推送有超时机制,业务处理完要同步返回一个加密的"success"字符串。如果你在回调接口里同步调用B1接口创建单据,万一Service Layer响应很慢(超过钉钉的超时时间),钉钉会认为推送失败并重复推送。所以回调接口里尽量只做"接收事件+落库+异步处理"这三件事,把B1写入放到异步队列或者定时任务里去执行,避免因为处理超时导致重复推送。
3.4 多公司/多账套的标识问题
做SAP B1项目的朋友都知道,一套Service Layer可能对应多个账套/公司,或者一个企业同时有多个独立核算的分公司。这种情况在钉钉侧怎么区分?
方案是在审批模板上增加一个不可见字段(比如叫"公司编码"),由系统通过钉钉的"流程表单外部选项"能力预填,或者在模板里放一个下拉框由申请人选择公司。字段值会随着审批单数据一起回传,中间件收到后根据这个字段决定调用哪个Service Layer实例。这个字段在表单上是否可见、是否允许修改,要根据实际管理需求来设置。
4. SAP B1侧写入:Service Layer与DI API的选型对比及采购申请单核心字段映射
4.1 Service Layer还是DI API,怎么选
SAP B1的集成接口,历史上常用的是DI API和DI Server,现在更推荐使用Service Layer。
| 对比项 | DI API | Service Layer |
|---|---|---|
| 技术栈 | 依赖COM组件,Windows环境 | REST API,跨平台 |
| 部署要求 | 需要安装B1客户端/DLL | 需要安装Service Layer服务 |
| 性能 | 单条操作快,但并发能力一般 | 支持批量,HTTP并发更好 |
| 维护性 | 版本升级容易出兼容问题 | 接口版本由SAP管理 |
| 社区/文档 | 老资料多 | 官方标准接口,逐步成为主流 |
如果你是在新项目里做集成,优先用Service Layer。一方面它不需要在中间件服务器上安装B1客户端,另一方面Service Layer在SAP HANA版本的B1上支持得最好。
连接Service Layer的标准方式是:安装B1的时候,选择以"公司数据库"和"单一公司"模式启动Service Layer(如果有多个账套,也可以用管理公司模式)。连接时用B1超级管理员账号或者专门创建的集成账号,先调用登录接口获取SessionId,之后每次请求Token放在Cookie里。登录接口的典型请求如下:
POST http://<service-layer-host>:50000/b1s/v1/Login Content-Type: application/json { "CompanyDB": "SBO_YourCompany", "UserName": "manager", "Password": "your_password", "Language": 24 }请求成功后,会返回一个SessionId,后续接口调用时在请求头加上Cookie: B1SESSION=<SessionId>即可。注意,SessionId是有有效期的,默认是30分钟左右,建议在中间件里做Session的缓存和自动重新登录,不要每次请求都登录一次。
4.2 采购申请单(Purchase Request)的核心字段映射
SAP B1里采购申请单对应的对象是PurchaseRequest,数据表底层是OPRQ(主表)和PRQ1(明细行)。Service Layer的请求体结构如下(核心字段版):
{ "DocType": "dDocument_Items", "DocDate": "2025-06-01", "DocDueDate": "2025-06-05", "ReqDate": "2025-06-01", "ReqType": "rntEmployee", "Requester": "12", "RequesterName": "张三", "U_ApprovalProcessID": "ding1234567890abcdef", "U_DingTalkInstanceID": "proc1234567890", "Comments": "生产急需物料采购申请", "PurchaseRequestLines": [ { "ItemCode": "A0001", "Quantity": 100, "Price": 12.5, "TaxCode": "VAT17", "U_RequireDate": "2025-06-05", "LineText": "用于产线A的紧急补充物料" } ] }逐字段解释下为什么这么设:
DocDate:单据日期,建议直接用钉钉表单里的"申请日期",不要用系统当天日期。因为有些公司要求按实际业务日期入账,万一审批跨天、提交日期和审批日期不一致,用提交日期更符合业务事实。DocDueDate:到期日期。一般等于申请人填的"需求日期",采购部门拿到这个单子后,需要在这个日期前完成采购订单的下达。ReqType / Requester:申请类型和申请人ID。ReqType可以设置为员工(rntEmployee),Requester对应B1内部的员工ID。更简单的做法是直接用RequesterName,但为了报表统计准确性,还是建议维护一张钉钉用户ID和B1员工ID的映射表。U_ApprovalProcessID / U_DingTalkInstanceID:自定义字段,存储钉钉的审批流ID和审批实例ID。这是整个集成追溯链路的桥梁字段,建议加到B1的采购申请单界面上并创建索引,后期按钉钉审批单号反查B1单号、按B1单号反查钉钉审批记录,都靠这两个字段。PurchaseRequestLines:明细行。关键字段是ItemCode(物料编码)、Quantity(数量)、Price(价格)、TaxCode(税码)。特别注意:如果B1物料主数据里设置了税码,通常会被带到单据行里,但钉钉表单里如果允许申请人填含税单价,需要转换或者直接由B1端的税码规则计算,避免同一物料在不同单据上税率不一致。
4.3 单据编号策略:用B1自动编号还是自定义编号
写采购申请单时,另一个需要提前确定的是单据编号策略。SAP B1默认的采购申请单编号规则是OPRQ.DocNum,由系统按序列自动生成。如果你不做任何干预,直接POST请求后B1会返回一个系统单据编号。
这里有一个常见的认知误区:有人以为可以在Service Layer请求里自定义DocNum,实际上Service Layer默认是不允许直接修改DocNum的,因为编号规则被系统序列管控。如果你确实需要自定义编号(比如希望编号里包含部门代号),需要在B1的编号序列配置里做文章,而不是在Service Layer请求里硬编码。
我的建议是:编号走B1系统序列,不做自定义。原因很简单:自定义编号的规则维护成本高,而且一旦和钉钉侧的回写顺序冲突,很容易出现编号重复或者空号。集成追溯靠的是U_DingTalkInstanceID,不是靠单号格式。单号留给B1管,钉钉侧通过回写字段展示。
4.4 联系细节:Service Layer 创建采购申请单的完整调用示例
这里给出一个相对完整的创建采购申请单的代码示例,以Java + RestTemplate为例:
public String createPurchaseRequest(String jsonBody, String sessionId) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Cookie", "B1SESSION=" + sessionId); HttpEntity<String> request = new HttpEntity<>(jsonBody, headers); String url = "https://<service-layer-host>:50000/b1s/v1/PurchaseRequest"; try { ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class); if (response.getStatusCode().is2xxSuccessful()) { JSONObject obj = new JSONObject(response.getBody()); // obj里面会返回 DocEntry 和 DocNum return obj.getString("DocNum"); } else { // 具体错误信息在response body的error字段中 throw new RuntimeException("创建采购申请失败: " + response.getBody()); } } catch (HttpStatusCodeException e) { // 错误码处理:400通常是字段校验失败,500通常是服务端异常 throw new RuntimeException("Service Layer调用异常", e); } }需要注意,Service Layer 在返回400错误时,响应体里的error.message会有非常详细的字段校验异常描述(比如"字段 XXX 必填"、"物料 A0001 不存在")。中间件要把这些错误信息完整记录到日志表里,这样排查问题时能直接定位到是哪一行的哪个字段出问题。
5. 状态同步与边界情况:驳回、撤回、离职审批人
5.1 审批驳回时如何回写状态
这是很多人做的时候容易漏的场景。钉钉审批通过后集成到B1,大家都能想到。但驳回呢?驳回后申请人在钉钉上修改一下表单,再重新提交,此时钉钉会创建一个全新的审批实例。如果你不做任何处理,系统里同一个采购需求可能会有两条审批记录,一条驳回、一条通过,而B1只收到通过的这条,表面上没问题,但审计时容易说不清楚。
建议的处理方式:在中间件里维护一个"关联申请ID"的概念。钉钉表单里加一个隐藏字段U_OldInstanceID,如果申请人是修改后重新提交,通过钉钉的流程表单功能把上一版审批实例ID带过来。中间件收到新的审批通过事件后,如果U_OldInstanceID有值,先检查B1里是否已经有一张对应的采购申请单(通过U_DingTalkInstanceID查询),如果有,可以选择先不删除旧单(因为B1里的采购申请单可能已经被采购订单引用),而是创建一个新单并在备注里写明"此单为对原审批单XXX的替代申请"。
这一点对实施团队尤其重要。采购申请单不比草稿,它在B1里如果已经关联了下游的采购订单,就不能随便删除或者取消。重新提一张全新的单子,在业务上反而是最安全的。
5.2 审批撤回和修改再提交的双写问题
钉钉审批流还有一个很坑的细节:审批人如果在审批过程中点了"退回并修改",申请人的表单会回到草稿状态,修改后可以重新提交,生成的是一个全新的审批实例ID。此时如果中间件只认U_DingTalkInstanceID做唯一标识,那么同一条采购需求就有多个审批实例ID,B1里可能出现重复单据。
解决思路有两种:
第一种,主数据层面。把钉钉表单的"申请事由+申请人+提交日期"作为业务唯一键,在中间件里做重复判断。但这种方式容易误伤,比如同一个申请人同一天提交两笔完全相同的物料申请,会被误判为重复。
第二种,用隐藏字段维护业务关联ID。在钉钉表单里加一个U_BizRequestID(业务申请ID),提交时如果该字段为空则自动生成一个UUID,修改后重新提交时保留之前的UUID。中间件以U_BizRequestID作为业务唯一键判断重复,确保同一个业务申请ID只在B1里创建一张单据。第一次创建成功后把B1单号回填到钉钉的单据编号字段;如果后续又收到同一U_BizRequestID的审批通过事件,直接更新已有单据或者忽略,避免重复创建。
我强烈建议采用第二种方案。业务唯一键的问题不要依赖自然字段拼凑,单独维护一个业务申请ID字段,逻辑清晰,排查也方便。
5.3 审批人离职或者组织架构变动
这个坑在项目运维阶段最让人头疼。钉钉审批流经常配置的是"按角色找审批人",如果某个部门的主管离职了,组织架构里没有及时更新新主管,钉钉会认为审批人不存在,整个流程卡住。更麻烦的是,如果管理员在钉钉后台直接删除了离职员工的账号,而该员工的审批单子还处于审批中状态,钉钉会允许其他管理员代审,但这个"代审"操作对审批流程的推进方式有要求,配置错了会直接导致流程终止。
应对手段:上线前做一轮审批人和角色的梳理,和HR系统对齐信息;上线后配置一个钉钉审批流失败通知人(通常是IT负责人),一旦出现"无审批人"异常,第一时间能在群里收到通知。这个看起来简单,实际项目里有很多企业因为没做这一步,流程卡了两三天都没人发现。
5.4 失败补偿:定时任务与手工修复通道
再稳定的中间件,也难免出现极端情况:Service Layer临时不可用、密码过期、网络抖动。这种情况靠同步重试解决不了问题,需要一套补偿机制。
我在中间件里做了一个定时任务,每5分钟扫描一次pr_sync_log表,找出所有status=3(失败)且retry_count小于5的记录,重试一次。重试时先检查B1里是否已经存在相同U_DingTalkInstanceID的单据,存在则直接标记为成功,并回写单号;不存在才真正发起创建。
超过5次重试还是失败的单据,状态变为终态失败,系统自动发钉钉消息通知IT管理员。管理员登录中间件后台,可以查看完整的请求和响应日志,手动修正数据后点"重新执行"。这个手工修复通道在项目初期特别有用,很多问题都是靠这一步兜底解决的。
6. 实测中遇到的高频问题与处理清单
最后把这些年在类似项目里真正踩过的坑梳理成一份清单,每一类都附了解决思路,后续实施时可以对照自检。
时间字段时区差异:钉钉和B1默认时区不同是高频问题。钉钉服务器用的UTC时间,你拿到的日期时间字符串如果不做转换,直接PUT给B1,经常出现差8小时的情况。建议在中间件里统一用Asia/Shanghai时区解析钉钉回传的日期字段,然后再转成B1需要的日期格式。解决方案:在读取钉钉事件的createTime、表单日期字段时,显式指定时区,不要依赖服务器系统时区。
金额精度不一致:钉钉表单里的金额数字默认是浮点数传输,B1的金额字段通常是两位小数(库存币种)。如果你把钉钉那头的含税金额原样传给Service Layer,遇到长小数(比如0.1+0.2这种),很容易出现实际写入金额比申请金额多0.01元的问题。解决方案:中间件解析时统一用BigDecimal或Decimal类型,做两位小数四舍五入后再组装JSON,不要在字符串阶段就直接拼接。
钉钉用户与B1员工ID映射:钉钉的userid(形如uidRandomString)和B1的员工ID没有任何天然对应关系,必须建一张映射表。映射表的维护方式建议做成"员工自维护+管理员审核":员工在钉钉上首次使用系统时,填写B1员工编号,管理员在后台审核绑定。审计比较严格的公司,尽量做成和HR系统或AD打通,自动同步。
Service Layer并发锁冲突:如果同一个B1账套同时被多个外部系统调用(比如其他自建系统也在写B1),Service Layer偶尔会返回"Could not lock object"之类的错误。这类错误通常是并发写同一类单据导致的。解决方案:在中间件里对同一种单据的创建请求加一把分布式锁(按账套粒度),其他请求排队等待,避免并发冲突。
附件要不要传到B1:钉钉审批单里经常有附件(比如供应商报价单扫描件、技术图纸),B1的采购申请单本身也支持附件管理(Attachments2)。但在实际项目里,我不建议默认把钉钉的附件自动同步到B1。第一,B1附件存的是附件路径/URL,文件本身还是要在文件服务器或对象存储里放一份,等于双份存储;第二,B1采购申请单的附件在后续单据流转中很少被下游真正读取,默认同步反而增加接口复杂度和存储成本。我的建议是:首次实施阶段不同步附件,B1单据的备注里记录钉钉审批实例ID,需要看附件时直接从钉钉审批详情打开。如果后续业务上真的有"必须在B1里离线查看附件"的需求,再单独做一个附件下载同步功能。
幂等控制必须做:钉钉的事件订阅在某些场景下会重复推送同一事件(网络超时后重试、后台手动重推等),Service Layer的调用如果重复执行,B1里就会出现两张完全相同的采购申请单。所以中间件在处理前,必须先根据process_instance_id在pr_sync_log表里查重。如果已经存在且处理成功,直接幂等返回,不再调用B1。
用户映射缺失时的临时处理:钉钉上提交申请的员工,如果在映射表里找不到对应的B1员工ID,建议不要直接报错,而是走"单子先落进B1,申请人字段留空,后续由管理员补录"的流程。因为采购申请往往对时效性要求高,因为一个用户映射问题卡住整个业务不划算。这个"宽进严出"的思路在很多集成项目里都能少踩不少坑。
最后分享一点心得
这个项目做完之后,我最深的体会是:SAP B1和钉钉的集成,技术上本身并没有多难,真正难的是把业务边界想清楚。钉钉解决的是"人怎么申请、怎么审批",B1解决的是"账怎么记、单怎么流转",中间的桥怎么搭,完全取决于企业的管理粒度。
另外,集成方案上线后一定要留一个星期到一个月的"双人并行观察期"。我见过不少项目,接口上线第一天就急着把钉钉上的线下流程全部停掉,结果发现B1里单据的字段映射有些地方不符合采购部门的实际习惯,又灰溜溜地恢复线下。稳妥的做法是先并行跑半个月,每天让采购人员在B1里抽查几张自动同步的单子,确认无误后再彻底切换到新流程。
最后再分享一个小技巧:调钉钉接口和Service Layer的时候,强烈建议在中间件里给每条外部请求和响应都打印全链路日志,格式包含时间戳、审批实例ID、B1单据号、耗时。这个习惯在项目初期看起来有点繁琐,但等系统上线几个月后,任何一次数据对不上,你只需要打开日志表,点开一条记录,就能知道这条单据走到哪一步、在哪一步出了问题,省下的排查时间绝对是当初写日志代码的好几倍。