WPS表格里那点数据要变成JSON,很多人第一反应是"另存为"里翻一遍,找不到,然后开始怀疑是不是版本不对。我最初做接口联调的时候也干过这事——把表格扔进某个在线转换网站,粘出来发现中文全是问号,长订单号末尾变成了0,日期变成一串四五万的数字,改一轮比重录一遍还慢。后来索性把这条链路自己搭了一遍,从宏到加载项都试过,现在基本是"表格改完点一下按钮,JSON直接落到指定目录"的状态。这篇就把我趟过的路完整写出来:WPS原生为什么不给JSON、三条可行路径各自适合什么场景、数据源要先做哪些清洗、宏和加载项的代码怎么写、一对多的嵌套结构怎么拼、导出的结果怎么验。不管你是刚接触表格转JSON的新手,还是已经在写脚本但总在细节上翻车的老手,应该都能从里面挑到能直接抄的部分。
1. 先把路径选对:WPS导出JSON到底有几种走法
1.1 原生"另存为"的格式清单里为什么没有JSON
打开WPS表格的另存为对话框,格式下拉里能看到的无非是自家的.et、微软的.xlsx和.xls、.csv、.txt、.pdf、.html、.dbf这些。这个清单不是随便定的,它反映的是"这一层该由谁负责"的分工:电子表格软件负责的是单元格网格的存取,JSON是描述对象结构的数据交换格式,两者中间隔着一层"结构映射"——哪一行是字段名、哪一列是数组、哪些行该合并成一个对象,这些规则表格软件没法替你猜。所以官方不给这个入口,不是遗漏,而是这件事本身就没有唯一正确答案。
理解这一点很关键,因为它决定了你后面所有方案的形态:JSON导出本质上是一个"你定义规则、程序执行规则"的过程,规则写得越明确,导出的结果越稳。我见过太多人把希望寄托在某个"一键转换"按钮上,结果每次表格结构微调一下,导出结果就面目全非,就是因为规则藏在工具里而不是握在自己手里。
顺带说一句,CSV确实是个中间站——先把表格存成CSV,再用一段脚本读CSV生成JSON。这条路人最少、门槛最低,但坑也集中:CSV默认编码在不同环境下可能是GBK,中文会烂;字段里本身带逗号或换行时,必须靠双引号包裹,解析器稍不注意就错位;空值和空字符串在CSV里长得一模一样。所以CSV方案我只在"一次性、字段简单、数据量不大"的场合推荐。
1.2 三条路径的横向对比:宏、加载项、外部脚本
我把实际用过的三条路整理成一张表,你可以对着自己的场景挑:
| 路径 | 实现方式 | 上手成本 | 维护成本 | 适合场景 | 主要短板 |
|---|---|---|---|---|---|
| 宏(VBA) | 表格内嵌VBScript风格代码,手动运行或绑按钮 | 中,需要装VBA运行环境 | 中,代码跟着文件走 | 单人、单文件、偶发导出 | 环境不一定有,跨机器容易失效 |
| 宏(JS宏/JSA) | 表格内置JavaScript宏编辑器,语法是现代JS | 低,无需额外安装 | 低 | 日常高频、需要处理复杂逻辑 | 文件读写接口在不同版本上命名有差异 |
| 加载项 | 用HTML/JS写功能区按钮,通过JS API操作文档 | 高,要打包注册 | 低,一次做好全员可用 | 团队共用、固定流程 | 调试链路长,注册环节容易卡住 |
| 外部脚本 | Python/Node读xlsx或csv再输出 | 中,要有运行环境 | 中,表格结构变了要改脚本 | 批量、定时、接入流水线 | 脱离表格界面,非技术同事用不了 |
我的实际选择是:日常自己用走JS宏,因为WPS现在内置了宏编辑器,不用额外装东西,语法又是JS,JSON.stringify直接能用,省掉手写转义的一大堆麻烦;要给整个小组用,就做成加载项,在功能区挂一个按钮,谁点都一样;如果是每天定时从一堆表格里抽数据进系统,那就放到外部脚本里跑,跟表格彻底解耦。
这里有个容易被忽略的判断依据:导出动作是谁触发的。如果是"人看着表格、觉得改完了、顺手导一下",那答案一定在表格内部(宏或加载项);如果是"程序定时跑、没人盯着",那就别硬塞进表格里,外部脚本更省心。反过来,把定时任务做成宏,得靠系统计划任务去唤起表格进程,稳定性差得不是一点半点。
还有一点,VBA在WPS里不是默认就有的,通常要单独装一个VBA宏运行环境组件;而JS宏是内置的。所以如果你的机器是干净环境、又不想折腾安装,直接奔JS宏去,能省掉一半的沟通成本——尤其是帮同事配环境的时候,你让他装个组件,他可能装完发现版本对不上,又回来找你。
2. 数据源先过关:表格结构决定了JSON能不能用
2.1 表头那一行就是schema,别随手写
JSON对象的键名全部来自表头,所以表头不是"给人看的标题",而是实打实的字段定义。我在项目里见过表头写成"客户名称(必填)"、"金额(元)"、"备注 "(末尾带空格)的,导出来键名里带括号带空格,前端拿到直接懵。规范的做法是:表头只用字母、数字、下划线,首字符不用数字,需要说明的信息放到单元格批注或者第二行说明里去,不混进键名。
命名风格也要统一。要么全小写下划线(order_no),要么全驼峰(orderNo),一个表里混着来,后面写消费端代码的人会很难受。我一般的习惯是跟后端接口字段对齐,表格直接照抄接口文档的字段名,这样导出的JSON几乎不用改就能塞进请求体。
另外,表头行的位置要固定。有些人喜欢在上面加个标题行、加个导出时间、加个制表人,结果第一行不是字段名。宏里如果按"第一行是表头"写死,就会把标题行当成字段。稳妥的写法是在代码里配置一个headerRow变量,明确告诉程序表头在第几行,别指望自动识别。这个参数虽小,但能挡掉后面一大类莫名其妙的错误。
还有一个细节:空表头列必须跳过。表格右边经常有临时计算的辅助列,表头是空的。如果代码不判空,就会生成一个键名为空字符串的字段,虽然JSON语法上合法,但消费端解析时会出问题。判断逻辑很简单,取到键名后trim一下,为空就continue。
2.2 合并单元格、隐藏行和空行:三个沉默的数据杀手
合并单元格是表格转JSON的头号敌人。Excel和WPS在存储合并区域时,只有左上角那个单元格真正持有值,其余单元格是空的。你眼睛看到的是"华东区"占了五行,程序读到的是一行有值、四行空。所以导出前必须做一次"向下填充":把上一个非空值填到下面的空单元格里。这个动作可以在临时副本上做,不破坏原表,我在代码里通常是复制一份工作表再填充,导完就删。
隐藏行的问题更隐蔽。用户筛选之后隐藏了一批行,或者手动隐藏了几行草稿数据,视觉上看不见,但UsedRange是包含隐藏行的,导出结果里就混进了不该出现的数据。如果业务上确实需要"只导可见行",就要在循环里加一个RowHeight > 0或者Hidden = False的判断;如果本意是全量导出,那就在操作说明里写清楚"导出前请取消筛选",避免两边理解不一致。
空行的处理要看约定。全部是空值的行,一般应该跳过,否则会生成一堆{"a":"","b":""}的垃圾对象。但有些业务里"空行"是有意义的占位,那就得保留。我的默认做法是跳过全空行,同时在日志里打出跳过的行号,方便回头核对是不是误跳了。
提示:导出前先另存一份副本再操作,尤其是要做填充、删行这类破坏性动作的时候。我吃过一次亏,填充操作直接写在了原表上,同事后来拿着那张表做汇报,发现数据"重复"了,解释了半天。
还有一个容易忘的点:表格末尾的空白行。有些人习惯性在数据下面敲几个回车,UsedRange的边界就被撑大了。判定数据末行时不要用UsedRange.Rows.Count,而应该从下往上找第一个非空行,这个逻辑在任何语言里都是几行代码的事,但能省掉大量"为什么多出来三个空对象"的追问。
2.3 数字、日期、前导零:类型丢失的三个高发区
先说什么叫类型丢失。JSON是有类型的:数字123和字符串"123"是两回事。表格单元格没有这么严格的类型概念,它是靠格式显示出来的。程序读单元格时,读到的可能是显示值,也可能是原始值,两种模式结果完全不一样。
第一类高发区是长数字。订单号、身份证号、银行卡号这种超过15位的数字,在表格里一旦被识别成数值,末尾就会被舍入成0,而且这个丢失是发生在录入那一刻的,导出环节再努力也救不回来。唯一的解法是录入时就设成文本格式,或者在数字前面加个单引号。导出代码里可以再加一道保险:对这类字段单独做字符串化处理,确保输出的是带引号的字符串。
第二类是高发区日期。表格里的日期本质是一个序列号,2024年1月1日存的是45292。如果代码直接取值,导出结果就是45292,消费端拿到一脸懵。处理办法是判断单元格格式是不是日期类型,是的话按yyyy-mm-dd格式化成字符串再输出。有些场景需要带时分秒,那就用yyyy-mm-dd hh:mm:ss。统一格式这件事一定要在导出层做死,别指望下游去猜。
第三类是前导零。像007、021这种编码,被当数字处理后前导零没了。同样只能靠文本格式解决。我在代码里会维护一个"强制文本字段"清单,配置成数组,命中就按字符串输出,其余按类型自动判断。这个清单是整个导出逻辑里最值得维护的东西,字段加一个改一行,比到处改判断条件清爽得多。
数值精度也要留意。小数运算在浮点数里天然有误差,19.9 * 3在某些语言里会得到59.699999999999996。导出前对金额类字段做一次保留两位小数的处理,输出就干净了。这个处理放在导出层做,别改原表,原表还得给人看。
3. 用JS宏做一次"另存为JSON":最小可用实现
3.1 宏编辑器在哪、怎么进入
WPS表格的功能区里找"开发工具"选项卡,里面能看到"JS宏"或者"宏编辑器"的入口,点开就是内置的代码编辑器。跟VBA不一样,这个不需要额外装运行环境,装好WPS就能用。第一次打开可能需要在设置里把宏安全性调一下,否则保存的宏会被拦。这个设置项的位置各版本略有差异,一般在"文件 - 选项 - 信任中心"之类的分类下。
宏文件跟着工作簿走,存成支持宏的格式(带m的xlsm,或者WPS自家的格式),下次打开还在。如果你希望这段逻辑在所有表格里都能用,可以把它放到个人宏工作簿里,那样每次启动都会加载。我个人是习惯每个业务表单独存一份,因为不同表的字段规则本来就不一样,放全局反而容易搞混。
调试的时候有个小技巧:在代码里用Console.log()输出中间结果,编辑器底部一般会有输出窗口。别一上来就对着最终JSON调,先把"读到的表头数组"和"第一行的值数组"打出来看看,八成的问题在这一步就能发现——要么表头读错位了,要么数据区起始行算错了。
3.2 读取区域与遍历的核心代码
下面这段是我常用的骨架,用的是WPS内置的JS宏语法,思路跟Excel的JS API基本一致:
function sheetToJson() { const app = Application; const wb = app.ActiveWorkbook; const sh = wb.ActiveSheet; // 表头行号,按实际情况改 const headerRow = 1; const startRow = headerRow + 1; const maxRow = sh.UsedRange.Row + sh.UsedRange.Rows.Count - 1; const maxCol = sh.UsedRange.Column + sh.UsedRange.Columns.Count - 1; // 收集表头 const headers = []; for (let c = 1; c <= maxCol; c++) { const v = sh.Cells(headerRow, c).Text; const key = v == null ? "" : String(v).trim(); headers.push(key); // 空键名后面跳过 } const rows = []; for (let r = startRow; r <= maxRow; r++) { const obj = {}; let isEmpty = true; for (let c = 1; c <= maxCol; c++) { const key = headers[c - 1]; if (!key) continue; const cell = sh.Cells(r, c); const val = readCell(cell); if (val !== "" && val !== null) isEmpty = false; obj[key] = val; } if (!isEmpty) rows.push(obj); } const json = JSON.stringify(rows, null, 2); Console.log(json); return json; }几个位置值得说明。UsedRange给出的行列数包含了格式残留,所以先用它划个大范围,再用"全空行跳过"的方式把噪声滤掉,比精确计算边界更省事。sh.Cells(r, c).Text取的是显示文本,对于日期和格式化过的数字,它给的是你眼睛看到的样子,这恰恰是我们想要的;如果取.Value2,拿到的是原始值,日期就会变成序列号。这个区别非常关键,我在早期版本里用了Value2,结果所有日期字段导出来都是四万多的数字,排查了半天才发现是取值方式的问题。
不过.Text也有副作用:列宽不够的时候,显示可能是###。所以对数值列,我建议保留一次原始值兜底——如果文本是###,就退回用.Value2。这个判断写起来也就三行。
3.3 类型判断函数与字段白名单
readCell是我封装的一个小函数,负责把单元格的值转成JSON里合适的类型:
const TEXT_FIELDS = ["orderNo", "idCard", "phone", "zipCode"]; function readCell(cell) { const raw = cell.Text; if (raw === null || raw === undefined || raw === "") return ""; // 列头命中强制文本清单,直接返回字符串 const colKey = String(cell.EntireColumn.Cells(1, 1).Text || "").trim(); if (TEXT_FIELDS.indexOf(colKey) >= 0) return String(raw).trim(); const s = String(raw).trim(); // 日期格式:按 yyyy-mm-dd 输出 if (/^\d{4}[-\/]\d{1,2}[-\/]\d{1,2}$/.test(s)) { const d = s.replace(/\//g, "-").split("-"); return d[0] + "-" + pad2(d[1]) + "-" + pad2(d[2]); } // 纯数字且不超过15位,转成数值 if (/^-?\d+(\.\d+)?$/.test(s) && s.replace(/[-.]/g, "").length <= 15) { return Number(s); } return s; } function pad2(x) { return String(x).padStart(2, "0"); }白名单那段是这里的重点。很多人写类型判断只按"能不能转成数字"来定,结果007变成7,13800138000变成数字后在某些语言的序列化里又被转成科学计数法。加一个强制文本清单,把这几类字段钉死成字符串,问题就没了。这个清单不需要很长,通常就五六项,但省下来的排查时间非常可观。
日期判断用正则而不是靠单元格格式属性,是因为格式属性的枚举值在不同版本、不同区域设置下不完全一样,用显示文本的模式匹配更稳。代价是必须约束录入格式,所以我会在表格上加数据验证,让日期列只能按yyyy-mm-dd录。约束入口永远比事后补救划算。
3.4 把结果落盘:编码这个坑必须提前躲
最省事的落盘方式是先输出到剪贴板或者新建一个工作表,让用户自己复制。但真到批量场景,还是要写文件。JS宏里写文件的接口在不同版本上命名有差异,稳妥的做法是用系统对话框拿到保存路径,再用当前版本提供的文件写入能力落盘;如果版本不支持直接写,就退一步写到临时工作表里再手动另存。
如果你走的是VBA路线,这里有个必须知道的坑:用FileSystemObject.CreateTextFile写文本时,第三个参数是"是否Unicode",传True写出来的是UTF-16LE,浏览器打开就是乱码;传False写的是ANSI,中文同样烂。正确的写法是用ADODB.Stream,显式指定UTF-8:
Dim st As Object Set st = CreateObject("ADODB.Stream") st.Type = 2 ' 文本流 st.Charset = "utf-8" st.Open st.WriteText jsonStr st.SaveToFile savePath, 2 ' 2 = 覆盖写入 st.Close这个细节我在别的文章里也提过,因为真的太容易踩。判断依据很简单:用系统自带的记事本打开文件,如果中文正常,编码基本没问题;如果显示成方块或者问号,那就是编码没对上。
4. 做成加载项:把转换固化成一个常驻按钮
4.1 加载项的基本骨架长什么样
宏的问题是每次都得打开编辑器点运行,对非技术同事不友好。加载项把这段逻辑包成一个功能区按钮,点一下就出结果,体验上跟原生功能没区别。代价是搭起来麻烦一些,要有几个固定文件。
一个最小的加载项大概包含这么几块:一个描述元信息的清单文件,声明名称、版本、入口页面;一个HTML页面作为容器;一个JS文件写业务逻辑;还有一个描述功能区的配置文件,定义按钮叫什么、图标用什么、点了调哪个函数。把这些打包成一个目录,通过WPS的加载项管理入口注册进去,重启后功能区就能看到按钮。
注册这一步是新手最容易卡住的地方。常见现象是文件都放对了,功能区就是不出按钮。我的排查顺序是:先确认注册的路径指向的是包含清单文件的那一层目录,而不是它的上一级;再确认清单文件里的入口文件名跟实际文件完全一致,大小写也要对上;最后确认是不是被安全设置拦了,通常会有提示,但有时候提示被折叠起来看不见。
4.2 调用表格对象的关键 API
加载项里拿文档对象的方式跟宏不太一样,表格类应用一般是通过专门的入口函数取到应用对象,再往下拿工作簿和工作表:
function exportJson() { const app = wps.EtApplication(); // 表格应用对象 const wb = app.ActiveWorkbook; const sh = wb.ActiveSheet; const used = sh.UsedRange; const rowCount = used.Rows.Count; const colCount = used.Columns.Count; const out = []; for (let r = 2; r <= rowCount; r++) { const item = {}; for (let c = 1; c <= colCount; c++) { const key = String(sh.Cells(1, c).Text || "").trim(); if (!key) continue; item[key] = sh.Cells(r, c).Text; } out.push(item); } const text = JSON.stringify(out, null, 2); saveToFile(text); }这段逻辑跟宏里几乎一样,差别只在取对象的入口。所以我的建议是:先用宏把逻辑调通,再整体搬到加载项里。宏的调试反馈快,改一行立刻能跑;加载项每次改完要重新加载,效率低得多。等逻辑稳定了再搬,能省不少时间。
saveToFile这一步在加载项里反而更好办,因为运行在Web环境里,可以直接用浏览器提供的下载能力,或者调用宿主提供的文件对话框。这就绕开了宏里那套编码折腾——Web环境的字符串本身就是UTF-8,写出去不会乱码。这也是我推荐团队场景走加载项的原因之一。
4.3 让按钮更好用的几个小设计
第一个设计是记住上次的配置。把表头行号、强制文本字段清单、输出目录这些参数存在本地,下次打开自动带出来。用户不用每次重新填,出错概率也低。存储用浏览器本地存储就行,简单可靠。
第二个设计是导出前做一次体检。点按钮之后先扫一遍数据,把"表头有空列""存在合并单元格""某列全部为空""前五行有重复键名"这些问题列成一个清单弹出来,让用户确认再导。这个功能看起来是锦上添花,实际用下来能挡掉至少一半的返工——很多人根本不知道自己表里有合并单元格,直到导出的JSON少了四分之三的数据。
第三个设计是导出后给个回执。不要只弹一句"导出成功",要给出具体信息:导出了多少条记录、输出到了哪个路径、耗时多少。我还会在回执里放一个"打开所在文件夹"的按钮。这些细节看着小,但对天天用的人来说,省下的就是实打实的时间。
5. 一对多结构:平表怎么拼成嵌套JSON
5.1 先想清楚"一行代表什么"
前面所有代码都基于一个假设:一行就是一个JSON对象。但真实业务里经常不是这样。最典型的是订单明细:订单号、客户名在一行里重复出现,每行对应一个商品。直接平铺导出,得到的是一个扁平数组,消费端还得自己按订单号分组。这时候就得在导出环节做聚合。
动手之前必须先回答一个问题:这一行代表的最小实体是什么。如果一行代表"一条明细",那JSON的最外层就应该是明细数组;如果一行代表"一个订单",那就要按订单号把多行合并成一个对象,明细挂在子数组里。这个问题想不清楚,代码怎么写都是错的。我在评审别人的导出脚本时,第一句问的永远是这句。
这里还有一个陷阱:分组键必须唯一且稳定。用订单号分组是安全的,用"客户名+日期"就危险,因为同一天同一客户下两单的情况太常见了。如果表里没有天然的唯一键,就在表格里加一列自增序号,别硬凑。
5.2 分组聚合的实现思路
核心就是一个哈希表,边遍历边往里塞:
const map = new Map(); for (let r = startRow; r <= maxRow; r++) { const orderNo = String(sh.Cells(r, orderCol).Text || "").trim(); if (!orderNo) continue; if (!map.has(orderNo)) { map.set(orderNo, { orderNo: orderNo, customer: String(sh.Cells(r, custCol).Text || "").trim(), items: [] }); } map.get(orderNo).items.push({ sku: String(sh.Cells(r, skuCol).Text || "").trim(), qty: Number(sh.Cells(r, qtyCol).Text || 0), price: Number(sh.Cells(r, priceCol).Text || 0) }); } const result = Array.from(map.values());这段代码有两个地方值得说。一是"主表字段取第一次出现的值",也就是订单号和客户名只在第一次遇到这个键时写入,后续行不再覆盖。如果后面某一行的客户名跟第一行不一致(表格填错了),当前逻辑是静默取第一个,不会报错。我的做法是加一个校验:如果发现不一致,把订单号记到异常清单里,导完提示用户。数据质量问题不该被代码掩盖掉。
二是明细的顺序。Map在现代JS里保持插入顺序,所以明细的先后跟表格里的行序一致。如果业务上要求按SKU排序,就在最后加一次sort,别指望表格里本来就是排好的。
更深一层的嵌套也是同样的道理,一层层套哈希表就行。但我要提醒一句:嵌套超过三层,就该考虑是不是表格设计有问题了。三层以上的结构,用表格维护本身就很痛苦,强行导出的结果往往连制表人自己都说不清。这种情况我更倾向于拆成多张表,用外键关联,导出时再拼。
5.3 导出之后的校验动作
JSON这东西有个特点:语法错了会立刻报错,但结构错了(比如字段名拼错、少了一层数组)语法上完全合法,肉眼也不容易发现。所以导出后必须验。
最低成本的验法是回读一次:把导出的文本用JSON.parse解析一遍,能过说明语法没问题;再抽查第一条和最后一条记录,对着表格看一眼字段值对不对。这一步我基本是条件反射,导出后必做。
再稳一点的做法是写一个校验函数,检查三件事:记录条数是否等于表格里的有效行数(分组场景则是唯一键的个数)、每条记录的键集合是否完全一致、必填字段有没有空值。这三条过一遍,九成的问题都能拦住。
如果是要对接后端接口,最好在导出后直接发一次测试请求。我之前吃过一次亏,本地JSON看着完全正常,发到接口才发现后端要的是{"data": [...]}包一层,而我们导的是裸数组,白折腾一轮。接口文档里的示例报文,值得在导出代码里照着抄一遍结构。
6. 实测中的偏差排查:这些现象对应这些原因
6.1 常见问题对照表
下面这张表是我这些年攒下来的,基本覆盖了九成以上的"导出来不对":
| 现象 | 大概率原因 | 处理办法 |
|---|---|---|
| 中文变成问号或方块 | 写入文件时用了ANSI或UTF-16编码 | 显式指定UTF-8,用记事本验证 |
| 长订单号末尾变0 | 单元格在录入时已被识别为数值,精度丢失 | 录入即设文本格式,导出时按字符串处理 |
| 日期变成四五万数字 | 取的是原始值而不是显示文本 | 改用显示文本取值,或按日期格式转换 |
| 数字字段带上了引号 | 统一按字符串输出,没做类型判断 | 加数值正则判断,命中转Number |
| 记录数比表格行数少 | 存在全空行被跳过,或分组时按唯一键合并 | 核对有效行数与唯一键个数 |
| 记录数比表格行数多 | UsedRange包含了格式残留的空行 | 从下往上找第一个非空行作为边界 |
| 合并单元格只导出了第一行 | 只有左上角单元格持有值 | 导出前做向下填充 |
| JSON解析报错 | 拼接时多逗号、少括号,或字符串未转义 | 用JSON.stringify,别手拼字符串 |
| 筛选后的数据也被导出了 | 循环没判断行是否隐藏 | 加可见性判断,或在说明里写明需取消筛选 |
| 点按钮没反应 | 加载项注册路径不对,或入口函数名不匹配 | 逐项核对路径、文件名、大小写 |
这张表建议存下来。遇到问题先对一遍,比从零开始查快得多。我发现很多人排查时喜欢直接改代码试,改了三版反而把原本对的地方也改坏了。先定位现象、再对原因,最后才动代码,这个顺序不能乱。
6.2 一次完整的排查链路复盘
说个真实的排查过程。某次导出的JSON,接口那边反馈说"客户名全是undefined"。我先做的是最小复现:把表格砍到只剩三行两列,导出,问题依旧,说明跟数据量无关。接着把导出的文本打印出来,发现键名是"客户名称",值是有的,但接口读的是customerName。到这一步就清楚了——不是导出坏了,是字段名没跟接口对齐。
但我没有立刻去改代码,而是先确认了一件事:这张表的字段名是谁定的。问下来是业务同事按自己的习惯写的表头,压根不知道有接口字段这回事。所以真正的修复不是改代码里的一行映射,而是在表格里加一行"字段名对照",让表头保持业务可读,导出时按对照表翻译。这个改动看起来更麻烦,但它解决的是根因,下次换个人填表也不会再错。
这件事给我留下一个习惯:遇到"值不对"先分清是取错了值还是名字对不上。取错值是技术问题,去查代码;名字对不上是协作问题,去查约定。两者的修法完全不同,混在一起查只会越查越乱。
还有一类问题出在"看起来对但就是不对"。比如导出的数字1.0变成了1,JSON里没法区分整数和浮点,这是格式本身的特性,不是bug。再比如对象里字段的顺序跟表格列顺序不一致,很多语言的JSON实现会重排键,消费端如果依赖顺序就会出问题。这类情况要在文档里写清楚,别指望靠代码去"修"——那是跟标准较劲。
最后说一个操作层面的建议:把每次导出都留一份带时间戳的副本。JSON文件很小,占不了多少空间,但出了问题的时候能立刻对比"上一版是什么样"。我现在的做法是文件名后面直接缀上20240513_1420这样的时间戳,谁改的、什么时候改的,一目了然。这个小习惯救过我至少两次,尤其是在多个人轮流维护同一张表的时候。