Skip to content

账号池较大时管理后台成本列可能显示 '-' #186

@yanhao98

Description

@yanhao98

问题描述

当账号池很大时,/api/admin/accounts 返回的部分账号会出现 billed_5h / billed_7dnull 的情况,即使这些账号在 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.goListAccounts 使用了 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

可选方案:

  1. 如果“成本列”的语义只是最近 5h / 7d,可直接复用已批量聚合的 usage5h[row.ID].AccountBilledusage7d[row.ID].AccountBilled
  2. 如果必须按账号的 reset 时间对齐窗口,增加批量查询:一次性传入或 join 每个账号的 (account_id, since),统一聚合所有账号的 billed 数据。
  3. 如果成本查询失败,不要静默保留 null;至少记录日志,或者在 API 中提供明确的 partial-data 指示。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions