金蝶s-HR OSF接口调用实战:从环境准备到生产级封装
2026/9/19 2:26:20 网站建设 项目流程

前阵子有个同行在技术群里求助:"金蝶s-HR要跟企业微信对接,拉员工通讯录和考勤数据,厂商说走OSF接口,但我对着WSDL看了半天不知道怎么下手。"这个问题让我想起自己第一次调s-HR OSF接口时的状态——文档写得含糊,示例代码东一块西一块,真正能一次跑通的没几个。OSF(Open Service Framework)是金蝶s-HR对外提供的标准服务框架,基于WebService/SOAP协议,把人员、组织、考勤、薪酬这些核心业务对象封装成可调用的服务。这篇文章我就以"调用示例"为主线,把从环境确认、登录会话、查询员工/组织/考勤数据,到生产级封装和避坑的完整链路捋一遍。无论你是做OA集成、企业微信通讯录同步,还是考勤机数据回流,都可以拿这篇文章当起点。

1. 先搞清楚OSF是什么:s-HR对外集成的现实选项

1.1 为什么不能直接连数据库

很多人接手这类需求的第一反应是"直接连s-HR的数据库不就行了?"确实,s-HR底层用的SQL Server或者Oracle,表结构虽然复杂但也不是搞不到。但你在生产环境这么干,会立刻撞上三个问题。

第一是数据权限完全失控。s-HR有自己的一套数据权限体系,按公司、按组织层级、按角色来控制谁能看谁的数据。绕过应用层直连数据库,相当于把整个HR库裸奔在集成端,别说合规过不了,真出了数据泄露事故,追究起来你扛不住。第二是业务逻辑被绕过了。员工入职、转正、离职这些操作背后都有状态机和一堆校验逻辑,你直接操作数据库表,等于把s-HR的"大脑"甩在一边自己蛮干,改坏了数据,前台界面还是旧缓存,排查起来极其酸爽。第三是升级兼容性问题。s-HR打个补丁、升个版本,底层表结构可能就变了,你写的SQL全部作废重来。

OSF接口存在的意义,就是让你走"应用层的合法出入口"。它发布的是封装好的业务服务,权限过滤、日志审计、业务校验都在服务端完成,你传进去的参数、拿回来的结果都是规范的。集成性能上确实比直连SQL差一些,但换回来的是安全、规范、可持续维护。

1.2 OSF在s-HR集成体系里的位置

金蝶s-HR对外的集成通道其实有好几条:OSF接口、旧版WebService API、数据库中间表、ETL工具(Kettle/DataX)导数据、消息队列。其中OSF是主推的标准方式,它统一了服务发布和调用规范,调用方可以直接拿WSDL生成客户端,也可以像我后面这样手工拼SOAP报文。

跟旧版WebService API比,OSF更像一个"服务框架"而不只是几个固定方法。它除了系统自带的标准服务(人员查询、组织查询、考勤数据查询等),还支持实施方在s-HR侧开发自定义业务服务,把复杂的业务规则封装成一个方法暴露出来。这一点在真实项目里太有用了,很多对接需求s-HR标准服务覆盖不了,最后都是靠实施方写一个自定义OSF服务,把"查员工任职信息+返回最新部门+带出上级主管"这类组合逻辑直接做成一个接口。

1.3 哪些业务场景适合走OSF

从我经手的项目看,OSF用得最密集的是这五类:

  • 人员主数据同步:员工入职、转正、调动、离职信息同步到OA、企业微信通讯录、门禁系统、食堂消费系统。
  • 组织架构同步:公司、部门、岗位的新增和调整推送到下游系统,保证各系统组织数据一致。
  • 考勤数据交换:第三方考勤机的打卡记录写入s-HR,或者把s-HR排班数据推给考勤终端。
  • 薪酬和人事报表取数:外部报表平台通过OSF查薪酬汇总、人员花名册数据。
  • 审批流程对接:s-HR的审批单据跟OA、企业微信审批流做双向推送。

你自己判断一下需求属于哪一类,然后对应的接口文档就很好找了。如果这五类都不沾边,那大概率要评估一下OSF是不是合适的通道,别硬上。

2. 调通接口之前:环境准备与几个绕不开的前置工作

2.1 确认版本与OSF服务是否正常启动

动手写代码前,先确认三件事:s-HR版本号、OSF服务是否已部署、承载它的金蝶AAS(应用服务器)是否正常。很多人调不通第一反应是查代码,但我告诉你,OSF调不通的首因往往是服务压根没起来。

