用 jq 清洗订单 JSON:先隔离异常,再统计已支付金额

接口导出的 JSON 能解析,不代表里面的金额就能直接求和。退款可能仍带着原订单金额,缺失值可能是 null,数字也可能被输出成字符串。如果一开始就把它们统一转成数字、空值补零,报表不一定报错,却已经换了业务口径。
最近查看 API 更新时,我注意到 Google 10 月 26 日调整了新项目的请求存储默认值;现有项目保留设置。1 这只是数据留存相关的选题线索,不是 jq 或结构化输出的新功能。对 AI/API 工作流而言,保存下来以后如何检查数据,同样值得做一个小而可复现的实验。
本次在 Linux 的 jq-1.7 上实际执行。下面七条订单完全由我构造,不是真实客户数据,也不是某个模型的实际回复。目的只有一个:用 jq 保留异常理由,再按明确规则统计有效的已支付金额。
先把统计口径写出来
这里的金额单位是"分",只接受非负整数。仅 status 为 paid 的记录进入汇总。退款和其他状态单独保留,不参与计算;记录不是对象、编号不是字符串,或已支付记录的金额不合格,则放进 bad。
保存为 orders.json:
json
{
"orders": [
{"id":"A100","status":"paid","amount_cents":1200},
{"id":"A101","status":"paid","amount_cents":800},
{"id":"A102","status":"refunded","amount_cents":500},
{"id":"A103","status":"paid","amount_cents":null},
{"id":"A104","status":"paid","amount_cents":"700"},
{"id":"A105","status":"paid","amount_cents":-300},
"not-an-order"
]
}
先试一条看似省事的命令:
bash
jq '[.orders[] | objects | (.amount_cents | tonumber? // 0)] | add' orders.json
# 实际输出:2900
它滤掉了非对象,却把退款的 500 分算了进去,把字符串金额转成了 700,还让负数抵减总额。null 被补成零,看不出原始缺失。2900 是这条命令的真实结果,但不是本例规则下的正确支付金额。问题不是加法算错,而是筛选和修复策略未经定义。
分类以后再汇总
把下面保存为 report.jq。我把分类独立成函数,这样异常保留在哪里、为什么不计入,都能从输出追溯。
jq
def classify:
if type != "object" then
{bucket:"bad", reason:"not_object", raw:.}
elif (.id | type) != "string" then
{bucket:"bad", reason:"id_not_string", raw:.}
elif .status == "refunded" then
{bucket:"ignored", id:.id, reason:"refunded"}
elif .status != "paid" then
{bucket:"ignored", id:.id, reason:"not_paid"}
elif (.amount_cents | type) != "number" then
{bucket:"bad", id:.id, reason:"amount_not_number"}
elif .amount_cents < 0 or (.amount_cents | floor) != .amount_cents then
{bucket:"bad", id:.id, reason:"amount_not_nonnegative_integer"}
else
{bucket:"accepted", id:.id, amount_cents:.amount_cents}
end;
if type != "object" then error("root must be object")
elif (.orders | type) != "array" then error("orders must be array")
else
[.orders[] | classify] as $rows
| [$rows[] | select(.bucket == "accepted")] as $ok
| {
accepted_count: ($ok | length),
paid_total_cents: ([$ok[].amount_cents] | add // 0),
accepted: $ok,
ignored: [$rows[] | select(.bucket == "ignored")],
bad: [$rows[] | select(.bucket == "bad")]
}
end
先检查根结构,再检查每条记录,是这里刻意安排的顺序。根节点不合格会直接 error,而单条坏数据不会把整份报告中断。不要把两类失败都吞掉,否则上游把 orders 改成对象,也可能被当作"今天没有订单"。
状态筛选在金额校验之前。因此退款订单只留下编号和退款原因,本脚本不检查退款金额是否完整 ;非 paid 状态也统一标为 not_paid。如果要审计未知状态或核对退款流水,应另加规则,而不能说这个输出已经验证了全部字段。
实际运行得到什么
bash
jq --version
# jq-1.7
jq -f report.jq orders.json > report.json
jq '{accepted_count, paid_total_cents,
ignored_count:(.ignored|length), bad_count:(.bad|length)}' report.json
最后一条命令的实际输出是:
json
{
"accepted_count": 2,
"paid_total_cents": 2000,
"ignored_count": 1,
"bad_count": 4
}

图中内容来自本次保存的 stdout 和后续筛选命令,只做排版,不是终端界面截图。两条有效订单合计 2000 分,也就是 20 元。四条坏记录没有消失:金额缺失和字符串类型分别能定位到 A103、A104,负数对应 A105,非对象记录保留在 raw 中。这样修数据时不必重新猜测问题来自哪条输入。
空列表与错误结构不能混为一谈
我另外实际执行了六组边界输入:空 orders、零金额支付、待支付状态、小数金额、根数组、orders:null。前四组命令退出码都是 0;空列表总额为 0,零金额可接受,待支付单进入 ignored,1.5 分进入 bad。最后两组退出码均为 5,stderr 分别提示根必须是对象、orders 必须是数组。
jq 手册说明,空数组执行 add 返回 null。2 所以本例在已通过筛选的金额列表后用 add // 0:这是把"没有有效支付记录"表达成零,不是给未知金额随手补零。两处看起来一样的 // 0,放置位置不同,含义完全不同。
这份脚本适合什么,不适合什么
它适合小型接口导出或测试夹具的快速检查,不是财务结算系统。脚本不去重订单编号,不验证编号是否为空,也不处理多币种、部分退款和超大整数的精度边界。金额用整数分能避开本例的小数金额输入,但不能据此保证任意规模的金额计算精确。
异常 raw 也可能包含敏感字段;真实项目里应按需要删减字段后再写日志。本例全部是合成数据,所以保留原值便于复现。大文件则还要考虑内存:这里构造了完整分类数组,不是流式处理。
使用 GPT 辅助写过滤器时,我更愿意先给出这些输入和规则,再检查脚本输出,而不是把一条能运行的命令当作可靠报表。gpt68.com 是第三方 AI 会员服务平台,并非模型厂商官网或授权合作方;会员服务与本例的数据正确性没有等价关系。
**AI 辅助说明:**本文整理过程使用 AI 辅助,规则、合成输入及上述结果已在本地 jq 实际运行核对;没有调用模型生成订单或虚构模型答复。