简介:一套基于C# Winform实现问财数据获取的完整源码,面向金融数据抓取与桌面程序开发爱好者,解决同花顺问财接口接入、V值获取及数据展示过滤等实际问题。资源共812个文件,压缩包约60MB,除核心的cs源码与项目配置外,还包含大量dll依赖库、json/xml数据文件以及前端静态资源,便于直接编译运行或对照学习。已有2005人学习下载。源码覆盖从V值抓取到数据落地的全流程:利用HttpClient发起请求,配合HtmlAgilityPack解析页面提取V值,再携带参数获取结构化数据,经Newtonsoft.Json解析后绑定到Winform界面,并支持条件过滤与EPPlus导出Excel。同时内置错误处理、缓存和异步加载思路,有助于理解金融数据接口对接和桌面工具开发的关键环节。
1. 问财接口对接这件事,卡住大多数人的不是请求而是那个 v 值
做本地选股工具的人,大多绕不过问财:它能把"近三日主力净流入超过一亿的股票"这种自然语言直接转成结构化数据,省掉自建指标库的力气。用 C# WinForm 去做问财数据获取,界面和数据绑定从来不是难点,真正让网上不少源码"跑不起来"的,是每个请求都要带上的 v 值——它不固定、会过期,算错一次,接口就返回空数据。
这篇笔记写给想自己写问财查询工具的 .NET 开发者。从抓包拆请求链路,到 v 值的计算与提取,再到 C# 端完整代码和避坑记录,照着做就能在本地跑通一个能翻页、能导出 CSV 的 WinForm 问财查询器。
先说结论:v 值不是秘钥,是随请求时间变化的动态参数,本质是服务端校验请求合法性的一道暗号。它怎么算、怎么兜底,是下面要解决的核心问题。
2. 拆开问财的数据请求链路:抓包定位接口参数,弄清 v 值到底是什么
2.1 问财的本质:一个把自然语言变成结构化数据的 Web 接口
问财的网页端看起来是个搜索框,实际背后是一套标准的 HTTP 接口:你在输入框里敲一句自然语言,前端把这句话和一堆固定参数拼成一个 POST 请求发出去,服务端返回一段 JSON,前端再把它渲染成表格。所以对 C# WinForm 来说,问财数据获取的本质就是"用代码模拟那个 POST 请求,再把 JSON 解析成自己的数据"。理解了这一点,后面的事情就顺理成章了。
我一般会先想清楚一个问题:为什么选 C# WinForm 而不是脚本或者 Web 页面?理由很实际。第一,WinForm 适合做本地工具,不需要考虑部署到公网,数据留在自己机器上,隐私上更踏实;第二,DataGridView 绑定和导出 CSV 在 WinForms 里非常顺手,一个下午就能把查询、翻页、导出串起来;第三,如果你平时就在写 .NET 业务代码,这条路的维护成本最低,不用把同样的逻辑用另一门语言重新写一遍。
需要提醒的是,问财接口并没有公开的、长期不变的官方文档。大家用的都是通过浏览器开发者工具观察到的"前端实际请求",这意味着接口路径、参数名、返回结构都可能在某次改版后变化。这不是坏消息,反而说明了一件事:你的程序里,请求构造和解析逻辑必须集中管理,不能散落在各个按钮事件里,否则改版时你会改到怀疑人生。
2.2 用 F12 抓包定位查询接口:关键参数与返回结构
动手第一步,先把浏览器开发者工具打开。操作路径是:按 F12 打开开发者工具 → 切到 Network 面板 → 勾选 Preserve log(保留日志)→ 在问财搜索框输入一句查询并回车 → 在 Network 列表里把过滤条件切到 Fetch/XHR,逐个查看响应内容,直到找到返回结果表格数据的那个请求。这一步别偷懒,很多参数名和字段名都要从这里抄准确。
找到之后,要看三样东西:请求 URL、请求头和请求体。常见的实现里,查询接口是一个 POST 请求,路径形如 /customized/chart/get-robot-data,域名以你抓到的为准。下面这张表是我在对接时记录的参数,如果你抓到的字段名不同,以实际抓包为准:
| 参数 / Header | 位置 | 说明 |
|---|---|---|
| question | 请求体 | 自然语言查询语句,比如"今日涨停的股票" |
| page / perpage | 请求体 | 页码、每页条数,翻页时逐页变化 |
| source / version | 请求体 | 客户端标识与接口版本,改版时最容易变 |
| hexin-v | 请求头 | 动态 v 值,每次请求都要重新生成 |
这里有两个容易忽略的细节。第一,返回 JSON 的字段名在不同版本里有过调整,比如列表数据有时在 data.datas 下,有时包了一层别的字段,所以解析代码不能写死一套结构,至少要留一个"打印原始 JSON"的调试入口。第二,请求体可能不是单纯的表单格式,也可能是 JSON 格式,抓包时注意看 Content-Type,这决定了你在 C# 里是用 FormUrlEncodedContent 还是 StringContent。
提示:首次抓包建议把 Network 面板的 "Preserve log" 保持勾选,否则提交查询后页面一跳转,之前的请求记录就清空了,又得重来一遍。
2.3 v 值的三种形态:时间戳变形、头部令牌与页面提取
v 值为什么值得单独开一节讲?因为它是问财数据获取源码里最像"玄学"的部分。早期流传的实现里,v 值被描述成"把毫秒级时间戳转成 36 进制字符串",确实有一段时间是能用的;但后来有一段时间,服务端把 v 值从请求体挪到了自定义请求头里,字段名也变了。坚持用老代码的人,就会看到请求正常发出去了,返回却是空的,根本不知道去哪里排查。
我梳理下来的结论是,v 值至少有三种形态,你要根据抓包结果判断自己遇到的是哪一种。
第一种是时间戳变形。把当前毫秒级 Unix 时间戳做一次进制转换,常见的是转 36 进制,也有转 16 进制的变体。特征是:同一个请求,过一秒钟再发,v 值就变了,但长度固定,字符集只有数字和小写字母。
第二种是请求头令牌。服务端在自定义 Header 里校验 v 值。这种情况下 v 值照样是动态的,但放在头里而不是 body 里,很多老示例代码没跟上这个变化,自然就翻车了。
第三种是页面初始化下发。访问问财首页时,HTML 或首个脚本响应里带一个隐藏的 v 值,后续查询请求直接带上它。它的特点是:在一个会话周期内相对稳定,但换浏览器、清掉 Cookie 后就会变。
对应的,C# 端要做的不是赌其中一种,而是做一个可替换的 v 值提供策略:默认用时间戳变形算法,失败时切换成页面提取方案。这个设计我会在最后一章给出代码。现在先记住一句话:v 值获取不是"找一个固定值填进去",而是"按服务端当前规则动态生成或提取",这是整个对接的核心认知,比任何一段源码都重要。
3. C# WinForm 最小实现:计算 v 值、发起请求并解析返回 JSON
3.1 先搭 HttpClient:压缩解压、Cookie 与超时不能省
C# 端的第一步不是写界面,而是把 HTTP 客户端封装好。问财接口的响应几乎都带压缩,常见的是 gzip、deflate、br(Brotli)三种;同时它会按会话下发 Cookie 用于维持状态。这两件事没处理,后面会遇到"返回乱码"和"第一次能查、重启之后查不了"的怪问题。
using System.Net; using System.Net.Http; using System.Text.Json; // 全局只维护一个 HttpClient,避免频繁 new 导致端口耗尽 var handler = new HttpClientHandler { // 问财响应常见 gzip / deflate / brotli 压缩,三个都打开 AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, // 保存请求过程中服务端下发的 Cookie,维持会话状态 UseCookies = true, CookieContainer = new CookieContainer() }; var client = new HttpClient(handler) { // 15 秒没响应就直接失败,别让界面一直转圈 Timeout = TimeSpan.FromSeconds(15) }; // 带上一个常见的浏览器标识,降低被服务端直接拒绝的概率 client.DefaultRequestHeaders.UserAgent.ParseAdd( "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");这段代码有几个参数值得说。AutomaticDecompression 三件套是必选项,缺了 Brotli,一旦服务端返回 br 压缩的内容,你拿到的就是一串乱码字节,问题非常隐蔽。CookieContainer 负责自动存取 Set-Cookie,配合 UseCookies 使用,可以让 HttpClient 的行为更接近浏览器。Timeout 设 15 秒是个经验值,问财接口在行情时段会明显变慢,太短容易误判失败,太长会拖慢排错节奏。
3.2 计算 v 值并拼装查询请求:核心代码与参数含义
v 值的计算是很多人到处找源码的原因。我在这里给出一个按"毫秒时间戳转 36 进制"实现的版本,并明确说一句:这是曾经被验证过的规则,如果抓包发现当前接口不是这个算法,请按 2.3 节的办法切换到页面提取方案。代码里的域名用的是占位符,实际开发时替换成你抓包得到的地址。
// 把毫秒级时间戳转换成 36 进制字符串 static string BuildVValue(DateTime now) { long ms = new DateTimeOffset(now).ToUnixTimeMilliseconds(); return ConvertToBase(ms, 36); } // 通用的进制转换:支持 2~36 进制,字符集为 0-9a-z static string ConvertToBase(long value, int radix) { const string chars = "0123456789abcdefghijklmnopqrstuvwxyz"; if (value == 0) return "0"; var stack = new Stack<char>(); while (value > 0) { stack.Push(chars[(int)(value % radix)]); value /= radix; } return new string(stack.ToArray()); } // 拼装并发送查询请求,返回原始 JSON 字符串 static async Task<string> QueryWenCaiAsync( HttpClient client, string question, int page, int perPage, string v) { // 用占位符代替真实域名,实际开发时替换成抓包得到的地址 var url = "https://your-host/customized/chart/get-robot-data"; var payload = new Dictionary<string, string> { ["question"] = question, // 自然语言查询语句 ["page"] = page.ToString(), // 页码,从 1 开始 ["perpage"] = perPage.ToString(), // 每页条数,常见 50 ["source"] = "web", // 客户端标识,以抓包为准 ["version"] = "2.0" // 接口版本,改版时最常见的变化点 }; using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(payload) }; // 常见实现里 v 值放在自定义请求头 hexin-v 中,以抓包为准 req.Headers.Add("hexin-v", v); using var resp = await client.SendAsync(req); resp.EnsureSuccessStatusCode(); return await resp.Content.ReadAsStringAsync(); }三个关键点说一下。ConvertToBase 的字符表顺序固定是 0-9a-z,不要按 10 进制数字习惯去改,否则算出来的字符串和服务端规则对不上,属于典型的"看着对、实际错"。BuildVValue 每次调用都取当前时间,所以不要把它算一次存起来反复用——v 值随请求时间变化,复用旧值是避坑章节里要重点讲的一个错误。QueryWenCaiAsync 里的 source 和 version 是抓包时最容易注意不到的两个字段,它们一旦和服务端当前版本不一致,请求可能返回 200 但 data 为空。遇到这种情况,优先怀疑这两个值,而不是怀疑 v 值算法。
注意:任何教程给出的 v 值算法都可能在某一天失效。这不是 bug,是接口正常演进。唯一可靠的校准方式就是重新抓包对比,这也是为什么我一直强调要留调试入口。
3.3 解析问财返回的 JSON:结构会变,解析前先打印一份
拿到 JSON 字符串之后,别急着写解析。我的习惯是先把原始 JSON 完整地输出到一个文本文件或日志窗口里,看清楚了再动手。因为不同时期返回结构有差异,最常见的是列表数据在 data.datas 下,但也可能多一层嵌套或换字段名。
// 解析问财返回的 JSON,转成统一的字典列表 static List<Dictionary<string, string>> ParseWenCaiData(string json) { var rows = new List<Dictionary<string, string>>(); using var doc = JsonDocument.Parse(json); JsonElement root = doc.RootElement; // 第一层通常是 status / data,data 里才是结果列表 if (!root.TryGetProperty("data", out JsonElement data)) return rows; // 结果列表字段常见为 datas,也可能是 data_list,以抓包为准 if (!data.TryGetProperty("datas", out JsonElement datas)) return rows; foreach (JsonElement item in datas.EnumerateArray()) { var row = new Dictionary<string, string>(); foreach (JsonProperty prop in item.EnumerateObject()) { row[prop.Name] = prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() : prop.Value.GetRawText(); } rows.Add(row); } return rows; }这个解析函数的容错点在于:字段名是字符串,值统一转成字符串,前端展示和导出都方便。数值类型(比如涨跌幅)用 GetRawText 保留原始文本,这样后面做排序时不会被字符串化的精度问题坑到。如果返回结构里还有 status 字段,建议先判断它,不为 true 时直接把原始 JSON 输出到日志,这是最快的问题定位方式。如果你发现列顺序乱,不要依赖字典的枚举顺序,应该在解析后按实际需要的字段名重新排序。
4. 把数据接进 WinForm:异步加载防卡界面,翻页与导出 CSV
4.1 异步请求与 UI 线程:async/await 与控件状态
WinForm 的 UI 跑在主线程上,如果在按钮点击事件里同步调用 QueryWenCaiAsync().Result,界面会直接假死,直到请求结束才恢复。解决办法是全程 async/await。事件处理函数用 async void 是允许的,但要注意异常处理不能漏。
private async void BtnSearch_Click(object sender, EventArgs e) { // 查询期间禁止重复点击 btnSearch.Enabled = false; try { string question = txtQuestion.Text.Trim(); if (question.Length == 0) return; // 每次查询都用当前时间重算 v 值,绝不复用旧值 string v = BuildVValue(DateTime.Now); string json = await QueryWenCaiAsync(_client, question, 1, 50, v); var rows = ParseWenCaiData(json); var table = ToDataTable(rows); dataGridView1.DataSource = null; // 先断开,避免刷新闪烁 dataGridView1.DataSource = table; // 再绑定新数据 lblStatus.Text = $"共 {rows.Count} 行(第 1 页)"; } catch (Exception ex) { MessageBox.Show($"查询失败:{ex.Message}"); } finally { btnSearch.Enabled = true; } }这段代码里有几个 WinForm 开发的固定动作。await 之后的代码会自动回到 UI 线程,所以你可以直接操作控件,不需要手动 Invoke。查询期间把按钮置灰,防止用户在慢网络上重复提交。DataSource 先赋 null 再赋新表,是为了触发 DataGridView 的完整重绘,否则翻页时界面会出现"数据叠加"的视觉问题。lblStatus 用来即时显示状态,排错时比弹窗友好。
ToDataTable 的转换函数放在 4.2 节一起讲,它解决的是"DataGridView 不能直接绑定字典列表"的问题。绑定 Dictionary 时 DataGridView 只会显示 Key 和 Value 两列,必须转成 DataTable 或实体类列表。如果后续要做列过滤、排序,建议在这里就把列顺序固定下来。
4.2 翻页累加:从 DataTable 绑定到分页计数
问财单页返回条数有限,做筛选工具通常要翻多页。我的做法是维护一个全局列表 _allRows,每翻一页把新数据追加进去,然后重新生成 DataTable 绑定。
// 把字典列表转成 DataTable,供 DataGridView 直接绑定 static DataTable ToDataTable(List<Dictionary<string, string>> rows) { var dt = new DataTable(); if (rows.Count == 0) return dt; // 以第一行的列名建立表结构 foreach (string key in rows[0].Keys) dt.Columns.Add(key, typeof(string)); foreach (var row in rows) { var dr = dt.NewRow(); foreach (var kv in row) dr[kv.Key] = kv.Value; dt.Rows.Add(dr); } return dt; } // 翻页加载:页码累加,数据追加到全局列表 private readonly List<Dictionary<string, string>> _allRows = new(); private int _currentPage = 1; private const int PageSize = 50; private async Task LoadNextPageAsync() { int nextPage = _currentPage + 1; string v = BuildVValue(DateTime.Now); // 每次翻页都要新 v 值 string json = await QueryWenCaiAsync( _client, txtQuestion.Text.Trim(), nextPage, PageSize, v); var newRows = ParseWenCaiData(json); if (newRows.Count == 0) { lblStatus.Text = "没有更多数据了"; return; } _allRows.AddRange(newRows); _currentPage = nextPage; dataGridView1.DataSource = null; dataGridView1.DataSource = ToDataTable(_allRows); lblStatus.Text = $"已加载 {_allRows.Count} 行(第 {_currentPage} 页)"; }注意这里翻页请求用的是同一个 question 和新的 v 值。有些实现会犯"翻页时 v 值不变"的错误,导致服务端判定为异常请求,返回空数据。另外,每页条数 perpage 调大虽然能减少请求次数,但服务端对单页条数通常有上限,超过后可能直接截断或报错,我一般用 50 作为默认值,更稳妥。
4.3 数据导出 CSV:带 BOM 写入,表格软件打开不乱码
数据查出来最终要落到本地。CSV 是最省事的格式,但有个经典坑:如果按默认 UTF-8 无 BOM 写入,中文系统的表格软件打开会乱码。解决方法是写入 UTF-8 with BOM,另外要把字段里的逗号、引号、换行转义掉。
using System.Text; static void ExportToCsv(List<Dictionary<string, string>> rows, string path) { if (rows.Count == 0) return; var sb = new StringBuilder(); List<string> headers = rows[0].Keys.ToList(); sb.AppendLine(string.Join(",", headers.Select(EscapeCsv))); foreach (var row in rows) { string line = string.Join(",", headers.Select(h => EscapeCsv(row.GetValueOrDefault(h, "")))); sb.AppendLine(line); } // UTF-8 带 BOM,表格软件打开才不乱码 File.WriteAllText(path, sb.ToString(), new UTF8Encoding(true)); } static string EscapeCsv(string value) { if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) return "\"" + value.Replace("\"", "\"\"") + "\""; return value; }EscapeCsv 处理的是三个特殊字符:逗号是 CSV 的分隔符,双引号是转义符,换行会让一行数据变成多行。不加这个函数,碰上"公司名称里带逗号"的字段,导出的文件就错位了,这属于导出功能里最隐蔽的坑。UTF8Encoding(true) 里的 true 就是 BOM 标志,这一行是很多次"导出乱码"之后的血泪经验,建议直接照抄。
5. 问财数据获取避坑与常见问题:v 值失效、乱码、限流与界面假死
5.1 v 值算对了仍返回空数据:接口版本号变了的典型症状
现象:BuildVValue 算出来的 v 值看起来没问题,请求也返回了 200,但解析后一行数据都没有,data 字段缺失或不含 datas。
原因:多数情况下不是 v 值算法错了,而是请求体里的 version 或 source 字段与服务端当前版本不一致。问财改版时,version 是最高频的变化点,其次是 v 值存放的位置,可能从 body 挪到 header。
解决:重新抓包,把请求体和请求头逐字段对比。我一般先把"本地发出的完整请求"用日志打出来,再和浏览器开发者工具里看到的真实请求做 diff,一眼就能看出差异。改完 version 后如果还不行,再检查 v 值的位置和字段名。别在一个错误的方向上反复试,那是最浪费时间的排错方式。
5.2 返回 JSON 中文乱码:压缩解压与编码的双重坑
现象:接口能通,但解析出来的中文全是乱码,像"æ¶¨å œ"这类。
原因:通常有两层。第一层是响应压缩没解压,服务端返回了 br 或 gzip 内容,你却按纯文本读;第二层是编码问题,问财部分响应用的字符集不是默认的 UTF-8,而你按 UTF-8 解码。
解决:先确保 HttpClientHandler 的 AutomaticDecompression 打开 Brotli 与 GZip,这一步能解决八成乱码。还剩两成是字符集问题,在 ReadAsStringAsync 时注意,它默认按 UTF-8 解码,如果乱码依旧,抓包看响应头里的 charset,再用对应的 Encoding 读取。不要把两层混在一起排错,先解压后编码,顺序反了会被表象误导。
5.3 连续翻页后被限流:频率控制与退避重试
现象:前三页正常,翻到第四五页时开始偶发空响应,或者响应时间明显拉长,最后直接失败。
原因:短时间内请求频率超过了服务端的容忍阈值,触发了限制策略。问财这类接口虽然没有公开的限流文档,但实际行为上是有节流的,规律性请求更容易被识别。
解决:翻页之间加一个可控延时,我常用 300 到 800 毫秒的随机间隔,避免请求规律性太强。同时要做失败退避:连续失败两次后,延时翻倍到 2 秒、4 秒,超过三次就停止并提示用户,而不是无限重试。另有一个实用技巧:把单页条数从 50 降到 30,翻页次数变多但单次请求更轻,整体成功率反而更高。
5.4 点击查询后窗体假死:.Result 阻塞 UI 线程的教训
现象:点击查询按钮后,窗体拖不动、按钮按不下去,系统里看到程序"未响应",过几秒才恢复。
原因:在事件处理里写了 var json = QueryWenCaiAsync(...).Result 或 .Wait(),把异步请求变成了同步阻塞,UI 线程被卡死。这是新手最容易踩的坑,而且症状非常吓人,会让人误以为是程序崩溃。
解决:事件处理函数直接用 async void,内部用 await,不要碰 .Result。同时把按钮禁用逻辑放进 try/finally,保证异常时按钮能恢复。如果一定要做取消功能,给异步方法传 CancellationToken,按钮第二次点击时触发取消,比"等它自己结束"体验好得多。这个问题的本质是线程模型没理顺,改法固定,别在代码里绕弯子。
5.5 换台电脑程序就失效:Cookie 与初始会话没处理好
现象:同一份程序,在开发机上跑得好好的,拷到另一台电脑或服务器上,第一次查询就返回异常。
原因:问财的部分接口依赖会话初始化,浏览器里访问一次首页会建立会话并下发 Cookie,后续查询请求带这个会话才能通过校验。你的程序如果跳过了"访问首页握手"这一步,等于带着空会话直接查询,服务端自然不认。
解决:程序启动时先请求一次问财首页,拿到 Set-Cookie 后再进行查询;或者把 CookieContainer 序列化存到本地,下次启动直接加载。注意这个"首页握手"不能只做一次,如果程序长时间运行后突然失效,多半是会话过期了,这时候重新握手一次即可。我习惯把它做成 InitSessionAsync 方法,每次查询前检查会话时间,超过 10 分钟就重新握手,这样长时间挂着也不会突然断。
6. 把 v 值获取封装成可替换策略,加一个自检开关控制请求细节
6.1 策略接口与两种可切换实现
v 值获取是整个程序里最容易变的部分,所以我把它的逻辑从业务代码里抽出来。接口只定义一个方法,调用方完全不关心内部是算出来的还是提取的:
// v 值提供策略:隔离"算 v 值"和"用 v 值"两部分逻辑 public interface IVValueProvider { Task<string> GetVAsync(DateTime now); } // 方案一:时间戳转 36 进制(曾经的主流实现) public sealed class TimestampBase36Provider : IVValueProvider { public Task<string> GetVAsync(DateTime now) { // 毫秒时间戳转 36 进制,规则以当前抓包为准 return Task.FromResult(ConvertToBase( new DateTimeOffset(now).ToUnixTimeMilliseconds(), 36)); } } // 方案二:从首页响应里提取(v 值由页面下发时使用) public sealed class HomePageExtractProvider : IVValueProvider { private readonly HttpClient _client; public HomePageExtractProvider(HttpClient client) => _client = client; public async Task<string> GetVAsync(DateTime now) { string html = await _client.GetStringAsync("https://your-host/"); // 不同页面里 v 值的出现形式不同,正则要按抓包结果调整 var match = Regex.Match(html, "v[\"']?\\s*[:=]\\s*[\"']([0-9a-zA-Z]+)[\"']"); return match.Success ? match.Groups[1].Value : null; } }两个实现对应第 2 章说的两种形态。切换方式很简单:在程序启动配置里放一个开关,默认用方案一,自检发现连续几次请求数据为空时,自动切到方案二。这样即使服务端改了 v 值的生成规则,程序也能靠兜底方案续命,而不是等你重新抓包、改代码、重新编译发布。
6.2 自检开关:把请求细节完整打出来
最后一个技巧是自检开关。很多问题(v 值不对、version 过期、会话失效)单看返回内容很难定位,但看请求头、请求体、响应状态码的完整细节就一目了然。我会在程序里留一个调试模式,打开后把每次请求的 URL、Headers、Body、响应状态码和原始 JSON 追加到一个日志文件里,等确认运行稳定后再关掉。这个日志文件就是排错时的第一现场。
这个习惯救过我很多次。问财接口不是黑匣子,它的每次变化都会在请求痕迹里留下线索,自检日志就是你的侦察兵。我现在接这类接口,都会先花半小时把"请求记录 + 失败重试 + 策略切换"这套骨架搭好,再去做具体功能。骨架稳了,后面接口怎么改都不会慌,顶多就是改个版本号、调个正则的事。希望帮到你。
本文还有配套的精品资源,点击获取