简介面向Winform开发者的HTTP POST提交JSON与接收返回结果示例程序聚焦桌面端与Web接口之间的数据交互场景适合具备C#基础的初中级开发者学习。内容围绕HttpClient发起异步POST请求、使用Json.NET完成对象序列化与响应解析并处理成功状态与错误分支展开覆盖网络通信中的关键环节。压缩包共34个文件约59KB以C#源码为核心配合resx窗体资源、config配置、sln解决方案与csproj项目文件以及编译后的exe和pdb等构成可直接查看与运行的完整工程。已有3385人学习说明该主题在桌面应用网络通信学习中具有一定参考需求。读者可从中获得可复用的请求封装思路、窗体调用示例与JSON收发处理范式并据此对照自身项目排查MIME类型设置、异步调用和返回结果解析等常见问题。1. 一个 Winform 窗体、一个按钮问题却比想象中多一个 Winform 窗体几个输入框一个按钮点下去把值拼成 JSON 用 HTTP POST 提交给后端再把返回的 JSON 显示出来——听起来像是半小时就能写完的活。真动手的人基本都会遇到同一批问题点完按钮窗体白屏几秒、中文发过去变成问号、接口明明返回了 400 却只看到一句「请求失败」、换台机器就报「基础连接已关闭」。这份 Winform 程序要解决的就是这条简单链路稳定地 POST JSON、完整拿到返回结果、并且等待期间界面还能拖动。它面向的是做内部工具、上位机配置端、桌面管理端需要和 HTTP 接口对接的开发者如果项目已经上了 gRPC 或者成熟的网关 SDK这套东西带来的收益就没那么大了。下面按「骨架 → 组包拆包 → 不卡界面 → 踩坑 → 收口」的顺序拆一遍。2. 先搭好基础框架HttpClient 选型与第一次 POST 请求2.1 为什么是 HttpClient为什么不能每次 new在 .NET Framework 时代很多人习惯用HttpWebRequest拼请求设置Method、ContentType、写GetRequestStream()、再读GetResponse()。这套 API 能用但有两个老问题。第一是ServicePointManager.DefaultConnectionLimit默认值很小桌面端并发几个请求就开始排队现象是「明明网络没问题第二个请求就是慢」。第二是流和响应对象必须手工Close()少关一个就泄露一个连接跑一上午之后开始报「操作超时」。WebClient更简单一点但它在 .NET 6 之后已经被标记为不推荐使用而且取消请求、设置超时都很别扭。HttpClient是现在的主流选择它把连接池、Keep-Alive、压缩、取消令牌都封在里面了。但HttpClient有一个被说烂了、仍然天天有人踩的点它是设计成复用的。每次new HttpClient()都会创建新的连接池旧连接在 TCP 四次挥手完成之前还占着端口请求频率一高就进入TIME_WAIT堆积最后报SocketException: 由于系统缓冲区空间不足或队列已满。正确做法是把它做成static readonly字段或者交给IHttpClientFactory管理。Winform 单窗体工具用前者就够了。序列化库也有个选型问题。.NET Framework 4.x项目里我一般直接用Newtonsoft.Json宽容度高、日期格式可控、JObject处理动态字段方便。.NET 6的新项目可以用内置的System.Text.Json性能更好但默认行为更严格——属性名大小写敏感、遇到多余字段的处理策略不同跟老接口对接时反而要多写几行JsonSerializerOptions。这份程序按兼容性优先走Newtonsoft.Json。2.2 建立工程、引入依赖、让第一次请求能够跑通新建一个 Windows Forms 应用目标框架选.NET Framework 4.7.2或.NET 6-windows都行。窗体上放四个控件txtUrl接口地址、txtUser示例参数、btnSend发送按钮、txtLog多行只读文本框用来显示返回原文。装依赖用 NuGet包管理器控制台或者命令行都可以# 包管理器控制台 Install-Package Newtonsoft.Json # 或者 .NET CLISDK 风格项目 dotnet add package Newtonsoft.Json然后在Form1.cs里写第一个能跑通的版本using System; using System.Net.Http; using System.Text; using System.Windows.Forms; using Newtonsoft.Json; public partial class Form1 : Form { // 静态只读全进程共用一个连接池避免端口耗尽 private static readonly HttpClient Http new HttpClient { Timeout TimeSpan.FromSeconds(15) // 整体超时连接 发送 读完响应 }; public Form1() { InitializeComponent(); // 请求头只设一次别在每次请求里重复 Add会累加出重复项 Http.DefaultRequestHeaders.Accept.ParseAdd(application/json); } private async void btnSend_Click(object sender, EventArgs e) { btnSend.Enabled false; // 防连点重复提交 try { var payload new { userId txtUser.Text.Trim(), action query, ts DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss) }; string json JsonConvert.SerializeObject(payload); // StringContent 的第二个参数是编码第三个是 Content-Type using (var content new StringContent(json, Encoding.UTF8, application/json)) using (HttpResponseMessage resp await Http.PostAsync(txtUrl.Text.Trim(), content)) { // 先拿原文再决定怎么解析4xx/5xx 的响应体往往才是真正的错误信息 string body await resp.Content.ReadAsStringAsync(); txtLog.AppendText($[{(int)resp.StatusCode}] {resp.StatusCode}{Environment.NewLine}); txtLog.AppendText(body Environment.NewLine Environment.NewLine); } } catch (TaskCanceledException) { txtLog.AppendText([超时] 请求超过 15 秒未返回 Environment.NewLine); } catch (HttpRequestException ex) { txtLog.AppendText([网络异常] ex.Message Environment.NewLine); } finally { btnSend.Enabled true; // 无论成功失败都要恢复按钮 } } }这段代码有几个地方值得说明。payload用匿名对象就够了字段少的时候不必专门建实体类。StringContent的第二个参数Encoding.UTF8决定了请求体的字节编码第三个参数application/json决定了Content-Type头——注意它不会自动带上charsetutf-8有些服务端框架尤其老版本的 Java 侧会因此按 ISO-8859-1 解中文就变成问号了稳妥写法是直接写application/json; charsetutf-8。HttpResponseMessage用using包住是因为响应流不释放会一直占着连接。await resp.Content.ReadAsStringAsync()把响应体整个读成字符串——很多人图省事直接resp.Content.ReadAsAsyncT()一旦服务端返回的是 HTML 错误页抛出的异常里只有一句「反序列化失败」原始内容全丢了排查时等于断了一条线索。提示如果项目是.NET Framework 4.6.2以下或者LangVersion没开到 7.0 以上using var这种写法编译不过改成using (...) { }的块式写法即可。2.3 把地址、超时、请求头提取到配置里第一次跑通之后紧接着要做的就是把硬编码的值拿出去。接口地址在测试环境和现场环境肯定不一样超时时间也要根据接口实际耗时调。用App.config的appSettings是最省事的做法现场改配置不用重新编译。!-- App.config -- configuration appSettings add keyApiBaseUrl valuehttp://127.0.0.1:8080/api/ / add keyApiTimeoutSeconds value20 / add keyApiToken value / /appSettings /configuration读取的时候用ConfigurationManager.AppSettings[ApiBaseUrl]超时值记得做一次int.TryParse兜底别让一个手抖写错的配置把程序搞成启动即崩。HttpClient.Timeout一旦设了就改不了它是只读属性所以要么在创建时读配置要么把它固定成一个上限具体的单次超时交给CancellationTokenSource控制——这个在第四章展开。请求头里的鉴权 token 是个容易出问题的地方。DefaultRequestHeaders是全局的如果你在多处Add(Authorization, ...)会累加出多个同名头服务端直接拒绝。正确做法是每次请求构造独立的HttpRequestMessage把 token 放在它的Headers上这属于「用一次就丢」的对象不存在污染问题。3. 组包与拆包请求体怎么拼返回体怎么读才不丢信息3.1 用实体类映射 JSON不做字符串拼接手拼 JSON 是这条链路上最典型的低级错误来源字段值里带个引号、带个换行、带个反斜杠拼出来的字符串就是非法 JSON服务端返回 400你还得肉眼找哪个字符没转义。改成实体类 序列化转义交给库处理。using Newtonsoft.Json; public class OrderQueryRequest { [JsonProperty(order_no)] public string OrderNo { get; set; } [JsonProperty(start_time)] public DateTime StartTime { get; set; } [JsonProperty(page_size)] public int PageSize { get; set; } 20; } public class ApiResponseT { [JsonProperty(code)] public int Code { get; set; } [JsonProperty(message)] public string Message { get; set; } [JsonProperty(data)] public T Data { get; set; } } public class OrderDto { [JsonProperty(order_no)] public string OrderNo { get; set; } [JsonProperty(amount)] public decimal Amount { get; set; } [JsonProperty(created_at)] public DateTime CreatedAt { get; set; } }[JsonProperty]的作用是把 C# 的PascalCase属性名映射到接口的snake_case字段名这样客户端代码保持 .NET 风格序列化出来的 JSON 又符合服务端约定。如果接口字段和属性名完全一致这些特性可以省略但一旦接口改过字段名有显式映射的一方改起来只是改一行特性没有的一方就得全局搜索替换。PageSize给了默认值 20这是个小设计序列化时如果这个属性没被赋值默认会输出page_size: 20如果你希望不传时干脆不出现在 JSON 里就配合NullValueHandling.Ignore用可空类型。3.2 序列化参数日期、空值、大小写三处最容易出偏差默认的JsonConvert.SerializeObject(obj)有很多隐式行为跟接口对不上就会出问题。我的习惯是显式声明一份JsonSerializerSettings全项目共用private static readonly JsonSerializerSettings JsonOpts new JsonSerializerSettings { // 值为 null 的字段不参与序列化某些服务端会把显式 null 当成要清空该字段 NullValueHandling NullValueHandling.Ignore, // 日期统一成本地格式字符串不指定的话默认是 ISO8601 且带时区老接口常解析失败 DateFormatString yyyy-MM-dd HH:mm:ss, // 服务端新增字段时客户端不因为找不到对应属性而抛异常 MissingMemberHandling MissingMemberHandling.Ignore, // 反序列化时允许把字符串 123 转成 int 123容错接口字段类型漂移 // 只在对接不受控的老接口时开内部接口不建议 // TypeNameHandling 保持默认 None开了会有反序列化安全风险 };这三项里最容易被忽略的是NullValueHandling。曾经有个更新接口传{remark: null}表示「把备注清空」而客户端用Ignore之后这个字段根本不出现在报文里服务端理解为「不修改备注」导致界面上明明清空了、查回来还是旧值。所以这个参数不是无脑开的要看接口语义增量更新用Ignore更安全全量覆盖则必须让 null 也发出去。日期同理。默认序列化DateTime会输出2024-05-01T10:00:0008:00这种带时区的 ISO 格式很多老接口的正则只认yyyy-MM-dd HH:mm:ss直接 400。反过来反序列化时如果服务端返回的是带时区的字符串而客户端按本地格式解析跨时区部署就会差 8 小时——这类问题在现场环境下极难复现属于典型的「开发机全对、客户机全错」。3.3 读响应先拿字符串再决定怎么解析响应处理的标准流程是「拿原文 → 判断状态码 → 尝试解析 → 校验业务码」。四步里少一步出错时就会丢信息。下面是一个完整的处理骨架public async TaskT PostAsyncT(string url, object payload, CancellationToken ct) { string json JsonConvert.SerializeObject(payload, JsonOpts); using (var content new StringContent(json, Encoding.UTF8, application/json; charsetutf-8)) using (HttpResponseMessage resp await Http.PostAsync(url, content, ct).ConfigureAwait(false)) { // 第一步无论状态码是什么先把响应体原文读出来 string raw await resp.Content.ReadAsStringAsync().ConfigureAwait(false); // 第二步非 2xx 一律当失败但要把状态码和原文一起抛别只抛状态码 if (!resp.IsSuccessStatusCode) throw new ApiException((int)resp.StatusCode, raw); // 第三步原文可能不是 JSON网关错误页、HTML、空串要单独兜住 if (string.IsNullOrWhiteSpace(raw)) throw new ApiException(-1, 响应体为空); ApiResponseT result; try { result JsonConvert.DeserializeObjectApiResponseT(raw, JsonOpts); } catch (JsonException ex) { throw new ApiException(-1, 响应不是合法 JSON raw, ex); } // 第四步HTTP 200 不代表业务成功业务码要单独判 if (result null || result.Code ! 0) throw new ApiException(result?.Code ?? -1, result?.Message ?? 未知业务错误); return result.Data; } }ConfigureAwait(false)在这里是可选的因为这段代码放在服务层不接触 UI 控件加了好处是不强制回到 UI 线程上下文减少一次调度但如果你在调用方await之后直接改控件调用方的上下文仍然会把它拉回 UI 线程不受影响。ApiException自定义异常要带Code和RawBody两个字段。RawBody是关键——用户看到的提示可以是「订单不存在」但日志里必须留下完整的原始报文否则线上排查只能靠猜。我在实际项目里会把它同时写进一个按天滚动的日志文件一次故障回看半小时就能定位。还有一个细节ApiResponseT里Data是泛型。当接口返回的是数组时T就是ListOrderDto返回空对象时Data会是null调用方必须判空不能直接.Count。这一条在代码评审里经常被漏掉。4. 界面不卡死async/await、取消与跨线程回填4.1 async void 只留给事件处理器Winform 的事件处理器签名是void没法改成async Task所以async void是唯一选择。但async void有个致命特性里面抛出的异常不会被调用方捕获会直接冒到Application.ThreadException表现就是「程序突然弹一个未处理异常框栈里全是框架代码」。所以每个async void方法体都必须自带完整的try/catch/finally。private async void btnQuery_Click(object sender, EventArgs e) { SetBusy(true); try { var req new OrderQueryRequest { OrderNo txtOrderNo.Text.Trim(), StartTime dtpStart.Value, PageSize 50 }; var list await _api.PostAsyncListOrderDto(order/query, req, _cts.Token); BindGrid(list); } catch (OperationCanceledException) { AppendLog(请求已取消); } catch (ApiException ex) { // 业务错误走这里Message 给用户看RawBody 进日志 MessageBox.Show(this, ex.Message, 接口返回错误, MessageBoxButtons.OK, MessageBoxIcon.Warning); AppendLog(业务错误 code ex.Code body ex.RawBody); } catch (Exception ex) { AppendLog(网络异常 ex.GetType().Name ex.Message); } finally { SetBusy(false); } } private void SetBusy(bool busy) { btnQuery.Enabled !busy; btnCancel.Enabled busy; Cursor busy ? Cursors.WaitCursor : Cursors.Default; lblStatus.Text busy ? 请求中… : 就绪; }SetBusy把按钮、光标、状态栏三处状态收在一个方法里是为了避免「请求成功了但按钮忘了恢复」这类问题。放在finally里是保证即使抛异常也会恢复。这几行看着啰嗦但它消掉的是用户那边「程序卡住了、点不动了」的投诉。4.2 按钮状态、超时与 CancellationTokenHttpClient.Timeout是「整体超时」设成 20 秒意味着连接、发送、等响应、读完响应加在一起不能超过 20 秒超了抛TaskCanceledException。这在多数场景够用但有两个短板一是它不能中途取消用户点了取消按钮也得等满 20 秒二是它无法针对单次请求调整。所以更细的做法是HttpClient.Timeout设一个宽松上限比如Timeout.InfiniteTimeSpan每次请求用CancellationTokenSource控制实际时长。private CancellationTokenSource _cts; private CancellationTokenSource NewRequestCts(int timeoutSeconds) { _cts?.Cancel(); // 取消上一次还在飞的请求 _cts?.Dispose(); _cts new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); return _cts; } // 取消按钮立刻中断当前请求 private void btnCancel_Click(object sender, EventArgs e) _cts?.Cancel();new CancellationTokenSource(TimeSpan)这个构造函数会在指定时间后自动触发取消等价于「定时取消 手动取消」二合一。_cts?.Cancel()放在新建之前是为了处理「用户连点两次查询」的情况——不取消旧请求两个响应先后回来界面可能被先发后到的旧数据覆盖这种问题在慢接口上特别容易出现属于典型的玄学 Bug。注意CancellationTokenSource用完要Dispose否则内部那个定时器会一直挂着。虽然单个窗体工具影响不大但这是习惯问题写顺手了就不会在长期运行的服务里出问题。4.3 回到 UI 线程InvokeRequired 与 BeginInvokeawait之后能直接改控件是因为 Winform 的SynchronizationContext会把续体调回 UI 线程。但下面两种场景不受这个保护一是你用了Task.Run包住同步代码二是回调来自HttpClient内部线程或者定时器。此时直接txtLog.AppendText(...)会抛InvalidOperationException: 线程间操作无效。标准写法是这个模式private void AppendLog(string text) { if (txtLog.InvokeRequired) { // BeginInvoke 是异步投递不阻塞调用线程Invoke 是同步等待容易死锁 txtLog.BeginInvoke(new Action(() AppendLog(text))); return; } txtLog.AppendText($[{DateTime.Now:HH:mm:ss}] {text}{Environment.NewLine}); txtLog.SelectionStart txtLog.TextLength; txtLog.ScrollToCaret(); // 日志长了自动滚到底部 }用BeginInvoke而不是Invoke是有原因的Invoke会阻塞调用线程直到 UI 线程处理完如果此时 UI 线程正好在等这个调用线程比如在等一个Task.Result两边互等直接死锁界面上表现为整个窗体永久白屏。这个坑在第五章还会再提一次。ScrollToCaret在日志刷得很快时会有轻微闪烁如果日志量特别大每秒几十条建议改成批量追加用一个ConcurrentQueuestring缓冲配一个 200ms 的Timer统一刷进文本框性能差别很明显。5. 避坑排查这条链路上最容易出错的五处5.1 症状点完按钮窗体白屏十几秒完全点不动现象点击查询后整个窗体失去响应标题栏显示「未响应」几秒到几十秒后恢复或者干脆弹异常。原因在 UI 线程上同步等待异步任务典型写法是var s Http.PostAsync(...).Result;或者.Wait()或者调用了某个内部用.Result封装的同步方法。await的续体需要回到 UI 线程才能继续而 UI 线程正卡在.Result上等它两边互等死锁。即使侥幸没死锁比如库内部用了ConfigureAwait(false)UI 线程也会被阻塞到请求结束白屏是必然的。解决从事件处理器到最底层的方法全部一路async到底不出现.Result、.Wait()、.GetAwaiter().GetResult()。如果某个第三方库只提供同步 API用await Task.Run(() SyncCall())包一层让它跑在线程池上别阻塞 UI 线程。5.2 症状请求里的中文到服务端变成问号或乱码现象本地日志看 JSON 是对的服务端收到的字段值全是?或者䏿–‡这种。原因三个位置都可能出问题。第一StringContent(json, Encoding.Default, application/json)用了系统默认编码中文系统是 GBK跨平台就对不上。第二Content-Type里没有charsetutf-8服务端按自己的默认编码解。第三读取响应时用了ReadAsStringAsync()但如果响应头里声明的是charsetgb2312而实际内容是 UTF-8也会乱。解决请求固定用Encoding.UTF8Content-Type显式写全application/json; charsetutf-8响应侧如果服务端不规范可以读成字节流自己按 UTF-8 解var bytes await resp.Content.ReadAsByteArrayAsync(); var text Encoding.UTF8.GetString(bytes);。另外注意.NET Framework项目里.cs文件本身要存成 UTF-8 with BOM不然源码里的中文字面量在编译期就已经坏了。5.3 症状接口报错但界面上只有一句「请求失败」现象服务端日志明确记了 400 和具体原因客户端只显示「发生错误」拿不到任何细节。原因调用了resp.EnsureSuccessStatusCode()它在非 2xx 时抛HttpRequestException而异常对象的Message只有一句「Response status code does not indicate success: 400」响应体根本没被读取服务端返回的错误 JSON 被丢弃了。解决不用EnsureSuccessStatusCode改成手动判断IsSuccessStatusCode然后把响应体原文读出来一起抛给上层。日志里至少留三样东西请求 URL、请求体原文、响应状态码 响应体原文。这三样凑齐90% 的对接问题看一眼就能定位不用再让对面查日志。5.4 症状开发机全对换到现场机器就报「基础连接已关闭」现象同一份程序、同一个接口本机一直成功装到另一台机器上稳定失败异常信息是The underlying connection was closed或无法建立 SSL/TLS 的安全通道。原因.NET Framework 4.5及更早版本默认只启用 TLS 1.0而现在绝大多数服务端已经禁用 TLS 1.0只接受 1.2 及以上。开发机可能因为系统补丁或注册表设置默认开了 1.2现场机器没有。解决在程序启动时Main方法或窗体构造函数最前面显式声明协议版本// 仅 .NET Framework 4.5~4.7 需要.NET Core / .NET 5 由系统自动协商不用写 ServicePointManager.SecurityProtocol SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11;另外如果现场是自签名证书还需要挂一个ServerCertificateValidationCallback但千万别写成无条件return true那是一刀切关掉校验等于把中间人攻击的门开着。正确做法是校验证书指纹是否在白名单里不在就拒绝。这条在对外交付的工具里必须守住。5.5 症状请求发多之后偶发超时、连接被重置现象单次测试永远成功连续点几十次或者批量跑的时候开始零星失败报SocketException或者TaskCanceledException重启程序又好了。原因主要是两个。一是每次请求都new HttpClient()连接池不断新建端口耗尽TIME_WAIT堆积系统分配不出新端口。二是DefaultRequestHeaders被反复Add同名头累加某些服务端或中间网关会直接拒绝超长或重复的头。解决HttpClient做成静态单例或者至少一个窗体实例对应一个请求级的信息token、traceId放到HttpRequestMessage.Headers上而不是DefaultRequestHeaders如果确实需要中途换BaseAddress不要改单例的属性而是拼接完整 URL 传进PostAsync。另外把HttpClientHandler.MaxConnectionsPerServer适当调大比如 10可以缓解批量并发时的排队。6. 收束成可复用的一层把请求、校验、日志统一封装进 ApiClient零散写在窗体事件里的请求代码第一次能跑改到第三版就一定开始出现重复和遗漏。我一般会把它们收进一个ApiClient类静态HttpClient管连接一个泛型PostAsyncT管全流程日志和异常在同一个地方处理。public sealed class ApiClient { private static readonly HttpClient Http CreateHttp(); private readonly string _baseUrl; public ApiClient(string baseUrl) _baseUrl baseUrl.TrimEnd(/) /; private static HttpClient CreateHttp() { var handler new HttpClientHandler { AutomaticDecompression DecompressionMethods.GZip | DecompressionMethods.Deflate, MaxConnectionsPerServer 10 }; var c new HttpClient(handler) { Timeout Timeout.InfiniteTimeSpan }; // 超时交给 CTS c.DefaultRequestHeaders.Accept.ParseAdd(application/json); return c; } public async TaskT PostAsyncT(string path, object payload, CancellationToken ct) { string url _baseUrl path.TrimStart(/); string json JsonConvert.SerializeObject(payload, JsonOpts); var sw Stopwatch.StartNew(); try { using (var content new StringContent(json, Encoding.UTF8, application/json; charsetutf-8)) using (var resp await Http.PostAsync(url, content, ct).ConfigureAwait(false)) { string raw await resp.Content.ReadAsStringAsync().ConfigureAwait(false); var result ParseT(raw, (int)resp.StatusCode); ApiLog.Info($POST {url} {sw.ElapsedMilliseconds}ms 请求{json} 响应{raw}); return result; } } catch (Exception ex) { ApiLog.Error($POST {url} {sw.ElapsedMilliseconds}ms 请求{json} 异常{ex.Message}); throw; } } // ParseT 即 3.3 节那段「状态码 → JSON → 业务码」的四步校验 }参数上我固定了几条规矩写进团队约定里比写在文档里管用配置项建议值理由HttpClient.TimeoutInfiniteTimeSpan超时由每次请求的 CTS 控制可单独调整、可中途取消单次请求 CTS 超时15~30 秒视接口实际耗时定超过这个值的接口应该改成异步任务 轮询MaxConnectionsPerServer10批量导入场景够用不至于把服务端打满AutomaticDecompressionGZip Deflate响应体大时省带宽几十 KB 的列表接口差别很明显日志落盘按天滚动保留 7 天现场排查主要靠它超过一周的日志基本没人看敏感字段日志里脱敏token、手机号、身份证在写日志前先替换掉还有一个验证习惯值得养成写完之后别只点界面测试。我会在项目里留一个Debug配置下可见的「压测」小按钮连续发 50 次同一个请求看成功率、看平均耗时、看内存有没有持续上涨。这个按钮帮我提前发现过两次连接未释放的问题——单次点击永远不会暴露只有连续请求才看得见。从那以后我每次对接新接口都强制先走一遍这个流程用 Postman 或 curl 确认报文格式再在ApiClient里跑通一次然后压测 50 次最后才去接 UI。跳过其中任何一步后面都得花更多时间补回来。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?