
前阵子有个同行在技术群里求助金蝶s-HR要跟企业微信对接拉员工通讯录和考勤数据厂商说走OSF接口但我对着WSDL看了半天不知道怎么下手。这个问题让我想起自己第一次调s-HR OSF接口时的状态——文档写得含糊示例代码东一块西一块真正能一次跑通的没几个。OSFOpen 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-HROSF服务路径不完全一样。有的版本是/shr/osf/service/有的是/shr/osf/services/还有的是/osf/...。路径不对就404所以最好先找实施方要一份当前环境的接口说明别死记网上看来的路径。2.2 拿到完整的服务清单和WSDLOSF的服务端通常会暴露一组服务常见的有服务名典型用途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:soapenvhttp://schemas.xmlsoap.org/soap/envelope/ xmlns:corhttp://core.service.osf.shr.kingdee.com soapenv:Header/ soapenv:Body cor:login cor:userNameapi_user/cor:userName cor:passwordyour_password/cor:password cor:localezh_CN/cor:locale /cor:login /soapenv:Body /soapenv:Envelope这里的xmlns命名空间和参数名务必以你自己环境WSDL里看到的为准别照抄我写的。登录成功后返回的XML里会有一个sessionId字段把它存下来后续所有调用都要带它。密码这块要留个心眼有的版本要求明文密码有的版本要求前端加密后再传。如果明文传过去一直报用户名或密码错误而你在s-HR前台用同样的账号密码登录完全正常那基本可以断定是密码处理方式不对。去翻接口文档或者直接问实施顾问要加密规则。3.2 查询员工信息的完整SOAP报文拿到sessionId之后假设你们环境有SimpleQueryService暴露了一个executeQuery方法那查询在职员工列表的报文大概是这样soapenv:Envelope xmlns:soapenvhttp://schemas.xmlsoap.org/soap/envelope/ xmlns:serhttp://query.service.osf.shr.kingdee.com soapenv:Header/ soapenv:Body ser:executeQuery ser:sessionId8f9a2b1c3d4e5f6a7b8c9d0e/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:soaphttp://schemas.xmlsoap.org/soap/envelope/ soap:Body ns2:executeQueryResponse return result row id100001/id numberE001/number name张三/name orgName研发部/orgName statusA/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 version1.0 encodingUTF-8? soapenv:Envelope xmlns:soapenvhttp://schemas.xmlsoap.org/soap/envelope/ xmlns:serhttp://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}, databody.encode(utf-8), headers{Content-Type: text/xml; charsetutf-8, SOAPAction: } ) resp.raise_for_status() return resp.text def login(user, password): params fser:userName{user}/ser:userNameser: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 statusA params (fser:sessionId{session_id}/ser:sessionId fser: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:sessionId8f9a2b1c3d4e5f6a7b8c9d0e/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:sessionId8f9a2b1c3d4e5f6a7b8c9d0e/ser:sessionId ser:data employeeNumberE001/employeeNumber attendanceDate2024-06-11/attendanceDate checkTime09:01:23/checkTime terminalNoDEV-001/terminalNo /ser:data /ser:saveAttendRecord这里有一个关键经验保存服务一般要求传员工编码工号不是传id。所以在调用保存服务前通常要先做一次工号到id的映射查询。如果员工量很大建议一次性把全量在职员工的工号和唯一标识缓存到本地别每条打卡记录都实时去查OSF性能扛不住。另外批量保存时一定要逐条看返回结果。我踩过这个坑批量同步了几百条考勤记录接口HTTP层面返回200我看整体成功就跳过了明细结果其中十几条因为重复打卡被服务端拦截但业务异常是放在返回体里的不影响HTTP状态码。后来我改成逐条解析返回体里的成功标识和失败原因才把这个问题彻底堵住。4.3 人员异动的增量获取做数据同步最核心的设计就是增量。人员异动查询通常用时间条件来筛ser:executeQuery ser:sessionId8f9a2b1c3d4e5f6a7b8c9d0e/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特殊字符转义忘了就等着报错吧。增量同步的落地方式我建议维护一个上次同步时间字段每次只拉上次同步时间之后发生变动的数据拉完把本次最大的业务时间记录下来作为下一次起点。这样哪怕某次同步挂了下次重跑也不会漏。选时间字段时优先找lastUpdateTime、modifyTime这类最后修改时间没有的话再退而求其次用创建时间但要注意创建时间抓不到被修改的记录。如果业务表连时间字段都不理想那就只能退到全量比对。全量比对的代价是大批量查询和逐条比对性能差、耗时长能用增量就别用全量。5. 真实调用中踩过的坑与排查路径5.1 登录成功但查不到数据先查数据权限这个问题的出现频率极高。现象很统一用admin账号调OSF一切正常换专用服务账号后login能返回sessionId但查询结果集为空。很多人会去怀疑SQL写错了、对象名不对、过滤条件有问题排查半天实际原因几乎都是数据权限。s-HR的查询服务在执行你传的查询语句时会在这个语句外面再套一层权限过滤。如果服务账号没有分配任何公司的数据权限过滤条件就会把能查到的数据集筛成空。我的排查路径是在s-HR前台用这个服务账号登录手工执行一遍同样的查询看能否查到数据。如果前台也查不到基本确定是账号数据权限没配置去系统管理里给账号授权公司和组织范围。如果前台能查到但OSF查不到检查OSF服务账号是否有允许接入的开关有些版本需要在外部接口用户或服务授权里单独配置。最后才怀疑报文格式。先走这条路径别一上来就改代码。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 version1.0 encodingUTF-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 statusA) 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接口本身不复杂复杂的从来都是版本差异和文档缺失这两样东西靠实践慢慢填就是。