简介:面向畅捷通T+12.1初学者与需要对接该平台接口的开发者,这份演示程序把开放接口的调用方式、认证流程与数据交互过程集中呈现,适合作为从接口认知到动手集成的入门样例。压缩包共40个文件,约98KB,以C#源码、项目工程文件、编译后的库与演示程序为主,并配有API命令说明文本和JSON报文示例,便于对照理解请求构造、响应解析及异常处理。目前已有947人学习,说明其在新手接口实践中具有一定参考价值。读者可通过演示程序直观看到数据查询、新增、修改、删除等操作的调用路径,结合源码工程了解证书请求、参数格式、REST异常与序列化等实现细节;借助预置命令和JSON示例,还能梳理财务、库存等业务功能与T+12.1核心接口的对接思路,为后续自定义业务逻辑或第三方应用集成打下基础。
1. T+12.1 接口演示程序:先让一条取数链路跑通,再谈二开
拿到 T+12.1 的接口演示程序,很多人的第一反应是直接翻到「新增单据」那段,把报文抄进自己的服务,结果连鉴权都过不去。这套演示程序真正的价值不在某一段示例代码,而在于它把畅捷通 T+ 12.1 开放接口的整条链路摆出来了:应用注册拿 appKey/appSecret、换 token、带 token 请求业务资源、解析返回码、处理业务校验失败。它面向的是做进销存与财务单据对接的二次开发人员——ERP 和电商、WMS、门店系统之间来回倒数据的那批人。下面按我拆包的顺序走:结构、鉴权、报文、坑、上线自检。
2. 拆开演示程序的四层结构:为什么不能把业务代码直接糊上去
演示程序最容易被人误读成「一堆接口调用示例」。真拆开看,它的骨架其实是分层的:配置、鉴权、请求、业务,四层各自独立。分层的意义在于换账套、换地址、换小版本时,改动只落在一处。我见过太多项目把这些揉成一个ApiHelper类,几百行里既有 URL 拼接又有业务字段映射,出问题只能靠打印日志一点点试,玄学得很。
2.1 配置层:地址、账套、应用凭据三件事必须分离
配置层要装的是四类东西:T+ 服务地址(含端口)、应用凭据(appKey/appSecret)、账套标识(部分部署形态下需要显式指定)、以及超时与重试参数。演示程序里通常把这些放在一个配置文件或常量类里,我认为这是它最值得抄的部分。
关键点是:服务地址和应用凭据的变更频率完全不同。地址随部署环境变(测试环境、生产环境),凭据随应用生命周期变(重建应用就得换)。混在一个配置节里,运维改地址时顺手把 secret 覆盖掉,第二天全线 401,这种事故不稀奇。
| 配置项 | 含义 | 常见取值/形态 | 高频出错点 |
|---|---|---|---|
| 服务地址 | T+ 应用服务器访问入口 | http://内网IP:端口 | 少了端口、末尾多了斜杠导致路径拼成// |
| appKey | 开放接口应用标识 | 应用注册时生成 | 多环境共用同一个,日志混在一起查不出问题 |
| appSecret | 应用密钥 | 应用注册时生成,只展示一次 | 贴进代码时带了首尾空格或全角字符 |
| 账套 | 目标账套 | 账套号或账套名称 | 测试账套和生产账套串了,数据写错地方 |
| 超时 | 单次请求超时 | 10~30 秒 | 设 100 秒,界面卡死却看不出是接口慢 |
| 重试次数 | 失败重试上限 | 0~2 次 | 无脑重试写操作,重复单据一堆 |
凭据不要硬编码进源码,也不要提交进仓库。演示程序为了跑通通常会写在配置里,上生产前换成环境变量或配置中心,这一步不能省。
2.2 鉴权层:token 不是每次请求都去换
开放接口的通行做法是「凭据换 token,token 带在请求上」。演示程序里一般会有一个GetToken()方法,然后每个业务方法开头调一次。这种写法能跑通,但绝对不能照搬到生产——token 有有效期,但也经不起每个请求换一次,频繁换取轻则拖慢整体耗时,重则触发服务端的频率限制,表现就是「跑批到一半突然全部失败」。
我一般的做法是:token 缓存在进程内,记录签发时间和有效期,留出安全余量(比如有效期 7200 秒就按 6600 秒算过期),过期前主动刷新;并发场景下用一个锁或者SemaphoreSlim保证同一时刻只有一个线程在换 token,其余线程等结果,不要十几个线程同时去打鉴权接口。
还有一点容易被忽略:token 要按「服务地址 + 账套 + appKey」做缓存键。多账套场景下只用一个静态变量存 token,A 账套的 token 拿去请求 B 账套,返回的报错信息往往含糊,排查起来就是黑匣子。
2.3 请求层:超时、重试、日志三件套缺一不可
请求层负责的事:拼 URL、设 Header、序列化报文、读响应、判状态码、写日志。演示程序通常只做了前四件,后两件要自己补。
日志要落原文。不是「请求失败」四个字,而是完整的请求地址、请求体、响应体、耗时。T+ 的接口在业务校验失败时经常返回 HTTP 200,错误信息藏在响应体里,如果只按状态码判断成功失败,日志里一片 200,业务数据却没进去,翻车现场就是这样来的。
重试策略要按方法区分:查询类接口可以重试 1~2 次;写操作(新增单据、审核)默认不重试,除非你已经在业务层做了幂等。这一条是血泪经验,后面第 5 章还会展开。
2.4 业务层:档案和单据分开写,别做万能方法
演示程序里档案查询和单据新增是两组不同的调用,字段约定差别很大:档案偏「按编码/名称模糊查,返回主键」,单据偏「一次性提交主子表结构,服务端做大量校验」。把它们塞进一个CallApi(service, method, dict)的万能方法里,参数全靠字典拼,半年后没人看得懂某个字段为什么是那个值。
我倾向按资源建类:客户、供应商、存货、仓库各一个查询封装,销售订单、销货单、采购入库单各一个写入封装。每个类里把该资源的必填字段、字段类型、编码规则写清楚。演示程序的示例报文就是最好的字段清单来源,抄报文的同时把字段注释补上,后面少踩很多坑。
3. 跑通第一条链路:C# 里把 token 缓存和请求管道写对
T+ 的服务端是 .NET 体系,随包的演示程序大概率是 C# 写的。不管你最终用什么语言对接,先把 C# 这条链路跑通有好处:能确认服务地址、凭据、账套这三样是对的,后面换语言只是语法问题。
3.1 一个带过期时间的 token 缓存
先看缓存实现。下面这段是精简版,去掉日志和异常包装,只保留核心逻辑:
public sealed class TokenProvider { private readonly HttpClient _http; private readonly TOpenOptions _opt; // 服务地址、appKey、appSecret、账套 private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); private string _token; private DateTime _expireAt = DateTime.MinValue; public TokenProvider(HttpClient http, TOpenOptions opt) { _http = http; _opt = opt; } public async Task<string> GetTokenAsync(CancellationToken ct = default) { // 快路径:未过期直接返回,避免每次请求都抢锁 if (!string.IsNullOrEmpty(_token) && DateTime.Now < _expireAt) return _token; await _lock.WaitAsync(ct); try { // 双检:等锁期间可能已被其他线程刷新 if (!string.IsNullOrEmpty(_token) && DateTime.Now < _expireAt) return _token; var url = $"{_opt.BaseUrl.TrimEnd('/')}/tplus/api/v2/authentication"; var body = new { appKey = _opt.AppKey, appSecret = _opt.AppSecret, account = _opt.AccountNo // 多账套部署时必填,单账套可留空 }; using var resp = await _http.PostAsJsonAsync(url, body, ct); var text = await resp.Content.ReadAsStringAsync(ct); var result = JsonSerializer.Deserialize<TokenResult>(text); if (result?.Token is null) throw new InvalidOperationException($"换取 token 失败:{text}"); _token = result.Token; // 留 10% 安全余量,避免边界时刻过期 _expireAt = DateTime.Now.AddSeconds(result.ExpiresIn * 0.9); return _token; } finally { _lock.Release(); } } }逻辑上有三个点值得说。第一是双检锁:快路径不加锁,保证 99% 的调用没有锁开销;慢路径抢锁后再判一次,防止等锁期间别的线程已经刷好了。第二是安全余量:ExpiresIn * 0.9,别卡在最后一秒,网络抖动半秒就变成 401。第三是把完整响应体塞进异常信息,401 的时候能一眼看出是凭据错、账套错还是地址错。
参数上,BaseUrl末尾的斜杠要统一处理,TrimEnd('/')之后再拼路径,否则很容易拼出//tplus//api,某些网关会直接返回 404。account字段的名称和是否需要,不同部署形态不一样,以演示程序里的示例为准,别照抄别人的字段名。
3.2 统一请求方法:把传输成功和业务成功分开判
拿到 token 之后,所有业务请求都走同一个管道:
public async Task<ApiResult> PostAsync(string path, object payload, CancellationToken ct = default) { var token = await _tokenProvider.GetTokenAsync(ct); var url = $"{_opt.BaseUrl.TrimEnd('/')}/tplus/api/v2/{path.TrimStart('/')}"; using var req = new HttpRequestMessage(HttpMethod.Post, url); req.Headers.Add("openToken", token); // 鉴权信息放 Header req.Content = JsonContent.Create(payload, options: JsonOpts); // JsonOpts 里统一编码与日期格式 var sw = Stopwatch.StartNew(); using var resp = await _http.SendAsync(req, ct); var text = await resp.Content.ReadAsStringAsync(ct); sw.Stop(); _logger.LogInformation("T+ {Path} 耗时 {Ms}ms 状态 {Status} 响应 {Body}", path, sw.ElapsedMilliseconds, (int)resp.StatusCode, Truncate(text, 4000)); // 传输层失败才算失败,业务失败要看响应体 if (!resp.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)resp.StatusCode}:{text}"); return JsonSerializer.Deserialize<ApiResult>(text); // 含 code / message / data }这里最关键的一句是注释那行:HTTP 200 不代表业务成功。T+ 的接口习惯在响应体里放一个业务状态字段,可能是code、result、success之类,值非成功时message里给原因。判断顺序必须是「先看 HTTP 状态,再看业务状态」,两步都过才算成功。我见过只在catch里记日志的实现——业务失败压根不抛异常,数据没写进去,程序静默跑了一晚上。
Truncate的作用是防止响应体过大把日志撑爆,单据列表一次返回几千行是常事,截断到 4000 字符既能定位问题又不至于把磁盘写满。JsonOpts里统一定义日期格式和数字精度,不要在每个业务方法里各写一套,日期格式不一致是接口对接里最隐形的坑之一。
3.3 用 Python 脚本做旁路验证,别只信自己的代码
C# 跑通之后,建议再写一个几十行的 Python 脚本做交叉验证。目的不是重复劳动,而是把「是接口的问题」和「是我封装的问题」区分开。当业务方说数据没进去的时候,你先跑脚本,脚本能进去,问题就在你的代码里,排查范围立刻缩小一半。
import requests, json BASE = "http://<T+服务地址>:<端口>/tplus/api/v2" APP_KEY, APP_SECRET, ACCOUNT = "<appKey>", "<appSecret>", "<账套号>" # 1) 换 token r = requests.post(f"{BASE}/authentication", json={"appKey": APP_KEY, "appSecret": APP_SECRET, "account": ACCOUNT}, timeout=15) r.raise_for_status() token = r.json()["token"] # 字段名以实际响应对齐 # 2) 带 token 查一条存货,pageSize 先给 1,只验证链路 r = requests.post(f"{BASE}/inventory/query", headers={"openToken": token}, json={"code": "<存货编码>", "pageIndex": 1, "pageSize": 1}, timeout=15) print(r.status_code, json.dumps(r.json(), ensure_ascii=False)[:800])脚本里timeout必须显式给,requests默认不超时,服务端卡住时脚本会一直挂着,你在那边等结果,其实早就断了。pageSize先给 1 是刻意为之:验证阶段只关心链路通不通,不关心数据量,返回大了反而看不清结构。字段名token、openToken以你手上这套演示程序的实际示例为准,不同小版本命名可能不同,别拿网上的帖子硬套。
4. 档案与单据:查询和新增两类别碰同一套字段约定
接口调通之后,真正耗时间的是字段。档案类接口的核心是「怎么准确查到那一条」,单据类接口的核心是「怎么让服务端校验通过」。两者的思路完全不同。
4.1 基础档案查询:编码、名称与分页边界
查客户、供应商、存货、仓库,通常支持按编码精确匹配、按名称模糊匹配、按更新时间增量拉取。实际项目里最稳的做法是按编码精确查,因为编码在业务系统里是唯一且稳定的,名称会有重名、会有别名、会有人在系统里改名。
演示程序里的查询报文大致长这样,字段名按你的实际接口文档对齐:
{ "code": "C001", "name": "", "pageIndex": 1, "pageSize": 50, "ts": "2024-05-01 00:00:00" }pageIndex从 1 开始还是从 0 开始,各家不一样,第一次调的时候把pageSize设成 1,看返回的总条数字段,再决定翻页策略。pageSize有上限,常见是 100~500,设成 5000 可能被服务端截断成默认值,而你从返回里看不出来被截断了——只看到数据少了一半,以为是数据问题。稳妥做法是按 100 一页翻,翻到返回条数小于pageSize为止。
增量拉取用的时间字段(上面示例里的ts)一定要用服务端时区理解。你的服务器在 UTC,T+ 服务器在东八区,你传2024-05-01 00:00:00以为是凌晨,对面理解的可能不是。跨时区部署时统一在配置里写死时区偏移,别依赖运行环境的默认值。
4.2 单据新增:必填项、明细行与组织字段
单据新增是演示程序里最长的示例,也是踩坑最集中的地方。它一般包含三层结构:单据头(日期、往来单位、部门、业务员)、明细行(存货、数量、单价、仓库、计量单位)、以及一些由服务端计算或校验的字段(税率、金额、折扣)。
{ "externalCode": "EXT-20240501-0001", "voucherDate": "2024-05-01", "customerCode": "C001", "warehouseCode": "WH01", "details": [ { "inventoryCode": "I001", "quantity": 10, "unitCode": "PCS", "price": 12.50, "taxRate": 13 } ] }三个参数要重点看。externalCode是外部单号,如果你的业务系统自己生成单号,务必带上并且保证全局唯一,它后面做幂等和对账全靠这个字段;不带的话,重复提交就是两张单,删起来很麻烦。voucherDate的日期格式跟接口约定走,2024-05-01和2024/05/01在序列化层面都能过,服务端解析不一定都认,第一次对接时用演示程序里的格式原样试。unitCode和quantity是配套的,多计量单位存货(比如箱/瓶)如果单位给错,数量含义就整个变了,库存对不上账的时候往这个方向查。
还有一类字段是「档案里配了才能用」的:仓库要允许负库存、往来单位要启用、存货要在这个仓库有可用量。这些不是接口层面的错,是基础资料没配好,接口只会返回一句笼统的校验失败。上线前让业务方把要用到的档案过一遍,比在代码里猜原因快得多。
4.3 返回体解析:业务失败也是 200
单据新增的响应里通常有单据号和单据 ID,成功时直接拿;失败时给一段中文提示,比如某个字段不能为空、某个存货不存在、数量必须大于零。解析的时候别只取data,把message也原样透传到你的错误日志里,否则运维看到的就是「新增失败」,然后所有人一起猜。
一个实用的做法是给常见错误提示做一张映射表,把「某某不能为空」映射到具体字段名,前端能直接高亮那个输入框。这张表靠实际跑出来积累,跑一轮完整的业务场景,把遇到的报错都记下来,两三轮之后覆盖率就上去了。
5. 避坑与排查:五个真会翻车的点
下面这五条基本覆盖了对接初期 80% 的故障,每条都按现象、原因、解决写,遇到了直接对照。
5.1 现象:本地跑得好好的,上了服务器全部 401
原因通常有两个。一是服务器时间不同步,如果这套接口带时间戳或签名,时间偏差超过几分钟就验不过;二是 appSecret 在部署时被配置中心的占位符覆盖了,实际传上去的是环境变量名本身。
解决:先在服务器上curl一次鉴权接口,把完整响应打出来,确认时间、凭据、地址三项。服务器开 NTP 同步,时间偏差控制在秒级。配置加载后打印一次凭据长度(不要打印内容),长度不对立刻就知道是占位符没替换。
5.2 现象:单据新增返回成功,库存和应收却没变化
原因是单据状态。T+ 里单据通常有「未审核」和「已审核」之分,未审核的单据不参与库存和账务,接口新增出来的往往就是未审核状态。很多人以为接口写完就完事了,业务方查库存查不到,回头找开发。
解决:确认接口是否提供审核动作,或者新增时能否直接指定状态。如果不能,就把「审核」做成你这边流程里的一步,明确写进对接方案,别指望自动生效。这条在项目启动会上就该说清楚,事后扯皮最费时间。
5.3 现象:跑批时偶发重复单据,删到手软
原因是写操作做了重试。请求超时了,你的代码重试一次,实际上第一次已经写进去了,只是响应没回来,于是一张单变两张。网络抖动越大,重复越多。
解决:两条腿走路。第一,写操作默认不重试,交给人工或定时任务兜底;第二,如果必须重试,带上externalCode做幂等,重试前先用外部单号查一次是否已存在。查询接口本身可以放心重试,它不改数据。
5.4 现象:中文名称查不出来,编码查就正常
原因是编码问题。请求体编码、Content-Type的 charset、以及 HTTP 客户端默认的序列化行为,三者不一致时,中文在传输中就被转义坏了,服务端拿到的是乱码,自然查不到。
解决:统一用 UTF-8,显式设置Content-Type: application/json; charset=utf-8,序列化时不转义非 ASCII 字符。验证方法很土但有效:先把中文原样打印成字节数组看一眼,再和抓包结果对比。
5.5 现象:翻页翻到第 5 页之后数据重复或缺失
原因是排序不稳定。服务端分页如果没有明确的排序字段,或者排序字段有大量重复值,翻页时记录的相对位置会变,出现重复和漏读。数据量小的时候看不出来,几万条时就明显了。
解决:翻页请求里带上确定的排序字段(通常是主键或编码),并且按「更新时间 + 主键」做复合增量拉取,而不是单纯依赖页码。全量同步场景下,用增量时间窗口切割比分页更可靠,每个窗口拉完做一次条数校验。
6. 上生产前的自检:用幂等键和对账把接口钉死
演示程序能跑通,离能上生产还有一段。我一般会在正式放量之前做两件事:把幂等键钉死,把对账脚本写出来。
幂等的核心是「外部单号 + 本地流水表」。每次调用写接口之前,先在本地流水表里插一条记录,状态为「处理中」,唯一索引建在外部单号上;插入成功才发请求,请求返回后更新状态为「成功」或「失败」,失败时把响应体存进去。这样重复提交会被唯一索引挡住,出了问题也有完整的请求-响应留痕,不用再靠猜。
对账脚本按天跑,比对两边的单据条数和金额合计:
-- 本地流水表与 T+ 侧单据数按日比对,差异行人工介入 SELECT l.biz_date, COUNT(*) AS local_cnt, SUM(CASE WHEN l.status = 'SUCCESS' THEN 1 ELSE 0 END) AS pushed_cnt, SUM(CASE WHEN l.status = 'FAILED' THEN 1 ELSE 0 END) AS failed_cnt FROM t_openapi_push_log l WHERE l.biz_date >= :from_date AND l.biz_date < :to_date GROUP BY l.biz_date ORDER BY l.biz_date;biz_date用业务日期而不是创建时间,跨零点跑批时才不会把两天的数据混在一起。FAILED的行要能一键重推,重推前用外部单号查一次是否已存在,避免第 5.3 条那个坑。
上线自检我通常过一遍这张表:
| 检查项 | 检查方法 | 通过标准 |
|---|---|---|
| 凭据与地址 | 生产环境单独跑一次鉴权 | 能换到 token,日志无 401 |
| token 缓存 | 连续压 200 次查询 | 鉴权接口调用次数 ≤ 2 |
| 业务失败识别 | 故意传一条不存在的存货 | 能拿到业务错误信息而非静默成功 |
| 幂等 | 同一外部单号连发两次 | 只生成一张单据,第二次被拦截 |
| 对账 | 跑一天数据后比对条数 | 差异为 0 或有明确原因 |
| 日志 | 抽查 10 条失败记录 | 请求体、响应体、耗时齐全 |
最后说个习惯。早些年我接过一个对接,演示程序跑通了就直接上,结果月底对账差了三万多块,查了两天才发现是某类单据的税率字段一直是默认值,接口不报错,金额就是错的。从那以后我每次接这类接口,都强制先跑一遍幂等和对账脚本,再小的改动也跑,宁可多花半天,也不要在月底对着差额掉头发。希望帮到你。
本文还有配套的精品资源,点击获取