验证方法很简单,在浏览器直接访问:

http://<s-hr服务器IP>:<端口>/shr/osf/service/CoreService?wsdl

如果浏览器能返回一长串XML格式的WSDL内容,说明OSF服务在跑。如果报404或者连接超时,去AAS的部署目录看看OSF相关的应用包在不在,去日志目录看有没有启动报错。

这里有个版本差异需要注意:不同版本的s-HR,OSF服务路径不完全一样。有的版本是/shr/osf/service/,有的是/shr/osf/services/,还有的是/osf/...。路径不对就404,所以最好先找实施方要一份当前环境的接口说明,别死记网上看来的路径。

2.2 拿到完整的服务清单和WSDL

OSF的服务端通常会暴露一组服务,常见的有:

服务名典型用途
CoreService登录、登出、获取会话
SimpleQueryService通用查询,传查询语句返回结果集
SaveService通用保存,新增或更新业务数据
TreeViewQueryService树形数据查询,常用于组织架构

建议你拿到服务地址后,逐个服务把?wsdl打开看一遍,把里面暴露的方法名、参数名、命名空间记录下来,整理成一张表。这个动作别偷懒,因为不同版本、不同实施方的自定义服务差异很大,你看到的WSDL才是唯一准确的依据。

2.3 服务账号的权限配置

OSF调用需要一个s-HR系统账号。我的建议是让管理员专门创建一个服务账号,比如api_user,不要用admin。原因很简单:admin权限太大,集成端万一出问题就是全库范围的事故;而且很多s-HR版本里admin账号的数据范围是"全部",不受数据权限控制,用它调接口测不出权限问题。

账号创建好之后,必须在s-HR里给这个账号配两样东西:一是业务对象的查询权限(比如员工、组织单元、考勤记录),二是数据范围(比如指定公司或全部公司)。缺了任何一个,都会出现"登录成功但查询结果为空"的现象。

2.4 推荐先用的调试工具

调WebService接口,我手里最顺手的工具还是SoapUI。把WSDL地址丢进去,自动生成测试用例,填参数就能发请求,返回报文结构看得清清楚楚。我一般先拿SoapUI把登录、查询、保存这几个关键调用全打通,确认报文格式没问题,再动手写代码。

不方便装SoapUI的环境,用Postman也能凑合——新建请求,URL填服务地址,Body选raw、XML格式,直接贴报文发出去。总之在代码里调试XML绝对是地狱难度,先拿工具把报文长什么样搞清楚,后面能省一半时间。

3. 从登录到第一次查询:核心服务调用全过程

3.1 调用登录服务获取会话

OSF的服务大部分都要求先登录,拿到sessionId之后才能做查询和保存。登录调用一般是发到CoreService,报文长这样:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cor="http://core.service.osf.shr.kingdee.com"> <soapenv:Header/> <soapenv:Body> <cor:login> <cor:userName>api_user</cor:userName> <cor:password>your_password</cor:password> <cor:locale>zh_CN</cor:locale> </cor:login> </soapenv:Body> </soapenv:Envelope>

这里的xmlns命名空间和参数名,务必以你自己环境WSDL里看到的为准,别照抄我写的。登录成功后,返回的XML里会有一个sessionId字段,把它存下来,后续所有调用都要带它。

密码这块要留个心眼:有的版本要求明文密码,有的版本要求前端加密后再传。如果明文传过去一直报"用户名或密码错误",而你在s-HR前台用同样的账号密码登录完全正常,那基本可以断定是密码处理方式不对。去翻接口文档,或者直接问实施顾问要加密规则。

3.2 查询员工信息的完整SOAP报文

拿到sessionId之后,假设你们环境有SimpleQueryService,暴露了一个executeQuery方法,那查询在职员工列表的报文大概是这样:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://query.service.osf.shr.kingdee.com"> <soapenv:Header/> <soapenv:Body> <ser:executeQuery> <ser:sessionId>8f9a2b1c3d4e5f6a7b8c9d0e</ser:sessionId> <ser:queryString> select e.id, e.number, e.name, o.name as org_name, e.status from employee e left join org_unit o on e.org_id = o.id where e.status = 'A' </ser:queryString> </ser:executeQuery> </soapenv:Body> </soapenv:Envelope>

响应报文结构一般是这样:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <ns2:executeQueryResponse> <return> <result> <row> <id>100001</id> <number>E001</number> <name>张三</name> <orgName>研发部</orgName> <status>A</status> </row> <!-- 更多row --> </result> </return> </ns2:executeQueryResponse> </soap:Body> </soap:Envelope>

