资讯动态

基于Spring Boot与Quartz的SAP RFC定时同步系统实战

发布时间:2026/10/26 20:45:33 来源:尧图企业网站定制
简介面向需要将SAP数据与外部系统双向打通的Java开发者这套基于Spring Boot的后台管理系统以Shiro权限、Quartz定时调度和SAPJCo3 RFC调用为核心提供数据定时同步与接口对外服务的基础实现并覆盖后台管理基本功能适合企业集成、后台管理脚手架参考或SAP对接入门学习。压缩包共336个文件、约9.46MB其中98个Java源文件承载后端分层与业务逻辑41个JavaScript、28个HTML、25个CSS组成前端页面与交互另有XML、YML、properties配置、SQL脚本及SAPJCo3本地库文件便于梳理依赖、数据库与运行环境。项目已有432人浏览学习。其价值在于把权限控制、定时任务、SAP RFC访问和简约响应式单页前端放进同一套可运行结构里读者可据此理解接口服务如何编排、同步任务如何落地并参考Druid数据源、MyBatis-Plus持久层与Thymeleaf模板的组织方式快速形成自己的企业系统对接方案。1. 一件「定时把数捞回来」的接口需求为什么值得单独搭个系统起因很普通对方丢过来一页纸上面列着几个 RFC 函数名和一串字段末尾一句「每天跑一次」。真动手才发现难的不是用 Spring Boot 起服务而是后面那些细碎事——网络时通时不通、单次调用几百毫秒、改个 Cron 表达式要重启、谁点了「手工执行」出了事要能查到人。这套 sapweb 把这些事一次性收拢Spring Boot 做骨架MyBatis 存任务配置和调用流水Quartz 2 管定时触发Shiro 管入口与按钮权限Thymeleaf 出后台页面页面里再垫一小段 JavaScript 轮询执行进度最底下 sapjco 负责真正的 RFC 交互。如果你手上也是一份「SAP 侧出函数、Java 侧定时拉」的活或者想找一个把 JCo、调度、权限串成闭环的骨架这套源码可以直接当底子改。2. 整体架构与技术选型六个组件各自守哪一截这套项目的分层很朴素没有 DDD 那套弯弯绕。Controller 只管接请求和返回Service 里放业务编排真正跟 SAP 打交道的代码被压在一个 RfcInvokeService 里再往下就是 sapjco 的 API。之所以这么切是因为 RFC 调用的失败率明显高于普通 CRUD把它孤立在一层里加日志、加重试、加熔断都只改一个文件。选型上几乎每一项都是被场景逼出来的不是为了堆技术。任务要能动态增删改所以调度必须落库Quartz 2 的 JDBC JobStore 正好后台要给业务人员点模板引擎选 Thymeleaf 而不是前后端分离是因为页面就十几个服务端渲染省掉一整套构建链权限要细到按钮Shiro 的字符串权限模型写起来比 Spring Security 轻JCo 没得选跟 SAP 做 RFC 交互Java 侧就这一条路。组件在项目里负责什么关键落点Spring Boot容器、配置装配、内嵌容器启动启动类、application.ymlsapjco3建立连接、调用 RFC、读写参数SapDestinationConfig、RfcInvokeServiceMyBatis任务配置、字段映射、调用流水mapper 接口与 XMLQuartz 2Cron 触发、并发控制、失火策略RfcSyncJob、JobScheduleServiceShiro登录、角色、按钮级权限ShiroConfig、SapUserRealmThymeleaf JavaScript后台列表页、执行进度轮询templates/job/list.html、static/js/job.js2.1 一次同步任务在各层的落点把「页面上点一次执行」这条链路摊开看能最快理解各层的边界。请求进 JobController先过 Shiro 的perms[job:manage]校验再交给 JobServiceJobService 不直接调 RFC而是往 Quartz 里塞一个一次性 Trigger让执行走和定时任务完全相同的路径。这样手工执行和定时执行共用一套逻辑日志格式一致排查时不用分辨「这次是点的还是自动跑的」。真正干活的 RfcSyncJob 从 JobDataMap 里取任务主键回表查配置拼出 RFC 的入参调 RfcInvokeService拿到结果后按字段映射表转成业务对象最后把成功条数、耗时、异常堆栈写进 rfc_call_log。整个过程中Job 里不出现任何 SAP 相关的代码Service 里不出现任何调度相关的代码这条线守住了后面加接口才不至于一锅粥。提示手工触发别写成直接new Thread(() - service.call())。绕过调度器看着省事等你要做「同一任务不允许并发」时就得再补一套锁血泪经验。2.2 权限与页面层Shiro 的过滤器链先兜住入口Shiro 的配置集中在 ShiroConfig 里核心就是一张有序的过滤器链。顺序很关键静态资源和登录页必须放在最前面否则会出现「没登录就加载不了 CSS页面一片白」的经典翻车。Configuration public class ShiroConfig { Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factory new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); factory.setLoginUrl(/login); factory.setUnauthorizedUrl(/403); // LinkedHashMap 保证顺序先放行再收紧 MapString, String chain new LinkedHashMap(); chain.put(/static/**, anon); chain.put(/login, anon); chain.put(/logout, logout); chain.put(/rfc/**, authc,perms[rfc:invoke]); // RFC 手工调用入口 chain.put(/job/**, authc,perms[job:manage]); // 任务增删改与触发 chain.put(/**, authc); factory.setFilterChainDefinitionMap(chain); return factory; } }anon表示匿名可访问authc表示必须登录perms[...]是自定义的权限断言会去 Realm 里取当前用户的权限串比对。参数名job:manage、rfc:invoke是我自己定的命名规范冒号前是资源、冒号后是动作后面加新模块照着加一行就行不需要动 Java 代码。Realm 这边做两件事认证时按账号查库比对密码授权时把该账号的角色和权限串塞进SimpleAuthorizationInfo。public class SapUserRealm extends AuthorizingRealm { Autowired private SysUserMapper userMapper; Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String account (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); info.setRoles(new HashSet(userMapper.selectRoles(account))); info.setStringPermissions(new HashSet(userMapper.selectPerms(account))); return info; } Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { UsernamePasswordToken t (UsernamePasswordToken) token; SysUser user userMapper.selectByAccount(t.getUsername()); if (user null) { throw new UnknownAccountException(账号不存在); } // 库里存的是加盐哈希盐值单独一列交给 Shiro 自己比对 return new SimpleAuthenticationInfo(user.getAccount(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); } }密码比对交给 Shiro 的HashedCredentialsMatcher盐值用ByteSource传进去不要在业务代码里自己算一遍 hash 再比字符串那种写法一旦换算法就全乱。授权部分每次请求都会走doGetAuthorizationInfo用户多的时候要加缓存最简单的做法是在 Realm 里挂一个本地缓存权限变更时手动清一下。2.3 JCo 的部署约束jar 和原生库必须一起走这是整个项目最容易在上线当天翻车的地方。sapjco3 不是一个纯 Java 库sapjco3.jar只是 Java 侧的壳真正干活的是同目录下的原生库——Windows 上是sapjco3.dllLinux 上是libsapjco3.so。只把 jar 打进 fat jar启动时看着没事第一次调 RFC 就抛UnsatisfiedLinkError。我一般的做法是jar 手动 install 进本地仓库用普通依赖引入原生库单独放一个目录启动脚本里加-Djava.library.path/opt/sapjco。之所以不塞进 resources 里靠 Spring Boot 解压是因为解压路径每次重启都可能变java.library.path却只读一次玄学问题特别多。# 把 jar 装进本地/私服仓库避免每个开发者自己找文件 mvn install:install-file -Dfilesapjco3.jar \ -DgroupIdcom.sap.conn -DartifactIdsapjco3 -Dversion3.1 -Dpackagingjar # 启动时指向原生库目录注意是目录不是文件 java -Djava.library.path/opt/sapjco -jar sapweb.jar依赖声明里把 scope 设成system也能跑但打 fat jar 时会丢文件还是要走 install 这条路。另外注意 Linux 上原生库的执行权限chmod 755 libsapjco3.so少一步就是加载失败报错信息还特别模糊。2.4 目录结构一眼能看出改哪儿sapweb ├── src/main/java/com/example/sapweb │ ├── config # ShiroConfig、SapDestinationConfig、QuartzConfig │ ├── controller # 登录、任务管理、RFC 手工调用 │ ├── service # JobService、RfcInvokeService、FieldMappingService │ ├── job # RfcSyncJob 等 Quartz 任务 │ ├── mapper # MyBatis 接口 │ ├── entity # SysJob、RfcCallLog、FieldMapping │ └── shiro # Realm、权限过滤器 ├── src/main/resources │ ├── mapper # 与接口同名的 XML │ ├── templates # Thymeleaf 页面 │ ├── static # js / css │ └── application.yml └── sql # 业务表 Quartz 官方建表脚本config和shiro是两个高频改动区前者放所有外部依赖的装配后者放权限逻辑job包刻意跟service分开因为 Quartz 的 Job 类有实例化约束混在一起容易写出「注入不进 Bean」的问题。sql目录别嫌土部署到新环境时那几张QRTZ_开头的表经常被忘掉忘了就是启动即报错。3. RFC 调用链路从 JCoDestination 到 JCoFunction 的一次完整往返sapjco 的 API 不多摸清四个类的职责就能干活JCoDestination代表一条到 SAP 的连接目的地JCoRepository是函数元数据的缓存JCoFunction是一次具体调用JCoTable/JCoStructure处理表参数和结构参数。真正容易出问题的是生命周期——谁该复用、谁该每次新建弄反了就是连接数打满或者内存泄漏。3.1 Destination 的建立与复用JCoDestination是线程安全的创建一次可以反复用。创建过程本身很贵内部要做一次完整的登录握手几百毫秒起步。如果每次 RFC 调用都JCoDestinationManager.getDestination(...)拿一个新的压力测试下 SAP 侧连接数很快就打满表现为偶发的CPIC-CALL超时或者账号被锁。推荐做法是实现DestinationDataProvider把连接参数集中管理注册一次全局生效再包一个 Holder 单例。Configuration public class SapDestinationConfig implements DestinationDataProvider { Value(${sap.ashost}) private String ashost; Value(${sap.sysnr}) private String sysnr; Value(${sap.client}) private String client; Value(${sap.user}) private String user; Value(${sap.passwd}) private String passwd; PostConstruct public void register() { if (!Environment.isDestinationDataProviderRegistered()) { Environment.registerDestinationDataProvider(this); } } Override public Properties getDestinationProperties(String name) { if (!SAP_PRD.equals(name)) { return null; // 返回 null 表示不认这个 destination别抛异常 } Properties p new Properties(); p.setProperty(DestinationDataProvider.JCO_ASHOST, ashost); p.setProperty(DestinationDataProvider.JCO_SYSNR, sysnr); p.setProperty(DestinationDataProvider.JCO_CLIENT, client); p.setProperty(DestinationDataProvider.JCO_USER, user); p.setProperty(DestinationDataProvider.JCO_PASSWD, passwd); p.setProperty(DestinationDataProvider.JCO_LANG, zh); // 连接池容量和峰值按并发任务数估别拍脑袋写大 p.setProperty(DestinationDataProvider.JCO_POOL_CAPACITY, 5); p.setProperty(DestinationDataProvider.JCO_PEAK_LIMIT, 10); return p; } }几个参数值得说清楚。JCO_POOL_CAPACITY是常驻连接数JCO_PEAK_LIMIT是峰值上限两者之比决定了突发并发时是排队还是新建连接如果定时任务集中在整点触发峰值要按「同时跑的任务数」给不是按用户数给。JCO_LANG影响返回文本的语言也影响字符编码中文环境写zh比默认值稳。注册完成后拿连接的代码就一行JCoDestination destination JCoDestinationManager.getDestination(SAP_PRD);别在这行外面套 try-catch 然后每次失败就Environment.unregisterDestinationDataProvider重注册那属于典型的过度防御会把已经健康的连接一起干掉。3.2 一次 RFC 调用的完整代码真正的调用就三步从仓库取函数模板、填导入参数、执行并读导出参数。中间的判空和异常分类是重点。public RfcResultListMaterialDto queryMaterials(String werks, int maxRows) { long start System.currentTimeMillis(); JCoDestination destination JCoDestinationManager.getDestination(SAP_PRD); try { // 元数据有缓存第一次取会慢之后基本是内存命中 JCoFunction function destination.getRepository().getFunction(Z_GET_MATERIAL_LIST); if (function null) { throw new IllegalStateException(RFC Z_GET_MATERIAL_LIST 不存在或未在 SAP 侧激活); } function.getImportParameterList().setValue(I_WERKS, werks); function.getImportParameterList().setValue(I_MAX, maxRows); function.execute(destination); // 真正发请求超时和连接异常都从这里抛 ListMaterialDto list new ArrayList(); JCoTable items function.getTableParameterList().getTable(T_ITEM); for (int i 0; i items.getNumRows(); i) { items.setRow(i); // 必须先定位行否则读到的是最后一行 MaterialDto dto new MaterialDto(); dto.setMatnr(items.getString(MATNR)); dto.setMaktx(items.getString(MAKTX)); dto.setMeins(items.getString(MEINS)); list.add(dto); } return RfcResult.ok(list, System.currentTimeMillis() - start); } catch (JCoException e) { // 区分「连不上」和「SAP 业务报错」两者的处理策略完全不同 return RfcResult.fail(e.getKey(), e.getMessage(), System.currentTimeMillis() - start); } }getFunction返回 null 是个高频现象原因通常是函数没激活、账号没权限、或者拼错了名字。这里直接抛异常比返回空列表好空列表会被上层当成「今天没数据」问题就藏进去了。items.setRow(i)这行是新手最容易漏的。JCoTable内部维护一个游标不显式定位就一直在当前行上读循环十次拿到的可能是十份同样的数据。这个 bug 在测试环境数据少的时候几乎发现不了。JCoException的getKey()携带了 SAP 返回的错误码值得原样记进日志。业务侧的错误比如物料不存在会以AbapException的形式抛它是JCoException的子类区别是这类错误重试没有意义而网络类错误重试一次往往就好了。3.3 表参数与结构参数的读写往 SAP 写数据的接口入参通常是表结构。写法上跟读对称先appendRow再setValue。JCoFunction function destination.getRepository().getFunction(Z_POST_INVOICE); JCoTable items function.getTableParameterList().getTable(T_HEAD); for (InvoiceDto dto : dtoList) { items.appendRow(); // 追加一行游标自动落到新行 items.setValue(VBELN, dto.getOrderNo()); items.setValue(BLDAT, dto.getBillDate()); // 日期字段一定要给对格式 items.setValue(WRBTR, dto.getAmount().toPlainString()); } // 结构参数用 getStructure读法和 Table 类似但不循环 JCoStructure head function.getImportParameterList().getStructure(S_HEAD); head.setValue(BUKRS, 1000); function.execute(destination);日期和金额是两类经典坑。日期在 RFC 层是yyyyMMdd的字符串用LocalDate直接toString()会变成2024-05-01SAP 那边直接报格式错误金额要用BigDecimal的toPlainString()用toString()遇到科学计数法比如1E3就废了。这类错误 SAP 返回的消息往往只有一句「数据无效」排查起来全靠日志里把原始报文打出来。3.4 超时、异常与连接健康检查RFC 调用是同步阻塞的网络一抖线程就挂在那儿。默认超时可能长达几分钟正好卡在 Quartz 的工作线程上攒几个就把调度池占满后面的任务全部延后——表现出来是「任务莫名不跑了」实际是被前面那个卡死的拖住了。我的处理分三层。第一层在 Destination 属性里把超时调短业务能接受的范围通常是几十秒第二层在 Service 里做一次有限重试只对连接类异常重试AbapException直接放弃第三层是调度器的DisallowConcurrentExecution保证同一个任务不会因为上一轮没结束而叠加。健康检查别搞复杂用一个无参的轻量函数探活就够了登录后调一次能返回就说明网络和账号都正常。探活结果放进内存页面上显示一个绿色的连接状态点比出事之后翻日志强得多。4. Quartz 2 动态调度把定时配置交给数据库定时任务硬编码在代码里的项目改一次 Cron 就要发一次版业务侧根本等不了。Quartz 2 的 JDBC JobStore 把触发器状态存进数据库配合业务表存 Cron 和参数就能做到「页面上改完立即生效」。代价是多了一组QRTZ_开头的表以及一堆序列化相关的坑。4.1 任务表设计与 JobDetail 动态注册业务表只存「要做什么」和「什么时候做」Quartz 自己存「已经排到哪一步」。两张表职责别混。CREATE TABLE sys_job ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_name VARCHAR(64) NOT NULL COMMENT 任务名界面上展示, rfc_name VARCHAR(64) NOT NULL COMMENT RFC 函数名, cron_expr VARCHAR(64) NOT NULL, param_json VARCHAR(512) COMMENT 入参JSON 串, status TINYINT DEFAULT 0 COMMENT 0 停用 1 启用, last_run_time DATETIME, create_by VARCHAR(32) ); CREATE TABLE rfc_call_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_id BIGINT, rfc_name VARCHAR(64), trigger_type VARCHAR(16) COMMENT AUTO / MANUAL, success TINYINT, row_count INT, cost_ms BIGINT, error_msg TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );注册逻辑要幂等先checkExists存在就删掉重建。删了再建比试图原地修改 Trigger 简单得多也不会出现「旧触发器还挂着」的残留。public void register(SysJob job) throws SchedulerException { JobKey key JobKey.jobKey(rfc- job.getId(), RFC_JOB); if (scheduler.checkExists(key)) { scheduler.deleteJob(key); // 幂等改 Cron 就等于重建 } JobDetail detail JobBuilder.newJob(RfcSyncJob.class) .withIdentity(key) .usingJobData(jobId, job.getId()) // 只传主键绝不传实体对象 .build(); Trigger trigger TriggerBuilder.newTrigger() .withIdentity(rfc-t- job.getId(), RFC_JOB) .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpr()) .withMisfireHandlingInstructionDoNothing()) .build(); scheduler.scheduleJob(detail, trigger); }usingJobData里塞什么是有讲究的。JDBC JobStore 会把JobDataMap序列化进QRTZ_JOB_DETAILS塞一个 Spring 管理的实体对象进去轻则NotSerializableException重则反序列化后拿到一个所有字段都是 null 的幽灵对象。只传jobIdJob 里回表查最新配置顺带还解决了「改了参数不重启不生效」的问题。失火策略也得选。withMisfireHandlingInstructionDoNothing表示错过就不补跑对「每天同步一次」这种任务是对的如果业务要求补数据就换成FireAndProceed但要保证接口本身是幂等的否则重复写入会很难收拾。4.2 可中断的 Job 实现与并发控制Job 类本身很短逻辑全在 Service 里。加DisallowConcurrentExecution是刚需没有它一个跑十分钟的任务会在十分钟内被叠上好几轮。DisallowConcurrentExecution public class RfcSyncJob implements Job { Autowired private JobService jobService; // Spring Boot 的 JobFactory 负责注入 Override public void execute(JobExecutionContext context) { Long jobId context.getMergedJobDataMap().getLong(jobId); String type context.getTrigger().getKey().getGroup().equals(MANUAL) ? MANUAL : AUTO; jobService.runOnce(jobId, type); // 异常在 Service 内部吞掉并落库别往外抛 } }Job 里不要往外抛异常。Quartz 会把异常记进日志但业务侧的rfc_call_log就缺了一条页面上看不到失败记录运维只能去翻服务器日志。统一在 Service 里 try-catch失败也写一条日志页面上有红色标记体验完全不同。关于「中断」Quartz 的interrupt()只是给线程打个标记能不能停下来取决于任务里有没有检查。RFC 调用是阻塞的function.execute()卡在 socket 上时中断完全无效。所以页面上那个「停止」按钮实际做的是「把状态位改成停用下一轮不再调度」不要给用户「点了就立刻停」的错觉这类预期管理在交付时比代码本身还重要。4.3 执行日志、失败重试与手工触发日志表按任务维度查最近 N 条就够用没必要做复杂的聚合。MyBatis 那边写一个selectByJobId加limit配合索引idx_job_time(job_id, create_time)几十万行也不卡。手工触发走调度器不绕过RestController RequestMapping(/job) public class JobController { Autowired private Scheduler scheduler; RequiresPermissions(job:manage) PostMapping(/{id}/run) public R run(PathVariable Long id) throws SchedulerException { JobKey key JobKey.jobKey(rfc- id, RFC_JOB); // 用独立 Trigger 组手工触发顺便在 Job 里能识别来源 scheduler.triggerJob(key, new JobDataMap(Collections.singletonMap(manual, true))); return R.ok(); } }RequiresPermissions是 Shiro 的注解式校验前提是开了AuthorizationAttributeSourceAdvisor忘了开的话注解静默失效权限形同虚设这一点很难在测试中发现。前端这边用 Thymeleaf 渲染列表按钮按权限显示进度用一小段原生 JavaScript 轮询// 触发后每 2 秒查一次日志拿到最新一条就停 async function watch(jobId) { const timer setInterval(async () { const res await fetch(/job/${jobId}/latest).then(r r.json()); if (res.data res.data.finished) { // 后端用 success 字段标记 clearInterval(timer); location.reload(); } }, 2000); }轮询要有上限别写成死循环。加一个计数器超过 60 次就停并提示「任务仍在执行请稍后刷新」否则用户关页面之前会一直发请求日志表被刷得没法看。5. 避坑排查JCo 与调度里最容易翻车的五处下面这几条都是上线后真真切切踩过的按「现象 → 原因 → 解决」记一遍能省不少半夜爬起来的时间。第一条本地跑得好好的服务器上启动就报UnsatisfiedLinkError: no sapjco3 in java.library.path。原因是只部署了sapjco3.jar原生库没跟着走或者放了但没进java.library.path。解决是把libsapjco3.so放到固定目录启动参数里显式指定并确认文件权限是 755容器化部署时注意基础镜像的架构要跟原生库一致x86 和 arm 的库不通用。第二条页面上把 Cron 从「每小时」改成「每天凌晨」保存成功但任务还是按小时跑。原因是只更新了业务表的cron_expr没有重建 Quartz 的 Trigger。解决是在保存逻辑里统一走register()先deleteJob再scheduleJob同时给保存操作加一条日志写清楚「旧表达式 → 新表达式」出问题时一眼能对。顺带提醒Quartz 的QRTZ_TRIGGERS里残留的旧记录要清掉光删业务表没用。第三条任务在执行时报NotSerializableException或者取出来的参数全是 null。原因是JobDataMap里塞了非序列化对象或者塞的是当时那一刻的实体快照。解决是只传主键 ID 这类基础类型Job 里回表查最新配置。这个改法还顺带解决了「改完参数不重启不生效」的老大难。第四条个别时候 RFC 调用特别慢紧接着所有定时任务集体延后。原因是某个调用卡在网络上一直占着 Quartz 的工作线程线程池被耗尽。解决是缩短 Destination 的连接与响应超时给调度线程池设一个合理上限跟并发任务数匹配别用默认的十个再对连接类异常做一次有限重试。监控上补充「单次调用耗时超过阈值就告警」比事后翻日志主动得多。第五条中文物料描述读回来是乱码或者末尾被截断。乱码多半是JCO_LANG没设成中文或者中间某个环节用了平台默认字符集。截断则是 JCoTable 里字段的WA长度限制SAP 侧返回的字符串按声明长度做裁剪长文本要用内表分段或者换字段。解决是把原始返回值在 debug 日志里打出来对照别只看转换后的对象中间少了几百个字节是看不出来的。6. 二次开发换一套 RFC 接口要动哪几处真正落地时需求方给的往往不是一套接口而是十几套。好在骨架已经把边界划清楚了新增一套接口的改动是可以列成清单的照着填就行。改动位置具体内容容易漏的点数据库sys_job插一条任务配置param_json写入参Cron 表达式用六位还是七位先跟运维对齐字段映射field_mapping表加 RFC 字段到业务字段的对应日期、金额的格式转换规则要一起配Service新增一个方法或复用通用的invoke(rfcName, params)导出参数的表名大小写必须跟 SAP 侧一致Job通常不用改走通用的RfcSyncJob特殊逻辑多的话单独建 Job 类页面任务列表自动带出无需改模板权限串记得给对应角色加上如果新接口结构差异很大我建议别硬套通用 Service宁可多写一个XxxRfcService。抽象层次拉太高最后会变成一堆if (rfcName.equals(...))的分支比复制粘贴还难维护。验证方法上我的顺序是固定的先在测试环境用一个只读的查询函数打通链路确认连接、参数、表结构都对再用手工触发跑一次看rfc_call_log里的耗时和条数是否符合预期然后把 Cron 改成每分钟观察三轮确认不会并发叠加最后才改回正常频率交给业务侧验收。这四步一步都不能跳尤其第三步跳过之后集群里重复执行的问题往往要等到数据重复了才被发现。我印象最深的一次是改字段映射表时手滑把物料号映射到了描述字段上测试环境数据少没看出来第二天业务说「物料全变成中文名了」。从那以后我每次改映射表都强制先跑一次单条数据的对比——把 RFC 原始返回和入库结果并排打出来肉眼过一遍再放开批量。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