简介:旺店通WMS对接流程代码包是一份面向供应链与物流领域软件开发工程师的完整对接方案,重点解决企业系统与旺店通WMS之间的WebAPI接口集成问题。包内涵盖接口地址说明、销售出库单查询接口、接口调用规范以及标准定制接口的使用方法,并提供了基于C#与HttpClient的完整POST请求、签名构建和URL参数处理示例,帮助开发者从原理到实现快速贯通。压缩包大小为380KB,共32个文件,以C#源码(.cs)、DLL依赖库、JSON配置和文本说明为主要类型,另含Python辅助脚本与项目工程文件,便于对照编译和二次开发。目前已有183人学习浏览。资源特别附有具体调用示例,详细列出了请求地址、方法、密钥及参数设置等关键信息,可直接作为模板套用,有效降低对接门槛,加快项目落地。
1. 旺店通WMS对接流程:订单、库存和状态回传,一张图看懂的落地路径
WMS 对接听起来是个接口活儿,实际上一半以上的项目都耗在数据口径、状态回传和重复单这三件事上。旺店通WMS对接流程,本质上就是你的自研系统或第三方 ERP,通过对外开放接口把货品档案、库存余额、出入库指令和回传状态这四类数据,和 WMS 内部作业链路打通。你手上的单据在 WMS 里经过收货、上架、拣货、复核、出库之后,对应的状态还要能回到你的系统,让订单、账务和库存有个唯一的答案。这篇文章适合谁?正在接旺店通WMS的 Java/后端开发,被“同步失败”“回调丢失”“库存对不上”折磨到想换业务的对接负责人,以及准备把仓储切换成旺店通WMS、正做技术评估的架构师。它的价值很简单:照着链路走,你知道先调哪个接口、后调哪个接口,踩进坑里的时候知道去看哪张表、哪一个字段。
2. 对接前先理清 WMS 开放接口的三种边界:签名、数据模型和流程方向
2.1 签名与授权:MD5 签名和 session 是怎么凑出来的
旺店通WMS开放接口的调用方式与市面上大多数电商开放平台类似。每个请求要带上一组公共参数:app_key 标识你是哪个应用,timestamp 是当前毫秒时间戳,session 是这个应用在某商家账号下的授权令牌,sign 则是把所有业务参数和 app_secret 按规则拼接后做 MD5 得到的签名。签名不通过,服务端直接拒绝,连业务参数都不会解析。这里有个容易被忽略的细节:拼接参数时 key 要按字母序排序,value 不需要 URL 编码,但 secret 在拼接时放在参数串两端。用 Python 写一个签名函数大概长这样:
import hashlib import time import requests def make_sign(params: dict, app_secret: str) -> str: # 参数按 key 的字典序排序,拼成 key=value 形式 sorted_keys = sorted(params.keys()) raw = app_secret + "".join(f"{k}{params[k]}" for k in sorted_keys) + app_secret return hashlib.md5(raw.encode("utf-8")).hexdigest().upper() def build_request(method: str, biz_params: dict, app_key: str, app_secret: str, session: str): params = { "method": method, "app_key": app_key, "session": session, "timestamp": str(int(time.time() * 1000)), "format": "json", "v": "1.0", } params.update(biz_params) params["sign"] = make_sign(params, app_secret) return params # 示例:请求货品查询 resp = requests.post( "https://open-api.example.com/gateway", # 以实际网关地址为准 data=build_request( "wms.goods.query", # 货品查询方法名,各版本略有差异 {"goods_no": "SKU001", "page_size": 20}, "your_app_key", "your_app_secret", "your_session_token" ), timeout=10, )这段代码的核心在于make_sign:它把所有公共参数和业务参数放在同一个字典里排序,签名覆盖的是全部参数。不要漏掉毫秒时间戳这一点,曾经有人把 timestamp 写成了秒级,服务端返回“时间戳失效”,排查了大半天。app_secret 和 session 是两回事:app_secret 是你的应用密钥,session 是某个具体店铺的授权,一个 app 可以同时管理多家店铺,换店铺只需要换 session,不需要换密钥。生产环境里这两个值建议放在配置中心或环境变量里,别写死在代码仓库里,泄露的后果比接口限流严重得多。
2.2 数据模型边界:货品、SKU、仓库、单据类型怎么映射
旺店通WMS的数据模型核心是“货品 + 仓库 + 单据”,跟 ERP 里的物料档案、组织架构、业务单据有许多类似之处,但又不完全一致。对接前务必要画一张映射表,把两边系统里的概念放在一起对齐。WMS 侧的货品档案通常包含货品编号、货品名称、条码、规格、单位,而你的 ERP 里可能有主 SKU、子 SKU、辅助单位、自定义属性。常见的映射错误是拿“条码”当“货品编号”传,WMS 扫不到对应的主档,导致入库单创建失败。库存相关的几个概念也要提前分清:可用库存、在途库存、冻结库存、残次品库存,WMS 接口里返回的每个数量字段都要明确对应关系,不然你做出来的库存同步报表永远跟仓里实际点数对不上。
仓库维度上,WMS 的仓库编码(warehouse_no)是核心关联键,你的 ERP 里可能同时有销售仓、备用仓、虚拟仓。对接时只需要映射实体仓,虚拟仓不要下发到 WMS,否则会造成“虚拟仓库存被扣减”这类难排查的账实差异。单据类型上,WMS 侧主要分入库单、出库单、调拨单、盘点单,每种单在接口里的方法名和状态机都不一样。比如出库单可能有“待审核、已审核、拣货中、已出库、已取消”这些节点,你的系统在映射时要把“WMS 已出库”翻译成自己业务里的“已发货”,中间不要自作聪明地加多余的中间态,会增加对账成本。这步做得好,后面写状态回传逻辑时基本不会改结构。
2.3 流程边界:先拉后推还是先推后拉,同步还是异步
接口流程设计上,最常见的链路是“先拉后推”:每天早上或每隔一段时间,先从 WMS 拉一次货品和库存到本地,作为基线数据;然后本地系统把创建好的单据推送给 WMS;WMS 作业完成后,再通过查询或回调把状态拉/推回来。这种做法最稳,因为库存和货品总是以 WMS 为准,单证状态以本地为准。另一个方向是“纯推模式”,完全依赖回调,回调丢失就补单,适合单据量不大但实时性要求高的场景。我一般建议物流单量在每天几千单以下的企业从“先拉后推”起步,先把同步链路理顺,再逐步加大实时性比例。
同步还是异步这个问题,取决于你的业务容忍度。库存查询必须同步,因为界面上要即时展示可售数;出库单下发可以用异步,本地先把单存起来,后台任务推送,失败自动重试;状态回传则必须是异步的——WMS 不可能等你处理完才继续走下一步。对接初期把这三类请求的线程池分开,避免一个慢接口拖垮整个系统。
3. 搭一个最小可用的对接链路:从查货品到下发一张出库单
3.1 先用一个通用 Client 把签名、请求、重试封装好
把签名逻辑和 HTTP 请求封装成一个统一入口,是这一节最值得做的事情。不要在每个业务方法里重复写签名,更不要在代码里散落一堆 requests.post。封装之后,后面接任何新接口都只是加一个方法的问题。一个可用的 Client 大致如下:
class WmsClient: def __init__(self, app_key, app_secret, session, gateway, timeout=10): self.app_key = app_key self.app_secret = app_secret self.session = session self.gateway = gateway self.timeout = timeout def call(self, method, biz_params, retry=3): params = { "method": method, "app_key": self.app_key, "session": self.session, "timestamp": str(int(time.time() * 1000)), "format": "json", "v": "1.0", } params.update(biz_params) params["sign"] = make_sign(params, self.app_secret) for attempt in range(retry): try: resp = requests.post(self.gateway, data=params, timeout=self.timeout) data = resp.json() # 业务失败时错误码通常在返回体里的 code/error_code 字段 if str(data.get("code", "0")) != "0": # 如果是签名错误或限流,值得重试;业务参数错误不重试 if data.get("code") in ("sign_error", "rate_limit"): time.sleep(1 * (attempt + 1)) continue raise RuntimeError(f"接口业务错误: {data}") return data.get("data") or data except requests.Timeout: if attempt == retry - 1: raise time.sleep(1 * (attempt + 1)) raise RuntimeError("重试耗尽")这段封装重点在两个地方:一是签名每次都实时算,不要缓存,时间戳一变签名就变;二是重试策略只对网络超时和服务端明确返回的限流/签名错误进行,业务参数错误(比如 SKU 不存在)重试一万次都没用,反而会把错误数据留在中间表里。实际对接时建议在 Client 外加一层本地日志,把每次调用的 method、参数摘要、返回码和耗时记录到文件或日志系统,上线初期这份日志是你排查对账差异的唯一线索。
3.2 查货品和查库存:注意分页和字段过滤,别一把梭
最小链路的第一步是确认 WMS 侧货品档案和你的 SKU 对得上。常见的做法是先调用货品查询接口拉最近更新的货品,再逐个核对是否存在。下面是一个带分页的写法:
def fetch_all_goods(client: WmsClient, page_size=100): all_goods = [] page_no = 1 while True: data = client.call("wms.goods.query", { "page_no": page_no, "page_size": page_size, "modified_begin": "2024-01-01 00:00:00", # 增量拉取,减少数据量 }) goods_list = data.get("goods_list", []) if data else [] all_goods.extend(goods_list) total = data.get("total", 0) if data else 0 if len(all_goods) >= total or not goods_list: break page_no += 1 return all_goods分页参数各家命名可能有差异,但“用页大小控制单次返回条数、用总数判断是否拉完”这个套路是通用的。这里有个效率陷阱:每天全量拉一遍几百个 SKU 没问题,但几十万个 SKU 全量拉就会很慢,还容易触发服务端的限流。建议日常只增量拉最近 30 分钟或 1 小时内有变更的货品,全量拉取放到凌晨低峰期。库存查询接口要注意区分“按货品查”和“按仓库查”,库存返回的是多仓库结构时,本地要按“货品编号 + 仓库编码”作为唯一键落库,这样才能跟后面的出库单校验对上。
3.3 下发一张出库单:字段映射和编码规则必须在代码里写死
下发单据是整个对接的核心动作。一张出库单通常包含单号、仓库、收货人信息、明细行(货品编号、数量)这几块。重点在于单号的生成规则:一定要保证你的系统单号在 WMS 侧全局唯一,推荐用平台编码 + 业务类型 + 自增序列来拼,比如FW20240220001,不要直接拿数据库自增 ID 上去,WMS 那边没法解释“为什么这个 ID 这么小”。
def send_outbound_order(client: WmsClient, order: dict): biz_params = { "method": "wms.outbound.order.create", "out_order_no": order["out_order_no"], # 唯一单号 "warehouse_no": order["warehouse_no"], # 仓库编码,必须映射过 "shop_no": order.get("shop_no", ""), # 店铺编码,选填 "logistics_code": order.get("logistics_code", ""), # 物流公司编码 "is_auto_check": "1", # 1=单据下发后自动审核 "receiver_name": order["receiver_name"], "receiver_mobile": order["receiver_mobile"], "receiver_phone": order.get("receiver_phone", ""), "receiver_province": order["receiver_province"], "receiver_city": order["receiver_city"], "receiver_district": order["receiver_district"], "receiver_address": order["receiver_address"], "detail_list": order["detail_list"], # 明细, 包含 goods_no, qty } data = client.call("wms.outbound.order.create", biz_params) # 返回的 wms_order_no 是 WMS 侧的单号,要落库保存 return data.get("wms_order_no")detail_list 里每一项至少要有货品编号和数量。数量字段要特别注意:WMS 的数量一般按最小单位来,如果你的系统里订单卖的是“箱”,而 WMS 主档里货品单位是“件”,下发前必须做单位换算,否则会出现“客户买了一箱,仓里发了一件”的事故。is_auto_check 这个参数看你的业务要不要 WMS 自动审核,一般来说电商订单希望自动审,B2B 的订单希望对单后人工审。单据创建成功后返回的 wms_order_no 一定要记下来,它就是后续查状态、对账、做逆向的主键。
4. 回传与异步通知:把 WMS 的状态变成自己系统的状态
4.1 主动轮询和 Webhook 回调怎么选:数据量决定答案
单据下发到 WMS 之后,最关心的问题是“拣了没、发了没、签收了没”,这就要靠状态回传了。两种方式各有适用场景。主动轮询的做法是:本地保存所有待确认的单号,每隔几分钟调用一次状态查询接口,把最新状态更新到本地。实现简单,逻辑直观,问题在于单据量大了之后轮询频率和数据库压力不好平衡,每分钟查一遍可能有一半单子根本没动,纯属浪费请求。Webhook 回调则是 WMS 在状态变化时主动推送给你的接口,实时性好,但要求你的回调接口必须稳定:接收、校验、落库、返回成功,环环都要可靠。
我一般会按这个标准来选:日均出库单量在 5000 以下,轮询完全够用;超过 5000,建议轮询加回调并行,回调负责实时更新,轮询只兜底查那些超过 30 分钟还没变化的单子。两种方式本质上是“推”和“拉”的取舍,没有绝对好坏,只有你的技术团队擅长哪种。回调推送的方式对服务端稳定性要求高,如果你只有一台应用服务器,回调接口的线程池配置不好,一个慢 SQL 就可能把整条链路拖死。
4.2 回调验签与幂等处理:你必须写对的两段逻辑
回调接口的正确姿势是先验签,再判重,最后才处理业务。很多事故都出在“收到推送就直接更新数据库”上,结果要么是推了两次导致同一单子被处理两遍,要么是黑客伪造了一个假状态把订单改成已发货。验签逻辑跟前面请求签名一致:把推送过来的参数(除 sign 外)按字典序拼接,加上回调密钥做 MD5,和签名字段比对。注意回调的密钥可能与应用签名所用的 secret 不同,这是出于安全的考虑,不要默认用同一个。
def webhook_handler(request): # 1. 验签:回调参数与 sign 字段 params = request.form.to_dict() sign_received = params.pop("sign", "") sign_expected = make_sign(params, "your_callback_secret") if sign_received != sign_expected: return {"success": False, "msg": "sign error"}, 403 # 2. 按业务单号判断是否已处理过 out_order_no = params.get("out_order_no") status = params.get("status") # 比如 OUTBOUND_SHIPPED wms_order_no = params.get("wms_order_no") if is_processed(out_order_no, status): # 已经处理过同样状态,直接返回成功,不重复执行 return {"success": True, "msg": "duplicate ignored"} # 3. 更新本地订单状态,记录 wms_order_no 与本次状态 update_order_status(out_order_no, wms_order_no, status) mark_processed(out_order_no, status) # 4. 返回成功,WMS 侧收到成功才会停止重推 return {"success": True, "msg": "ok"}这段代码里的关键不是状态更新,而是mark_processed这一步。判断“已处理”时要细到“单号 + 状态”组合,比如“已发货”和“已完成”是两个状态,如果只按单号判重,后面“已完成”的回调会被误判成重复而丢掉。库存变化和单据状态建议分别落不同的表,不要混在一张表里,否则以后对账的时候你会花大量时间拆数据。另外回调接口要返回结构化的 JSON,不要返回 200 空页面,也不要返回一串纯文本,WMS 侧多数期望标准 JSON 里带 success 字段。回调处理是异步链路中最容易出幽灵问题的地方,超时、重复推送、乱序推送都会遇到,建议在回调落库时额外记一个“接收时间”字段,乱序问题可以通过状态机校验来拦截。这里有一半的功夫是靠本地补偿任务去兜底,回调只能算是一个加速器。
5. 旺店通WMS对接避坑:5 个踩完才能睡的坑
5.1 SKU 编码不一致,库存对不上
现象:本地系统显示某 SKU 可用库存 800 件,WMS 里查出来只有 80 件,且不是小数位问题,仿佛凭空少了十倍。原因:WMS 货品主档里这个 SKU 的编码是 ERP 里另一个不用的编码,导入主档时用了条码,而对接查询用的是货品编号,两边根本不是同一个主档记录。解决:上线前做一次“全量货品对账”,把两边系统的货品编码拉出来做 left join 比对,不一致的提前处理。不要相信“编码都一样”的口头承诺,哪怕同属一个公司,两边主数据归不同部门维护是常态。处理建议是在中间表里维护一套“本地货品编码 - WMS货品编码”映射,对接字段只准用映射后的 WMS 编码,杜绝出现“临时手输一个编码进去”这类操作。
5.2 仓库编码映射错位,单据下到了错误仓库
现象:创建出库单接口返回成功,但仓里始终没有这张单,查日志发现 warehouse_no 传的是“华东一号仓”,而接口文档里该字段要求的是仓库编码如 HZ01,你传的是仓库名称。原因:对接时没有仔细看字段定义,把界面里看得见的“仓库名称”当成了“仓库编码”直接塞进去,WMS 侧因找不到编码而默认走到了某个默认仓,或者直接丢进错误队列。解决:在配置表里把“本地仓名称 / 本地仓编码 / WMS仓库编码”三列维护好,代码里只允许用配置中心或数据库里的 WMS 仓库编码字段。测试环境验证时,先故意传一个错误编码看是否报错,如果传错还能建单成功,说明这个接口有容错,那更要小心——它可能把单子丢到某个你不知道的地方了。
5.3 数量精度与单位换算导致少发/多发
现象:明细里数量传的是 1,仓里拣货时拣了 1 件,可客户下单买的是 1 箱。核对代码发现本地数量字段是包装单位,而 WMS 接口要求的是最小零售单位。原因:两边系统的“数量”定义不同,本地走的商品上架时设定的箱规倍数,WMS 主档里的单位是“件”。解决:对接前在货品档案里增加“转换系数”字段,例如一箱等于 12 件,所有下发接口的明细数量都先乘以转换系数再传。这个系数建议跟货品主数据一起管理,不要散落在订单流程里。另一个常被忽略的是小数数量和非整除场景——比如订单要买 3.5 箱,换算成 42 件没问题,但有些系统会直接传 3.5,到了 WMS 侧解析失败,报“数量格式错误”。建议下发前做一个数量校验函数:非整数数量超过两位小数就抛异常人工介入。
5.4 回调丢失或超时,订单永远停在“已下发”
现象:WMS 里已经发货了,单据状态也已经变成“已出库”,本地系统里这张单却一直停留在“待发货”,直到客户来催单。原因:回调接口不稳定,WMS 推送时你的服务恰好不可用,WMS 重推了几次也没成功,于是放弃。也有的情况是回调接口处理时间太久,超过网关超时,服务端认为推送失败但不再重试。解决:不要依赖回调作为唯一状态来源。在本地维护一个“单据状态待确认”表,每次创建完出库单就往这个表里写一条记录,由定时任务每隔 10 到 30 分钟扫描一次,凡是超过 N 分钟还没有“已出库”终态的单子,主动调状态查询接口补拉。回调负责快,轮询负责稳,两边各干各的,任一边挂了系统都能兜住。另外一个更隐蔽的坑是状态回传顺序——WMS 先推“已出库”后推“已完成”,如果先收到“已完成”再收到“已出库”,很多新手直接更新,旧状态反而覆盖了新状态。应对办法是本地做状态值比较,只允许状态往前流转,往后流转的记录记入异常表。
5.5 重复推送导致同一张单处理两遍
现象:对账发现同一张出库单在本地系统里有两条发货记录,金额翻倍。原因:回调重推机制触发时,本地没有做幂等处理,或者判重键用的是“单号 + 状态”,而两次推送之间的状态恰好一致(比如都是已出库)。解决:落库之前用唯一索引兜底,在发货流水表上给“source + out_order_no”建唯一约束,第二次插入直接报冲突,代码里捕获这个异常当作已处理即可。同时要注意,判重键不要只用单号,因为一张单可能有多次状态变化。建议采用“单号 + 目标状态”联合判重,具体到你自己的流程里,就是要确保每个状态变更动作本身是幂等的——同一个状态的重复执行不应该产生任何新的业务记录。最后再提醒一句:所有重复和乱序问题,在最外层加一个对账定时任务,每天凌晨把 WMS 的已出库单据拉回来和本地流水做比对,差异主动告警,比任何代码技巧都有效。
6. 进阶:用一条命令的“对账脚本”把你的对接链路变成可信任的基础设施
对接跑通容易,长期稳定难。我的习惯是在整个对接模块旁边放一个独立的对账脚本,它不属于业务流程,只干一件事:把 WMS 的“当日已出库单”和本地系统的“当日已发货单”拉出来做全量比对。这个脚本要能单独跑、能出报表、失败只告警不影响线上。脚本的建议输出维度可以做成下面这样一张对比表:
| 比对项 | 本地系统数据 | WMS 数据 | 结果 |
|---|---|---|---|
| 单号缺失 | 本地有、WMS无 | 异常 | |
| 状态不一致 | 已发货 | 已出库 | 异常 |
| 数量不一致 | 3件 | 2件 | 异常 |
| 时间差异 | 发货时间 10:02 | 出库时间 10:05 | 正常 |
这个脚本不必天天跑,但上线初期至少跑一个月,它最大的价值不是发现差异,而是让你建立起“本地系统和 WMS 到底有多少口径差异”的感知。另一个值得做的小技巧是:在本地系统的订单状态流转里加一个“状态流转轨迹表”,每次状态变更都插入一条带时间戳的流水,之后无论哪个环节出了问题,翻开轨迹表就能看到完整的因果链,不用到处翻日志。我参与的一次对接项目中,团队就是靠这个轨迹表定位到一个“换单重发”场景里状态回退的 bug——那是代码逻辑问题,不是接口问题,没有轨迹表的话可能要排查几天。对接这件事没有炫技空间,把基础查漏补缺的工作做扎实,后面新对接任何渠道、任何仓库都会非常顺手,希望帮到你。
本文还有配套的精品资源,点击获取