资讯详情 云ERP WebAPI接口对接:鉴权、单据保存与查询字段映射
📅 2026/10/23 14:44:04
简介这份资料包面向需要接入金蝶云星空新版WebAPI的Java、.NET与Python开发者覆盖从环境搭建到接口调试的常见需求适合刚接触星空开放平台、希望快速跑通测试工程与SDK示例的技术人员。压缩包共49个文件约6.93MB包含dll、cs、csproj、sln等.NET工程与依赖文件jar、java、class、properties等Java组件以及whl、config、txt、docx、pptx等安装包、配置说明和文档材料便于按技术栈查找示例与参考资料。内容围绕.NET、Python、Java快速搭建开发测试环境的新手入门指南及配套测试工程/SDK展开并附其他操作指南可帮助读者理解项目结构、配置方式与调用流程减少接口接入和排错时的试错成本。目前已有1206人学习下载适合作为金蝶云星空WebAPI开发入门的查阅型资料包。1. 从一个报销单推送需求说起这份 WebAPI 资料包解决什么问题上周有个做实施的朋友扔过来一个压缩包说财务那边每天要把报销单手工导成 Excel 再录进云 ERP十几个人一个月对不上三次账问我能不能直接对接。这类需求在云 ERP 项目里太常见了——前端业务系统已经跑起来了缺的就是一层稳定的 WebAPI 调用。他发来的就是这份新版 WebAPI 资料包里面把鉴权、单据保存、提交审核、查询、附件、消息回调这几条主线全拆开了配套给了 Python、Java、C# 三套示例。它解决的核心问题不是「有没有接口」而是「接口怎么串起来不出错」。云 ERP 的接口是强 Schema 的字段名、分录结构、基础资料引用方式错一个字返回的就是一条没有堆栈的业务错误。这份资料包的价值在于把表单标识、字段命名、公共参数、错误码这些原本散落在各处的信息收拢成一份能直接照着抄的清单。适合三类人做第三方系统对接的后端、需要评估集成工作量的实施顾问、以及给低代码平台写连接器的开发者。2. 资料包结构拆解鉴权、单据、查询三条主线的接口分层2.1 目录骨架五层文件夹各管什么解压之后先别急着翻接口文档把目录扫一遍能省掉后面大量瞎找。整个包按「先跑通、再查细节、最后兜底」的顺序组织五个文件夹的职责分得很清楚新版WebAPI资料包/ ├── 01_快速开始/ # 环境准备、签名规则、Postman 集合先看这里 │ ├── 环境准备与租户申请.md │ ├── 鉴权与签名说明.md │ └── 接口调试集合.postman_collection.json ├── 02_接口文档/ # 按服务域切分一个文件对应一类接口 │ ├── 认证服务.md │ ├── 单据操作_保存提交审核删除.md │ ├── 单据查询与报表.md │ ├── 附件与文件服务.md │ └── 消息订阅与回调.md ├── 03_示例代码/ # 三套语言的等价实现用来对照字段结构 │ ├── python/ │ ├── java/ │ └── csharp/ ├── 04_字段与元数据/ # 最容易踩坑的部分FormId 和字段 Key 清单 │ ├── 常用表单FormId清单.xlsx │ └── 单据体字段命名规范.md └── 05_附录/ ├── 错误码对照表.md └── 常见问题.md04_字段与元数据这个目录最容易被忽略但它决定了后面一半的工作量。云 ERP 的表单是元数据驱动的同一张单据在不同租户下可能有自定义字段字段 Key 也不是业务名称而是F开头的内部标识。先在常用表单FormId清单.xlsx里确认你要操作的表单标识再去平台后台的元数据里导出字段列表比对着猜字段名靠谱得多。03_示例代码建议当参考而不是当依赖。示例为了可读性把参数都写死了也没有重试、没有连接池、没有错误分类直接搬进生产环境大概率会在并发场景下翻车。它的正确用法是对照字段结构——比如某个分录字段到底该传{FNumber: ...}还是裸字符串看示例里的 JSON 最快。提示先把01_快速开始里的 Postman 集合导入跑一遍确认网络能通、账号能登录、返回结构长什么样再动手写代码。跳过这一步直接写客户端往往会在「到底是网络问题还是参数问题」上耗掉半天。2.2 接口能力矩阵覆盖 80% 对接需求的九类接口资料包里的接口看着多真正高频的就这么几类。我把它们按调用频率排了个序新接手项目时照着这张表规划工作量能力典型接口主要用途返回重点鉴权ValidateUser / LoginByAppSecret换取会话后续请求带 Cookie会话标识、过期时间元数据QueryBusinessInfo查表单有哪些字段、字段类型字段 Key、类型、是否必填保存Save新增或更新单据含分录单据内码 Id、单据编号提交审核Submit / Audit / UnAudit走审批流是否成功、失败原因删除Delete作废错误单据删除结果查询ExecuteBillQuery按条件批量拉单据二维数组详情View按 Id 拉单张完整单据完整 Model下推Push源单生成目标单目标单 Id附件UploadFile上传发票、合同扫描件文件标识实际项目里的调用顺序基本都是「鉴权 → 查询 → 保存 → 提交 → 审核」这条链。真正需要反复调试的是 Save 和 ExecuteBillQuery 两个前者字段结构复杂后者参数组合多。Submit 和 Audit 反而简单只要 Id 传对返回基本就是成功或一条明确的流程错误。资料包里还单独有一份消息订阅文档讲的是平台主动往你的回调地址推事件比如单据审核通过、库存变更。这条链路适合做增量同步能省掉轮询查询的开销。但它对幂等性的要求比主动调用高得多——平台重推是常态回调地址必须能扛住重复消息。2.3 公共请求骨架所有接口共用的 parameters 外壳看完接口清单会发现所有接口的请求体长得很像都是一个固定外壳包着业务参数{ parameters: [ 表单标识或参数1, 参数2, 参数3 ] }这个parameters是位置参数数组不是对象。顺序错了不会报「参数缺失」而是给你一条莫名其妙的类型转换错误这是新手最容易卡住的地方。资料包的每个接口文档里都标了参数顺序抄的时候别自己重排。外层还挂着两个东西请求头和公共配置。请求头里放应用标识、时间戳、签名公共配置里是租户账号、语言标识一般传2052表示简体中文、组织编码。语言标识不传或者传错返回的字段名称会变成英文甚至直接返回空这个现象很像「接口没权限」实际只是国际化参数没给。# 公共请求头三个字段缺一不可签名算法见 3.1 COMMON_HEADERS { Content-Type: application/json; charsetutf-8, X-Api-AppId: APP_ID, # 应用标识平台后台分配 X-Api-Timestamp: , # 毫秒时间戳每次请求重新生成 X-Api-Sign: , # 由 appId appSecret timestamp 算出 }charsetutf-8显式写上别依赖默认值。有些网关会把没声明编码的请求按 GBK 解析中文字段比如备注、客户名称就变成乱码存进库事后排查极难定位。这个坑我在两个项目里都遇到过属于典型的「能跑通但有脏数据」的隐性故障。3. 跑通第一条链路从 ValidateUser 鉴权到 Save 保存一张单据3.1 鉴权与签名三个 Header 参数决定你能不能进门云 ERP 的鉴权分两层应用层验签确认调用方身份用户层登录确认数据权限。应用层靠三个 Header 完成签名算法是把应用标识、密钥、毫秒时间戳按顺序拼起来做一次 SHA1import hashlib import time import requests BASE https://your-tenant-host # 换成自己的租户域名 APP_ID your_app_id APP_SECRET your_app_secret def build_sign(app_id: str, app_secret: str, ts: str) - str: # 拼接口径appId appSecret timestamp中间不加分隔符 raw f{app_id}{app_secret}{ts} return hashlib.sha1(raw.encode(utf-8)).hexdigest() def build_headers() - dict: ts str(int(time.time() * 1000)) # 必须是毫秒秒级时间戳会验签失败 return { Content-Type: application/json; charsetutf-8, X-Api-AppId: APP_ID, X-Api-Timestamp: ts, X-Api-Sign: build_sign(APP_ID, APP_SECRET, ts), }三个参数各自有坑。X-Api-Timestamp用的是毫秒服务器会校验时间戳和自身时间的偏差一般允许几分钟机器时钟漂移大了直接拒绝。X-Api-Sign是对拼接串做 SHA1 后取小写十六进制别顺手写成大写或者 Base64。X-Api-AppId和密钥必须成对换了应用标识密钥也要跟着换否则报的是「应用不存在」而不是「签名错误」容易误判。用户层登录有两个入口资料包里都给了示例。走账号密码的方式适合人工调试走应用密钥免密登录适合服务端定时任务——后者不需要在代码里存明文密码安全性更好。登录成功后返回体里会带会话标识通过 Cookie 下发def login(session: requests.Session, acct: str, user: str, pwd: str) - dict: payload { # 位置参数租户账号、用户名、密码、语言标识 parameters: [acct, user, pwd, 2052], } resp session.post( f{BASE}/api/AuthService/ValidateUser, jsonpayload, headersbuild_headers(), timeout10, ) resp.raise_for_status() body resp.json() if not body.get(LoginResultType) 1: # 1 表示登录成功其余为失败码 raise RuntimeError(f登录失败: {body}) return body这里的关键是必须用requests.Session()而不是每次requests.post。会话 Cookie 存在 Session 对象里后续所有业务请求复用同一个实例才不会每次都重新登录。会话有有效期过期后业务接口返回的不是 401 而是一条IsSuccess: false的业务错误所以客户端里要专门识别「会话失效」这一类返回码并自动重登一次。3.2 保存单据Model 里哪些字段是必填基础资料怎么传登录拿到会话之后第一件事通常是保存一张单据。Save 接口的参数结构是「表单标识 控制选项 Model」三段def save_bill(session: requests.Session, form_id: str, model: dict) - dict: payload { parameters: [ form_id, # 表单标识从 FormId 清单里查 { NeedUpDateFields: [], # 只更新指定字段空表示整体覆盖 NeedReturnFields: [], # 需要返回的字段空表示按默认 IsDeleteEntry: true, # 分录整体替换而非追加 IsVerifyBaseDataField: false, # 基础资料是否做存在性校验 IsEntryBatchFill: true, # 分录批量填充减少往返 ValidateFlag: true, # 走平台字段校验 NumberSearch: true, # 允许按编码匹配基础资料 IsAutoAdjustField: false, Model: model, }, ], } resp session.post( f{BASE}/api/BillService/Save, jsonpayload, headersbuild_headers(), timeout30, ) return resp.json()控制选项里最需要理解的是IsDeleteEntry。设成true时传入的分录会整体替换库里已有的分录设成false时是追加。做「修改单据」场景时如果不显式设成true原本三条分录的单据改完会变成六条而且不报错——这是最隐蔽的一类数据问题。Model 的写法是重点。单据头字段直接放平级基础资料字段用嵌套对象按编码引用分录是数组model { FBillNo: , # 留空由平台按编码规则生成 FDate: 2024-05-20, # 日期只认 yyyy-MM-dd FBillTypeID: {FNumber: EXPENSE01}, # 单据类型基础资料 FCreateOrgId: {FNumber: ORG001}, # 创建组织基础资料 FNote: 5月差旅报销-华东区, FEntity: [ # 单据体分录 { FEntryNote: 高铁票 5.18, FExpenseItem: {FNumber: TRAVEL_TRAIN}, FAmount: 553.50, FCurrencyId: {FNumber: CNY}, FDATE: 2024-05-18, }, ], }几个必守的规则基础资料字段一定传{FNumber: 编码}对象直接传字符串CNY大部分场景能过但遇到禁用的基础资料就会静默失败金额和数量传数字类型别传字符串否则会出现精度截断日期统一yyyy-MM-dd带时间的字段才用yyyy-MM-dd HH:mm:ss。分录数组的 Key 名必须和元数据里的一致FEntity只是最常见的一个不同单据可能是FDetailEntity、FExpenseEntity。注意NeedReturnFields留空时返回体里没有完整单据结构只有内码和编号。如果保存后马上要拿字段做后续逻辑把需要的字段名显式列进去能省一次 View 调用。3.3 封装可复用客户端超时、重试、日志三件套一次调用能跑通和生产可用之间还差一个封装。资料包的示例代码是一次性脚本缺了超时、重试和日志我按实际项目里的做法补一下import logging import time class ErpClient: def __init__(self, base, app_id, app_secret, acct, user, pwd): self.session requests.Session() self.base base self.auth (app_id, app_secret, acct, user, pwd) self.logger logging.getLogger(erp) self._logged_in False def _ensure_login(self): if self._logged_in: return login(self.session, *self.auth[2:]) self._logged_in True def call(self, path: str, payload: dict, retry: int 2) - dict: self._ensure_login() for attempt in range(retry 1): try: resp self.session.post( f{self.base}{path}, jsonpayload, headersbuild_headers(), timeout30, ) body resp.json() except requests.Timeout: self.logger.warning(调用超时 path%s attempt%d, path, attempt) time.sleep(2 ** attempt) # 指数退避避免雪崩 continue # 会话过期统一由业务返回码标识重登一次再试 if is_session_expired(body): self._logged_in False self._ensure_login() continue return body raise RuntimeError(f调用失败: {path})_ensure_login做成懒加载第一次调用时才登录避免服务启动期间平台不可用导致进程起不来。重试只对超时和会话失效生效业务错误比如字段校验不通过绝对不能重试——重试只会让错误日志翻倍还可能产生重复单据。退避用2 ** attempt秒第一次 1 秒第二次 2 秒比固定间隔稳。日志里一定要记完整的请求和响应体但要在写日志前把密码字段和密钥抹掉。这份日志在排查「为什么这条单据没进系统」时是唯一的黑匣子光记一个状态码什么都查不出来。4. 核心模块拆解ExecuteBillQuery 查询与单据体字段映射4.1 FieldKeys 与 FilterString查询语句的两个核心参数保存解决了「写」查询解决「读」和「对账」。ExecuteBillQuery 的参数是位置式的顺序不能改def query_bills(session, form_id, field_keys, filter_str, order_strFBillNo ASC, top0, start0, limit2000): payload { parameters: [ form_id, # 表单标识 field_keys, # 逗号分隔的字段列表分录字段用下划线连接 filter_str, # 过滤条件SQL 片段风格 order_str, # 排序字段 top, # 0 表示不限非 0 时等同于取前 N 条 start, # 起始行号分页用 limit, # 单次返回上限平台侧有硬限制 ], } resp session.post( f{BASE}/api/BillService/ExecuteBillQuery, jsonpayload, headersbuild_headers(), timeout60, ) return resp.json()field_keys的写法是这套接口最容易错的地方。单据头字段直接写FBillNo,FDate分录字段要用「单据体标识_字段标识」的形式比如FEntity_FAmount。多个单据体的字段混在一个查询里返回的顺序就是字段列表的顺序靠位置取值。filter_str是 SQL 片段风格字符串要用单引号包起来日期也必须带引号FDate 2024-05-01 AND FDocumentStatus C。这里的坑在于状态值不是可读文本而是编码C表示已审核、A表示创建、B表示审核中这些编码在04_字段与元数据的说明里有对照表。传错状态值不报错只是查不出数据。分页要自己控制。limit单次别超过平台上限start按limit递增循环拉取直到返回条数为 0 或者小于limit为止。取数顺序必须带一个有唯一性的排序字段FID或FBillNo否则翻页时可能重复或者漏行——数据库层面没有稳定排序分页结果就是不确定的。4.2 FNumber、FId、FEntryID基础资料与分录的三角关系这三个概念搞不清楚后面所有字段问题都会变成玄学。简单说FNumber是业务编码人有可读性适合跨系统传输比如CNY、ORG001。新增单据时用它引用基础资料。FId是数据库内码全局唯一但不稳定适合做本地映射表的主键。FEntryID是分录行的内码只在单据内部唯一用于定位「改哪一行分录」。新增场景一律传FNumber。修改场景就要区分整体覆盖分录时不用FEntryID只改某几行分录时必须带上FEntryID加上NeedUpDateFields指定字段否则平台会按新增处理结果就是分录越改越多。# 只更新第 2 行分录的金额其余字段和分录保持不动 update_model { FID: 100234, # 单据内码更新必须带 FEntity: [ { FEntryID: 200871, # 分录内码定位到具体行 FAmount: 680.00, }, ], } payload_opt { NeedUpDateFields: [FEntity.FAmount], # 只更新这一个字段 IsDeleteEntry: false, # 关键不要整体替换 }NeedUpDateFields的写法是「单据体标识.字段标识」和查询里的下划线连接是两套规则这点资料包的示例代码里体现得不明显只能从文档的接口说明里抠出来。做跨系统对接时我一般会维护一张映射表外部系统主键、平台FID、平台FBillNo、最后一次同步时间、同步状态。有了它增量同步、幂等判断、问题回溯都有依据不用每次都靠单号去猜。4.3 返回结构解析ResponseStatus 的三层与二维数组的列映射Save、Submit 这类写接口的返回是标准的嵌套结构三层看下来就能定位问题{ Result: { ResponseStatus: { IsSuccess: false, Errors: [ { FieldName: FEntity.FAmount, Message: 第 1 行分录的金额不能为空, DIndex: 0, ErrorCode: 500 } ], SuccessEntitys: [] }, NeedReturnData: [] } }IsSuccess是一级开关false 时必须遍历Errors。FieldName直接指向出错字段DIndex是分录下标从 0 开始这两个字段配合能把错误精确落到某一行某一列。ErrorCode用来做分类处理字段校验类的错误重试没意义直接进人工队列会话过期、限流这类才值得重试。最外层有时还有一层Result有时没有取决于接口版本。稳妥的写法是先判Result in body再往下取别硬编码路径。查询接口的返回完全是另一个形状——直接给二维数组第一行数据对应field_keys的第一个字段def rows_to_dicts(rows, field_keys): keys field_keys.split(,) return [dict(zip(keys, row)) for row in rows] # 调用示例 field_keys FID,FBillNo,FDate,FDocumentStatus,FEntity_FAmount data query_bills(session, EXPENSE_BILL, field_keys, FDate 2024-05-01 AND FDocumentStatus C) print(data[Result][RowCount]) # 本次返回条数 for item in rows_to_dicts(data[Result][Rows], field_keys): print(item[FBillNo], item[FEntity_FAmount])二维数组没有列名字段顺序完全靠field_keys的位置保证。所以field_keys不要用变量拼接写成一个常量放在模块顶部取数和映射共用同一份改的时候只改一处。返回的数字字段可能是字符串做金额计算前统一转Decimal用float累加会出现分位误差对账时对不上就是这么来的。5. 避坑与排查签名、登录态、分录覆盖五个高频翻车点5.1 认证与连接层三个最费时间的坑现象本地 Postman 能调通服务端部署后固定报签名错误。原因服务器时钟存在漂移或者容器里时区设置成了 UTC生成的时间戳和平台侧差了几分钟超出允许偏差直接被拒。 解决先date看一眼服务器时间和标准时间比对容器里显式设置时区时间戳统一用 UTC 毫秒再转字符串。签名失败时不要反复改算法九成是时间戳问题。现象单条调用正常压测或者定时任务跑一会儿就开始大批量失败。原因多个线程共用一个Session对象Cookie 在并发写入时被覆盖会话状态被打乱。 解决每个线程一个Session实例或者用连接池配合无状态的应用密钥登录方式。共享一个 Session 在低并发下看不出问题上量之后就是随机失败非常难复现。现象调用超时后重试系统里出现了两张一样的单据。原因第一次请求其实已经成功写库只是响应在路上超时了客户端重试又提交了一次。 解决业务侧做幂等用外部单号作为唯一键保存前先按单号查一次或者用「先查后存」的补偿逻辑。写接口的重试必须配合幂等这不是可选项。5.2 单据数据层两个静默失败的坑现象修改单据后分录数量翻倍但接口返回IsSuccess: true。原因IsDeleteEntry设成了false或者没传平台按追加处理。 解决修改场景显式传IsDeleteEntry: true如果只想改某几行就带FEntryID配合NeedUpDateFields走局部更新路径。这个参数的行为差异一定写进团队规范。现象基础资料字段传对了编码单据保存成功但后续报表统计不到数据。原因基础资料在目标组织下是禁用状态IsVerifyBaseDataField为false时校验被跳过脏数据就这么进去了。 解决联调阶段把IsVerifyBaseDataField设成true让问题在测试环境暴露上生产前再评估性能影响数据量大的单据可以只在关键字段上开。另外组织字段FCreateOrgId一定要确认对跨组织取数时基础资料的可见范围是按组织隔离的。5.3 排查手册三条命令定位 90% 的报错遇到报错别急着翻文档按这个顺序走一遍大部分问题能自己定位报错特征先看哪里具体动作签名/应用相关请求头打印三个 Header核对时间戳位数和签名大小写登录相关会话对象打印session.cookies看是否有会话标识字段校验失败Errors 数组看FieldName和DIndex直接定位到行列返回空数据filter_str把条件拆成单条件逐个试确认状态编码传对中文乱码Content-Type确认 charset 为 utf-8检查库表字符集最有效的一招是拿单个最小请求去 Postman 里手工复现。把客户端里经过层层封装的参数原样打印成 JSON粘到 Postman 里发一次能通问题在客户端封装不通问题在参数或权限。这一步能砍掉一半排查时间。6. 二次开发把文档包改造成团队自己的对接层资料包给的是接口清单团队真正需要的是一个稳定的对接层。我的做法是分四层落地client只负责签名、会话、重试、日志dto按表单建数据类把字段名收敛成常量mapper做外部系统和平台字段的双向转换service写业务编排比如「新增报销单 → 提交 → 审核 → 回写状态」。分层之后换租户、加字段、改字段类型都只动一层。配套必须建两张表。一张是单据映射表字段包括外部主键、FID、FBillNo、同步状态、最后同步时间用来做幂等和增量一张是调用日志表记请求摘要、响应错误码、耗时用来做问题回溯和限流分析。这两张表加起来不到二十个字段但能把「线上出了问题只能靠猜」变成「查表就知道卡在哪一步」。验证方法上我一般准备三层回归。第一层是单接口冒烟用 Postman 集合跑一遍登录、保存、查询、提交、审核第二层是状态流转构造一张单据从创建到审核的完整链路检查每一步的状态码和数据库实际值是否一致第三层是异常注入手工把密钥改错、把时间戳改旧、把分录字段删掉确认客户端能给出可读的错误而不是静默成功。下面这张表是我在几个项目里累积的改造优先级按投入产出排改造点解决的问题优先级会话复用 失效重登每次调用都登录性能差且易触发限流高业务错误分类所有失败一锅炖无法自动处理高幂等键 映射表重试产生重复单据高字段常量集中管理平台加字段后到处改代码中回调幂等 验签消息重推导致数据错乱中批量调用分片大批量保存超时低有次为了赶进度跳过幂等表直接上线结果网络抖动重推了十几张重复单据财务对账对了两天最后写脚本按单号去重删的。从那以后我每次接写接口第一件事就是把幂等键和映射表建起来代码写得丑一点没关系重复数据才是真的还不了。希望这份拆解能帮你在对接这摊事上少走点弯路资料包里的表单清单和错误码对照表建议打印出来放手边比翻屏幕快。本文还有配套的精品资源点击获取