无尘阁日记

无尘阁日记

【实战手记】如何高效构建AI使用记录统计接口:从设计到落地的全流程拆解
2025-04-08

今天,我们用不到半天的时间,完成了一个企业级AI使用监控系统中最关键的数据接口之一:使用记录概览(Usage Summary)API

这个接口的目标是简单明了的:提供分页、筛选、关联信息丰富的用户调用统计数据,支持灵活的多维度分析,并保证前后端协同顺畅、数据结构直观、性能可控。

但要做对、做顺,不只是写几个SQL那么简单。这背后有一整套清晰的“接口即产品”的设计思维——而这正是我这次主导的核心部分。

以下是完整复盘,供你参考、复用、迭代。


一、项目背景与需求拆解

你在做一个AI平台,已经具备用户、组织、模型等基本数据结构,现在你需要有一个“总览接口”,让运营、财务或研发人员能快速看到:

  • 某个用户/组织/模型使用了多少 token?

  • 花了多少钱?

  • 距离各自的配额还有多少余量?

  • 这些数据,能否支持分页浏览、灵活筛选?

表面看是个“分页接口”,实则要解决数据聚合 + 多表关联 + 统计指标计算 + 实用性数据格式化四重难点。


二、接口设计思路:从“使用场景”倒推字段结构

我们没有从字段出发思考,而是反着来——从使用者视角出发,问自己五个问题:

  1. 谁最关心这些数据?→ 用户管理者、财务人员、模型分发方

  2. 他们想筛什么?→ 按用户、组织、模型、时间段等过滤

  3. 他们最想看什么?→ 当前用量、总花费、剩余额度(是否超限)

  4. 前端渲染需要哪些字段?→ 显示用户名、组织名、模型名等

  5. 最终导出或聚合会怎么用?→ 分页浏览,必要时导出明细表格

因此,字段不是“数据库里有什么”,而是“前端/使用者希望看到什么”。

我们提炼出了以下核心展示字段:

  • user_id, username

  • org_id, org_name

  • model_id, model_name

  • total_tokens_used, total_cost_usd

  • token_limit, cost_limit_usd

  • tokens_remaining, cost_remaining

  • updated_at

✍️ 这一步,是我主导的价值关键点。不是“字段搬运工”,而是“体验设计师”。


三、核心代码实现:不止是能跑,更要优雅

你原本写了一版能用的代码,我们在此基础上进行了一次系统性的重构,目标是:

  • 结构清晰:筛选、分页、数据组装三层职责分明

  • 命名统一:避免冗余缩写,提高可读性

  • 预加载替代连接:用 with() 代替不必要的 joinWith() 提升性能

  • 数据格式友好:把“剩余额度”直接计算好返回给前端,而不是让前端自己算

这是重构后的核心代码逻辑:

$query = AiUserTokenUsage::find()
    ->alias('u')
    ->with(['user', 'org', 'model'])
    ->orderBy(['u.updated_at' => SORT_DESC]);

if ($userId = $request->get('user_id')) {
    $query->andWhere(['u.user_id' => $userId]);
}
if ($orgId = $request->get('org_id')) {
    $query->andWhere(['u.org_id' => $orgId]);
}
if ($modelId = $request->get('ai_model_id')) {
    $query->andWhere(['u.ai_model_id' => $modelId]);
}

分页:

$pagination = new \yii\data\Pagination([
    'totalCount' => $query->count(),
    'pageSize' => $pageSize,
    'page' => max(0, $page - 1),
]);

数据组装:

$items = array_map(function (AiUserTokenUsage $usage) {
    return [
        'user_id' => $usage->user_id,
        'username' => $usage->user->username ?? '未知用户',
        'org_id' => $usage->org_id,
        'org_name' => $usage->org->name ?? '未知组织',
        'model_id' => $usage->ai_model_id,
        'model_name' => $usage->model->model_name ?? '(默认)',
        'total_tokens_used' => $usage->total_tokens_used,
        'total_cost_usd' => $usage->total_cost_usd,
        'token_limit' => $usage->token_limit,
        'cost_limit_usd' => $usage->cost_limit_usd,
        'tokens_remaining' => $usage->token_limit !== null 
            ? max(0, $usage->token_limit - $usage->total_tokens_used) 
            : null,
        'cost_remaining' => $usage->cost_limit_usd !== null 
            ? max(0, round($usage->cost_limit_usd - $usage->total_cost_usd, 5)) 
            : null,
        'updated_at' => $usage->updated_at,
    ];
}, $usages);

返回结构统一规范:

return [
    'items' => $items,
    '_meta' => [
        'totalCount' => $pagination->totalCount,
        'pageCount' => $pagination->getPageCount(),
        'currentPage' => $pagination->getPage() + 1,
        'perPage' => $pagination->getPageSize(),
    ],
];

四、关键设计原则回顾

  1. 字段由使用场景推导,而非表结构主导
    → 用“前端要什么”反推“后端提供什么”,是接口设计的本质。

  2. 分页逻辑内聚,避免表层冗余参数污染核心代码
    → 所有请求处理、分页、组装逻辑清晰分层,便于后期复用。

  3. 字段的语义要帮前端“减负”
    → 比如剩余配额、格式化时间、组织/模型名称等都直接给到,而不是让前端自己拼。

  4. 预加载而非 join 查询优先
    → 在无复杂多表过滤条件时,使用 with() 是 Yii2 的性能优选。


五、可拓展的改造方向

这个接口是“核心监控模块”的支点,未来可以从以下方向继续进化:

  • ✅ 支持时间范围筛选(updated_at between)

  • ✅ 增加按周/月/模型维度聚合统计接口

  • ✅ 支持 Excel 导出

  • ✅ 加入缓存或异步批量统计,解决高并发场景

  • ✅ 接入权限系统,限制非管理员访问敏感数据


六、结语:技术不只是“写出来”,更是“设计出来”

一个优秀的接口,往往不是一次性写完的,而是需要从场景出发、到数据结构设计、再到语义细节打磨,每一步都要围绕“谁在用、怎么用、怎样更轻”。

这次的协作开发,我更像是一个“接口产品经理”,而你是架构师和执行者,我们一起让技术不仅可用,而且好用、易用、复用

这才是工程师真正的生产力。