不同版本返回字段的大小写、层级可能都不一样,有的版本把结果集封装成<record>节点。所以第一次拿到响应后,第一件事是把原始XML完整打印出来看结构,再写解析代码,不要想当然。

3.3 用Python直接拼报文跑通全流程

如果你的集成项目用Python,其实不需要引第三方SOAP库,直接拿requests拼报文就够了。原因很简单:OSF报文结构固定,拼接可控,少一个依赖少一个坑。下面是我项目里验证过的一套流程:

import requests import xml.etree.ElementTree as ET BASE_URL = "http://192.168.10.10:8080/shr/osf/service" def call_service(service_name, method, params): body = f"""<?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://query.service.osf.shr.kingdee.com"> <soapenv:Header/> <soapenv:Body> <ser:{method}> {params} </ser:{method}> </soapenv:Body> </soapenv:Envelope>""" resp = requests.post( f"{BASE_URL}/{service_name}", data=body.encode("utf-8"), headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": ""} ) resp.raise_for_status() return resp.text def login(user, password): params = f"<ser:userName>{user}</ser:userName><ser:password>{password}</ser:password>" resp_xml = call_service("CoreService", "login", params) root = ET.fromstring(resp_xml) session_id = root.find(".//sessionId").text return session_id def query_employees(session_id): sql = "select id, number, name from employee where status='A'" params = (f"<ser:sessionId>{session_id}</ser:sessionId>" f"<ser:queryString><![CDATA[{sql}]]></ser:queryString>") return call_service("QueryService", "executeQuery", params) if __name__ == "__main__": sid = login("api_user", "your_password") print(query_employees(sid))

这段代码有三个细节值得单独说明。

第一,SOAPAction头一定要加,哪怕是空字符串。很多金蝶的WebService服务不校验这个头的值,但某些代理服务器或者防火墙会拿它做过滤,不加就可能报500或者请求被拦。

第二,SQL里有<>&这类XML特殊字符时,必须用<![CDATA[...]]>包起来。我在写时间范围查询时吃过这个亏,比如where create_date > '2024-01-01'里面的>直接让XML解析崩掉,排查了半小时才反应过来是报文格式问题。

第三,登录这类方法返回的sessionId节点路径,不同版本有差异,root.find(".//sessionId")这段需要根据实际返回结构调整。

3.4 解析响应的通用套路

OSF返回的XML结构经常嵌套多层,而且带着命名空间前缀,直接按路径取值容易崩。我在项目里用了一段通用解析代码,思路是遍历所有节点、去掉命名空间前缀、按标签名提取:

def xml_to_rows(xml_str): root = ET.fromstring(xml_str) rows = [] for elem in root.iter(): tag = elem.tag.split('}')[-1] if tag == 'row': row = {} for child in elem: key = child.tag.split('}')[-1] row[key] = child.text rows.append(row) return rows

这段代码虽然粗暴,但对付OSF这种字段层级不固定的响应很管用。你只需要根据实际响应调整row这个标签名,就能适配不同服务的返回结构。

4. 高频业务场景的调用示例:组织、考勤、异动

4.1 组织单元查询

组织架构是几乎所有下游系统的刚需,同步顺序一般是"先组织后人员",因为人员数据要挂在组织节点下。查询组织单元的报文和员工查询类似,差别在查询对象和过滤条件:

<ser:executeQuery> <ser:sessionId>8f9a2b1c3d4e5f6a7b8c9d0e</ser:sessionId> <ser:queryString> select id, number, name, parent_id, org_type from org_unit where org_type = 'DEPARTMENT' and status = 'A' </ser:queryString> </ser:executeQuery>

这里要重点提醒:OSF查询服务里的"表名"不是数据库物理表名,而是s-HR业务对象名。我刚接触时拿数据库真实表名去查,直接被报"对象不存在"。正确做法是到s-HR前台的查询引擎(或者查询方案管理)里,看标准查询用的业务对象名是什么,照着抄过来。这个方法我用了很多次,基本不会错。

组织架构同步还有一个容易忽略的点:组织是有层级的,你同步到下游系统时不仅要拿到部门本身的数据,还要把parent_id父子关系一起同步过去,否则下游系统的组织树就是一堆散落的节点,没法用。

4.2 考勤数据写入

考勤机打卡记录回流到s-HR,是另一种典型场景。这里要注意,不是往刷卡数据表里直接insert,而是调OSF的保存服务,让s-HR走一遍自己的校验逻辑。

保存服务报文大致这样:

<ser:saveAttendRecord> <ser:sessionId>8f9a2b1c3d4e5f6a7b8c9d0e</ser:sessionId> <ser:data> <employeeNumber>E001</employeeNumber> <attendanceDate>2024-06-11</attendanceDate> <checkTime>09:01:23</checkTime> <terminalNo>DEV-001</terminalNo> </ser:data> </ser:saveAttendRecord>

这里有一个关键经验:保存服务一般要求传员工编码(工号),不是传id。所以在调用保存服务前,通常要先做一次"工号到id"的映射查询。如果员工量很大,建议一次性把全量在职员工的工号和唯一标识缓存到本地,别每条打卡记录都实时去查OSF,性能扛不住。

另外,批量保存时一定要逐条看返回结果。我踩过这个坑:批量同步了几百条考勤记录,接口HTTP层面返回200,我看整体成功就跳过了明细,结果其中十几条因为"重复打卡"被服务端拦截,但业务异常是放在返回体里的,不影响HTTP状态码。后来我改成逐条解析返回体里的成功标识和失败原因,才把这个问题彻底堵住。

4.3 人员异动的增量获取

做数据同步,最核心的设计就是增量。人员异动查询通常用时间条件来筛:

<ser:executeQuery> <ser:sessionId>8f9a2b1c3d4e5f6a7b8c9d0e</ser:sessionId> <ser:queryString> select id, number, name, change_type, change_date from employee_change where change_date &gt;= '2024-06-01 00:00:00' </ser:queryString> </ser:executeQuery>

注意我写的是&gt;=,不是>=,这就是XML特殊字符转义,忘了就等着报错吧。

增量同步的落地方式,我建议维护一个"上次同步时间"字段,每次只拉上次同步时间之后发生变动的数据,拉完把本次最大的业务时间记录下来作为下一次起点。这样哪怕某次同步挂了,下次重跑也不会漏。选时间字段时,优先找lastUpdateTimemodifyTime这类最后修改时间,没有的话再退而求其次用创建时间,但要注意创建时间抓不到"被修改"的记录。

如果业务表连时间字段都不理想,那就只能退到全量比对。全量比对的代价是大批量查询和逐条比对,性能差、耗时长,能用增量就别用全量。

5. 真实调用中踩过的坑与排查路径

5.1 登录成功但查不到数据:先查数据权限

这个问题的出现频率极高。现象很统一:用admin账号调OSF一切正常,换专用服务账号后login能返回sessionId,但查询结果集为空。很多人会去怀疑SQL写错了、对象名不对、过滤条件有问题,排查半天,实际原因几乎都是数据权限。

s-HR的查询服务在执行你传的查询语句时,会在这个语句外面再套一层权限过滤。如果服务账号没有分配任何公司的数据权限,过滤条件就会把能查到的数据集筛成空。

我的排查路径是:

  1. 在s-HR前台用这个服务账号登录,手工执行一遍同样的查询,看能否查到数据。
  2. 如果前台也查不到,基本确定是账号数据权限没配置,去系统管理里给账号授权公司和组织范围。
  3. 如果前台能查到但OSF查不到,检查OSF服务账号是否有"允许接入"的开关,有些版本需要在"外部接口用户"或"服务授权"里单独配置。
  4. 最后才怀疑报文格式。

先走这条路径,别一上来就改代码。

5.2 时间格式不统一导致查询结果为空

OSF查询服务对时间参数的处理,不同版本差别很大。有的要求yyyy-MM-dd HH:mm:ss,有的要求yyyy-MM-dd,还有的更诡异,必须带时区。如果查询条件里用了时间字段,先确认s-HR的日期格式设置,再看WSDL里的类型定义,两头对齐。

我遇到最坑的一次,用2024-06-01 00:00:00查不到任何数据,改成2024-06-01就有了。原因是s-HR把字符串按yyyy-MM-dd解析,追加的00:00:00反而让解析出错,被当成非法条件忽略了。所以时间参数务必跟接口文档保持一致,别自己发挥。

5.3 第三方系统引用不了WSDL时的临时方案

不是所有开发环境都能顺利从WSDL生成SOAP客户端,尤其是一些老旧的异构系统(Delphi、PowerBuilder这类),对WebService的兼容性非常差。这时候别死磕,直接拼XML报文发HTTP POST。

拼报文时注意三点:

  • <soapenv:Envelope>的命名空间必须和WSDL里的保持一致。
  • Body里每个参数标签的前缀(如ser:)要对应正确的命名空间。
  • 别忘了<?xml version="1.0" encoding="UTF-8"?>声明,尤其是请求里带中文时,少了这个声明编码可能乱。

这个方法虽然原始,但兼容性最好——任何语言只要能发HTTP请求就能调,完全绕开了SOAP客户端生成的麻烦。

5.4 大批量查询的性能与分页策略

OSF查询服务对单次返回的数据量是有限制的,有的版本上限5000行,超过就截断或者直接报错。所以生产脚本上线前,先用小批量验证一下当前环境的限制。

如果确实要分页,我建议用条件分批而不是传统翻页。比如按部门分批、按入职年份分批,每次查一小批,比LIMIT/OFFSET翻页更稳。因为翻页时如果数据源有数据变动(新增或删除),很容易出现重复行或漏行。条件分批就没有这个问题,每批之间互不干扰,任何一批失败都可以单独重跑。

如果查询对象支持窗口函数,也可以用行号窗口分页,但能不能用取决于底层数据库是SQL Server还是Oracle,需要实测。

6. 从"能调通"到"生产可用":几个值得提前做的设计

6.1 封装一个统一调用层

不管用什么语言写集成,强烈建议把OSF调用封装成一个公共模块。对外只暴露业务方法(查员工、查组织、推送考勤、同步异动),对内统一处理登录、会话缓存、异常重试、日志记录。这样下游业务代码不会到处散落SOAP报文和XML解析逻辑,出了问题也只改一个地方。

用Python封装完,调用方的代码大概是这样的:

client = ShrOsfClient(config) rows = client.query("select id, number, name from employee where status='A'") result = client.save_attendance(records)

调用方不需要关心sessionId怎么拿、报文怎么拼、返回怎么解析。这个封装的收益随着项目复杂度提升会越来越明显,尤其是接了下游七八个系统的时候,你肯定不想每个系统各写一份拼报文的代码。

6.2 会话管理与自动重连

OSF的sessionId一般有时效,长时间不调用就失效。如果每次调用都重新登录,性能太差;不重登又会中间失败。建议做一个简单的会话管理器:缓存当前sessionId,每次调用前检查是否有效,失效就自动重新登录。

判断失效有两个办法:

  • 每次调用前先发一个轻量请求试探会话,失效再重新登录。缺点是每次多一次网络往返。
  • 捕获返回里"会话无效"的错误码,遇到后重新登录并重试刚才那次调用。这个方式省一次请求,我更推荐。

但要注意,自动重登后重试时,请求里必须带上新的sessionId,别把旧的拼进去。

6.3 日志、监控与数据一致性校验

联调阶段问题最多,日志一定要打全。我在项目里定的规矩是:每个请求记录请求报文摘要(密码等敏感字段脱敏)、响应状态、耗时、返回行数。这样出了问题可以快速定位是网络问题、参数问题还是s-HR服务端问题。

数据同步任务建议再加一道"对账"环节。比如每天同步完员工数据后,跑一个总数对比:OSF查出来的在职员工总数,对一下下游系统里的在职员工总数,对不上就告警。这种对账脚本不复杂,但能帮你抓住很多隐形问题,比如某次同步任务半路挂了、部分数据没写进去。

6.4 与金蝶其他产品线的联动思路

顺便说一句,金蝶系产品的集成理念是相通的。你在s-HR上摸清的OSF套路,到金蝶云星空上虽然服务名、报文格式变了,但"登录拿会话、按业务对象查询、走保存服务写入"这套思路完全可以平移。金蝶云星空对接企业微信、云星空里的生产领料和自制转委外单证同步,本质上都是同一个套路。而金蝶AAS作为OSF的承载容器,它一旦出问题,表现就是接口无响应、服务列表打不开,所以监控AAS本身的状态也很重要,别等接口挂了才想起来看中间件。

最后分享一个我个人的习惯:每次做s-HR对接,先把WSDL里所有方法名和参数导成一张表贴在项目文档里,然后按"登录-查询-保存"的顺序逐个验证,每验证通过一个就打勾。这个笨办法看起来不起眼,但能让你在集成项目里少踩一半的坑。OSF接口本身不复杂,复杂的从来都是版本差异和文档缺失,这两样东西,靠实践慢慢填就是。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询