API 账单导出对不上怎么办?按请求编号、重试与时区核对
把请求明细、账单汇总和余额流水分开,再用请求编号、尝试编号、UTC 时间和结算时间排除重复扣费与跨日差异,附本地对账示例。
账单导出后对不上,先不要重复充值或把两条记录直接相加。中转站、网关和上游服务可能使用不同的请求编号、时区、重试策略和结算时间。下面用一张可复核的对账表,把“同一次请求的多条记录”“同一账单日跨时区”和“余额变化晚于用量导出”分开处理。
先定义要对上的三个数字
每次对账先固定三个口径:请求明细中的费用、账单周期汇总中的费用、余额或支付流水中的净变化。三者不一定在同一时刻更新,也不一定包含相同项目。余额净变化还可能包含充值、退款、赠送额度失效和手续费。
建议把每一行记录成 request_id、attempt_id、created_at、settled_at、model、input_units、output_units、amount、currency、status 和 parent_request_id。如果平台没有提供某个字段,填空并在备注中记录“平台未提供”,不要用本地时间或自造编号冒充平台字段。先了解API 中转站的请求与费用边界,再把本篇的编号、重试和时区规则加到你的表中。
请求编号和重试要分成两层
用户点击一次发送,可能产生一个业务请求和多个网络尝试。业务请求通常有一个本地 job_id,每次发送尝试有自己的 attempt_id;上游还可能返回一个 request_id。重试时不要把三个概念合并:同一 job_id 的第二次尝试可能成功,而第一次尝试已经在上游排队并产生费用。
对账时按以下顺序归组:先按平台提供的 request_id,没有时按 parent_request_id,最后才用本地 job_id 加时间窗口。时间窗口只是辅助,不能证明两条记录一定相同。状态为超时或客户端取消也不能直接当作“零费用”;取消的边界要结合停止生成后的取消与账单核对一文中的传递层级检查。
时区差异先转成 UTC 再比较
导出文件常把时间显示为 UTC、服务商所在时区或账户设置时区。不要直接按日期字符串连接两张表。保留原始时间和原始时区,解析后统一存成 UTC,再额外生成 beijing_date 或你的财务日期。
例如,账单显示北京时间 2026-10-29 00:05,而请求明细显示 UTC 2026-10-28 16:05,它们是同一时刻。按字符串比较会把它们放进不同日期。结算日也可能按服务商的日界线切分,因此“当天请求金额”与“当天账单金额”只能在同一日界线下比较。跨日请求还要记录 created_at 与 settled_at 两列,不能用导出时间替代发生时间。
先做一张最小对账表
可以用 CSV 或数据库保存如下字段:
| 字段 | 用途 | 不能直接推断的内容 |
|---|---|---|
request_id | 关联上游或平台明细 | 不代表一定只扣一次 |
attempt_id | 区分每次发送尝试 | 不代表尝试一定成功 |
created_at_utc | 请求进入系统的时间 | 不等于结算时间 |
settled_at_utc | 费用落账时间 | 可能为空或延迟 |
amount、currency | 对比金额和币种 | 不含另一币种的汇率差 |
status | 成功、失败、取消等 | 不能单独证明是否收费 |
source | 明细、汇总、支付流水 | 不同来源可能有延迟 |
把金额统一成同一币种前,先保留原币种和原始金额。对于赠送余额、折扣、退款或手续费,单独用负数或独立类型表示;不要为了让合计相等而修改原始金额。余额不足、分组限额和预扣造成的差异,可参照账户有余额却提示额度不足的排查方法逐层核对。
用本地脚本识别重复和跨日记录
下面的示例只处理本地模拟数据,不访问任何真实 API。它把同一 request_id 的多条明细列出来,并把北京时间日期转换为 UTC 日期,供人工确认重试和结算延迟:
function reconcile(rows) {
if (!Array.isArray(rows)) throw new TypeError('rows must be an array')
const groups = new Map()
for (const row of rows) {
if (!row || typeof row.request_id !== 'string' || !row.request_id) continue
const amount = Number(row.amount)
if (!Number.isFinite(amount)) continue
const key = row.request_id
const group = groups.get(key) ?? { requestId: key, rows: [], total: 0 }
group.rows.push(row)
group.total += amount
groups.set(key, group)
}
return [...groups.values()].map(group => ({
...group,
duplicateCandidates: group.rows.length > 1,
total: Math.round(group.total * 1e8) / 1e8,
}))
}
const sample = [
{ request_id: 'r-100', attempt_id: 'a-1', amount: '0.012', status: 'timeout' },
{ request_id: 'r-100', attempt_id: 'a-2', amount: '0.012', status: 'succeeded' },
{ request_id: 'r-101', attempt_id: 'a-3', amount: '0.004', status: 'succeeded' },
]
console.log(reconcile(sample))
这里的 duplicateCandidates 只是提示需要人工查看上游关系,不能自动把第一条费用删除。若平台文档说明失败尝试不计费,也要用该平台明细或客服回执核对,而不是凭 HTTP 状态码猜测。脚本还故意跳过缺失编号和非数字金额,避免把脏行算进结论。
余额变化对不上时按来源拆解
先导出同一账户、同一结算币种和同一日界线的四类数据:请求明细、账单汇总、充值退款流水、赠送或折扣变动。将每类数据分别求和后,再用下面的关系检查:
期末余额 = 期初余额 + 充值 - 退款 - 请求费用 + 赠送变动 - 手续费
这是对账框架,不是任何平台的固定计费规则。缺少某个项目时,把差额保留为“待解释”,并记录导出时间和筛选条件。若同一请求在账单中出现两次,先检查一次是预扣、一次是最终结算,还是网络重试产生了独立尝试;不要仅凭金额相等就合并。涉及退款时,把实付、本金、赠送和已用额度分开保存,参考中转站余额退款材料清单。
找到差异后如何留证和处理
每次导出保存原文件、查询条件、导出时间、账户标识的脱敏版本和时区说明。对外沟通时只提供必要的 request_id、金额、币种、UTC 时间和状态,不发送 API Key、完整提示词或敏感响应正文。给客服的表格增加“平台原文”“本地推断”“仍需确认”三列,避免把推测写成事实。
如果差异来自免费额度、有效期或付费回退,先查看对应规则,而不是直接提出重复退款;免费额度到期与付费切换核对可以帮助你区分余额类型。若服务商更换了域名或收款入口,先按服务条款、停用与退出条件核验保存条款版本和迁移证据。
完成修正后,再用一个新的、低风险且可取消的本地模拟任务验证导出字段是否稳定。不要用真实付费请求制造“测试账单”,也不要为了凑平账目改写原始导出。只有当请求编号、时间口径、重试关系、币种和余额变动都能被复核时,才可以把差异标记为已解释。