Procházet zdrojové kódy

新增积分消耗与Token用量统计系统设计文档

lh před 1 měsícem
rodič
revize
7a687dd594

+ 180 - 0
docs/superpowers/specs/2026-08-19-points-usage-stats-design.md

@@ -0,0 +1,180 @@
+# 积分消耗与 Token 用量统计系统设计
+
+日期:2026-08-19
+状态:待审阅
+
+## 背景与目标
+
+需要基于 `mp_user_points_details` 明细表建立一套使用量统计系统,支持按**日期、用户、模型、中转站**四个维度查看数据,供平台管理员(superadmin)查看全部用户、组织管理员(admin)查看本公司(cpid)用户的统计数据。
+
+核心诉求:
+
+- 定时任务预聚合,前端查询走统计表,保证查询性能稳定。
+- 以 **token 消耗**为核心统计指标,积分消耗作为附带指标。
+- 初始化"模型名 → 中转站"映射表,便于后续人工维护。
+- 提供前端可用的查询 API,支持日期、用户、模型、中转站筛选。
+
+## 相关背景修复
+
+审查中发现并修复了视频积分明细 token 恒为 0 的 bug:
+
+- 根因:`AIVideoGenerationService::markVideoTaskSuccess()` 更新数据库后未同步内存中 `$task->result_json`,导致随后扣费解析 token 时读到旧空值。
+- 已修复 `markVideoTaskSuccess` 同步内存属性,并新增 `points:backfill-video-tokens` 命令回填历史数据(已执行完毕)。
+- 同时补齐了 `analyzeErrorMessage`(错误友好翻译)与 `geminiGenerateText`(潜在未接线路径)两处缺失的 token 记录。
+
+## 数据库设计
+
+### 1. 模型→中转站映射表 `mp_model_relay_map`
+
+用途:维护模型名与中转站的对应关系,统计任务按模型名关联出中转站。
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint unsigned PK AI | |
+| model | varchar(255) NOT NULL UNIQUE | 模型名 |
+| relay_name | varchar(100) NOT NULL | 中转站名称 |
+| remark | varchar(255) NULL | 备注 |
+| created_at / updated_at | timestamp | |
+
+migration 中初始化以下映射(智能分析所得,后续可人工增改):
+
+| 模型 | 中转站 |
+|---|---|
+| deepseek-chat / deepseek-reasoner / deepseek-v4-flash / deepseek-v4-pro | DeepSeek 官方 |
+| deepseek-v3-2-251201 | 火山方舟 |
+| doubao-seed-2-0-mini/lite/pro-260215 | 火山方舟 |
+| doubao-seedream-4-5-251128 / doubao-seedream-5-0-260128 / doubao-seedream-5-0-lite-260128 | 火山方舟 |
+| doubao-seedance-1-5-pro-251215 / doubao-seedance-2-0-260128 / doubao-seedance-2-0-fast-260128 | 火山方舟 |
+| doubao-seedance-2.0 | 百度 |
+| zhizhen-20 / zhizhen-20-fast / zhizhen-20-mini | 智帧 |
+| gpt-5.4 / gpt-5.6-terra / gpt-5.6-sol / gpt-5.6-luna / gpt-image-2 / GptImage2 | GPT 中转站 |
+| gemini-3-pro-preview / gemini-3-flash-preview / gemini-3.1-pro-preview / gemini-3.1-flash-image-preview-t / gemini-3.6-flash | Gemini 中转站 |
+| NanoBanana2 / NanoBananaPro | NanoBanana |
+
+未命中映射的模型在统计中归为 `unknown`。
+
+### 2. 统计表 `mp_points_daily_stats`
+
+用途:按 `(stat_date, uid, model, relay)` 维度预聚合的日粒度统计。
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint unsigned PK AI | |
+| stat_date | date NOT NULL | 统计日期(按明细 created_at 归属) |
+| uid | bigint unsigned NOT NULL | 用户 ID |
+| cpid | bigint NOT NULL DEFAULT 0 | 用户所属组织(组织管理员过滤用) |
+| model | varchar(255) NOT NULL | 模型名 |
+| relay | varchar(100) NOT NULL DEFAULT 'unknown' | 中转站名称 |
+| call_count | int unsigned NOT NULL DEFAULT 0 | 调用次数 |
+| tokens_consumed | bigint NOT NULL DEFAULT 0 | token 消耗合计(核心指标) |
+| points_consumed | decimal(13,1) NOT NULL DEFAULT 0 | 积分消耗合计(附带指标) |
+| created_at / updated_at | timestamp | |
+
+索引:
+
+- 唯一键 `uk_date_uid_model_relay (stat_date, uid, model, relay)`
+- 索引 `idx_stat_date_cpid (stat_date, cpid)`
+- 索引 `idx_relay (relay)`
+- 索引 `idx_model (model)`
+
+## 定时任务
+
+### 命令 `stats:points-daily`
+
+参数:
+
+- `--date=YYYY-MM-DD`:只统计指定日期(默认昨天)
+- `--from=YYYY-MM-DD --to=YYYY-MM-DD`:批量补跑日期区间
+
+统计口径:
+
+- 数据源:`mp_user_points_details`,按 `created_at` 归属到日期。
+- 只统计 `type IN (chat, image, video)`,排除 `system/company`(发放/回收,无模型、无 token)。
+- **不过滤 `test_mode`**,测试用户记录全部纳入(按用户确认)。
+- 模型:优先取 `charge_info->model`,为空回退 `api_type`,再为空归 `unknown`。
+- 中转站:按模型名 LEFT JOIN `mp_model_relay_map` 关联,未命中归 `unknown`。
+- 指标:`call_count = COUNT(*)`,`tokens_consumed = SUM(tokens_consumed)`,`points_consumed = SUM(points_consumed)`。
+
+写入方式:按日期在事务内先删除该日旧数据再批量插入(幂等,可重复执行)。
+
+调度:`Kernel::schedule` 中每天 00:10 执行 `stats:points-daily`,`withoutOverlapping()` 防止并发。
+
+## API 设计
+
+### 1. 查询统计 `GET /api/points/stats`
+
+权限:
+
+- `superadmin`:查询全部用户数据。
+- `admin`:自动限定 `cpid` 为当前登录用户所属组织。
+- 其他角色:返回无权限错误。
+
+请求参数:
+
+| 参数 | 必填 | 说明 |
+|---|---|---|
+| start_date / end_date | 否 | 日期范围,默认近 30 天 |
+| uid | 否 | 用户 ID,支持逗号分隔多个 |
+| model | 否 | 模型名 |
+| relay | 否 | 中转站名称 |
+| group_by | 否 | 聚合维度,`date/uid/model/relay` 任意组合(如 `date,model`),默认四维全部分组 |
+| page / page_size | 否 | 分页,默认 15,最大 100 |
+
+响应(沿用现有 ApiResponse 风格):
+
+```json
+{
+  "code": 0,
+  "data": {
+    "list": [
+      {
+        "stat_date": "2026-08-18",
+        "uid": 142858,
+        "nickname": "用户昵称",
+        "model": "zhizhen-20",
+        "relay": "智帧",
+        "call_count": 10,
+        "tokens_consumed": 1309900,
+        "points_consumed": 840.0
+      }
+    ],
+    "meta": {
+      "current_page": 1,
+      "last_page": 10,
+      "per_page": 15,
+      "total": 145
+    },
+    "summary": {
+      "call_count": 1000,
+      "tokens_consumed": 100000000,
+      "points_consumed": 80000.0
+    }
+  }
+}
+```
+
+### 2. 筛选项来源 `GET /api/points/stats/filters`
+
+返回当前权限范围内的可选值,供前端下拉框使用:
+
+- 用户列表(id + 昵称/账号)
+- 模型列表
+- 中转站列表
+- 可统计日期范围(最早/最晚 stat_date)
+
+## 目录与文件规划
+
+- `database/migrations/xxxx_create_mp_model_relay_map_table.php`
+- `database/migrations/xxxx_create_mp_points_daily_stats_table.php`
+- `app/Console/Commands/PointsDailyStatsCommand.php`
+- `app/Services/PointsStatsService.php`
+- `app/Http/Controllers/Points/PointsStatsController.php`
+- `app/Transformer/Points/PointsStatsTransformer.php`(如有需要)
+- `routes/api.php` 新增路由
+
+## 测试计划
+
+- 定时任务:手动执行 `--date` 统计结果与明细表 SQL 对账一致。
+- 权限:superadmin 可见全部、admin 仅本组织、其他角色拒绝。
+- 筛选:日期、用户、模型、中转站、group_by 组合。
+- 幂等:重复执行同一日期统计不产生脏数据。