问题描述
当账号池很大时,/api/admin/accounts 返回的部分账号会出现 billed_5h / billed_7d 为 null 的情况,即使这些账号在 usage_5h_detail / usage_7d_detail 里已经有明确的计费金额。
结果是管理后台账号页的“成本”列显示 -,看起来像这个账号没有成本数据,但实际上成本数据存在。
实际观察
在一个约 6k 账号的部署中:
GET /api/admin/accounts?limit=10000 耗时约 5.10s
- 一些靠后的账号出现了非零用量成本,但
billed_* 为空,例如:
{
"usage_7d_detail": {
"requests": 95,
"tokens": 10698650,
"account_billed": 7.953839999999999,
"user_billed": 7.953839999999999
},
"billed_5h": null,
"billed_7d": null
}
前端成本列只读取 billed_5h / billed_7d,所以会显示 -,但 usage_7d_detail.account_billed 里其实已经有实际成本。
也能观察到多条记录满足:
usage_7d_detail.account_billed > 0
billed_7d == null
疑似原因
admin/handler.go 的 ListAccounts 使用了 5 秒请求上下文:
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
账号列表前半部分会先批量聚合 usage5h / usage7d,但后面填充 Billed5h / Billed7d 时,又对每个账号单独查询:
for i := range accounts {
acc, ok := accountMap[accounts[i].ID]
if !ok {
continue
}
if t := acc.GetReset5hAt(); !t.IsZero() {
billed, err := h.db.GetAccountBilledSince(ctx, accounts[i].ID, t.Add(-5*time.Hour))
if err == nil {
accounts[i].Billed5h = &billed
}
}
if t := acc.GetReset7dAt(); !t.IsZero() {
billed, err := h.db.GetAccountBilledSince(ctx, accounts[i].ID, t.AddDate(0, 0, -7))
if err == nil {
accounts[i].Billed7d = &billed
}
}
}
GetAccountBilledSince 是单账号查询:
SELECT COALESCE(SUM(account_billed), 0)
FROM usage_logs
WHERE account_id = $1 AND created_at >= $2 AND status_code <> 499
账号数量达到几千时,这里会变成 N+1 查询。当前面的处理和部分账号成本查询耗尽 5 秒上下文后,后续账号的 GetAccountBilledSince 会失败。但代码只是 if err == nil 才赋值,错误被静默忽略,最终这些账号的 billed_5h / billed_7d 保持 null。
HTTP 接口本身仍然可能返回 200,所以前端看起来只是部分账号成本列变成 -。
期望行为
管理后台账号页不应该在账号已经有计费数据时显示 -。
至少应满足:
billed_5h / billed_7d 能稳定为所有账号计算出来;或
- 如果已有批量聚合结果可用,直接复用
usage_5h_detail.account_billed / usage_7d_detail.account_billed;或
- 成本窗口改为批量查询,避免账号多时 N+1 查询超时。
建议修复
避免在 ListAccounts 里对每个账号逐个调用 GetAccountBilledSince。
可选方案:
- 如果“成本列”的语义只是最近 5h / 7d,可直接复用已批量聚合的
usage5h[row.ID].AccountBilled 和 usage7d[row.ID].AccountBilled。
- 如果必须按账号的 reset 时间对齐窗口,增加批量查询:一次性传入或 join 每个账号的
(account_id, since),统一聚合所有账号的 billed 数据。
- 如果成本查询失败,不要静默保留
null;至少记录日志,或者在 API 中提供明确的 partial-data 指示。
问题描述
当账号池很大时,
/api/admin/accounts返回的部分账号会出现billed_5h/billed_7d为null的情况,即使这些账号在usage_5h_detail/usage_7d_detail里已经有明确的计费金额。结果是管理后台账号页的“成本”列显示
-,看起来像这个账号没有成本数据,但实际上成本数据存在。实际观察
在一个约 6k 账号的部署中:
GET /api/admin/accounts?limit=10000耗时约5.10sbilled_*为空,例如:{ "usage_7d_detail": { "requests": 95, "tokens": 10698650, "account_billed": 7.953839999999999, "user_billed": 7.953839999999999 }, "billed_5h": null, "billed_7d": null }前端成本列只读取
billed_5h/billed_7d,所以会显示-,但usage_7d_detail.account_billed里其实已经有实际成本。也能观察到多条记录满足:
疑似原因
admin/handler.go的ListAccounts使用了 5 秒请求上下文:账号列表前半部分会先批量聚合
usage5h/usage7d,但后面填充Billed5h/Billed7d时,又对每个账号单独查询:GetAccountBilledSince是单账号查询:账号数量达到几千时,这里会变成 N+1 查询。当前面的处理和部分账号成本查询耗尽 5 秒上下文后,后续账号的
GetAccountBilledSince会失败。但代码只是if err == nil才赋值,错误被静默忽略,最终这些账号的billed_5h/billed_7d保持null。HTTP 接口本身仍然可能返回 200,所以前端看起来只是部分账号成本列变成
-。期望行为
管理后台账号页不应该在账号已经有计费数据时显示
-。至少应满足:
billed_5h/billed_7d能稳定为所有账号计算出来;或usage_5h_detail.account_billed/usage_7d_detail.account_billed;或建议修复
避免在
ListAccounts里对每个账号逐个调用GetAccountBilledSince。可选方案:
usage5h[row.ID].AccountBilled和usage7d[row.ID].AccountBilled。(account_id, since),统一聚合所有账号的 billed 数据。null;至少记录日志,或者在 API 中提供明确的 partial-data 指示。