usage_log_repo_dashboard.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. )
  11. // getPerformanceStats 获取 RPM 和 TPM(近5分钟平均值,可选按用户过滤)
  12. func (r *usageLogRepository) getPerformanceStats(ctx context.Context, userID int64) (rpm, tpm int64, err error) {
  13. fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
  14. query := `
  15. SELECT
  16. COUNT(*) as request_count,
  17. COALESCE(SUM(input_tokens + output_tokens), 0) as token_count
  18. FROM usage_logs
  19. WHERE created_at >= $1`
  20. args := []any{fiveMinutesAgo}
  21. if userID > 0 {
  22. query += " AND user_id = $2"
  23. args = append(args, userID)
  24. }
  25. var requestCount int64
  26. var tokenCount int64
  27. if err := scanSingleRow(ctx, r.sql, query, args, &requestCount, &tokenCount); err != nil {
  28. return 0, 0, err
  29. }
  30. return requestCount / 5, tokenCount / 5, nil
  31. }
  32. // UserStats 用户使用统计
  33. type UserStats struct {
  34. TotalRequests int64 `json:"total_requests"`
  35. TotalTokens int64 `json:"total_tokens"`
  36. TotalCost float64 `json:"total_cost"`
  37. InputTokens int64 `json:"input_tokens"`
  38. OutputTokens int64 `json:"output_tokens"`
  39. CacheReadTokens int64 `json:"cache_read_tokens"`
  40. }
  41. func (r *usageLogRepository) GetUserStats(ctx context.Context, userID int64, startTime, endTime time.Time) (*UserStats, error) {
  42. query := `
  43. SELECT
  44. COUNT(*) as total_requests,
  45. COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
  46. COALESCE(SUM(actual_cost), 0) as total_cost,
  47. COALESCE(SUM(input_tokens), 0) as input_tokens,
  48. COALESCE(SUM(output_tokens), 0) as output_tokens,
  49. COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens
  50. FROM usage_logs
  51. WHERE user_id = $1 AND created_at >= $2 AND created_at < $3
  52. `
  53. stats := &UserStats{}
  54. if err := scanSingleRow(
  55. ctx,
  56. r.sql,
  57. query,
  58. []any{userID, startTime, endTime},
  59. &stats.TotalRequests,
  60. &stats.TotalTokens,
  61. &stats.TotalCost,
  62. &stats.InputTokens,
  63. &stats.OutputTokens,
  64. &stats.CacheReadTokens,
  65. ); err != nil {
  66. return nil, err
  67. }
  68. return stats, nil
  69. }
  70. // DashboardStats 仪表盘统计
  71. type DashboardStats = usagestats.DashboardStats
  72. func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) {
  73. stats := &DashboardStats{}
  74. now := timezone.Now()
  75. todayStart := timezone.Today()
  76. if err := r.fillDashboardEntityStats(ctx, stats, todayStart, now); err != nil {
  77. return nil, err
  78. }
  79. if err := r.fillDashboardUsageStatsAggregated(ctx, stats, todayStart, now); err != nil {
  80. return nil, err
  81. }
  82. rpm, tpm, err := r.getPerformanceStats(ctx, 0)
  83. if err != nil {
  84. return nil, err
  85. }
  86. stats.Rpm = rpm
  87. stats.Tpm = tpm
  88. return stats, nil
  89. }
  90. func (r *usageLogRepository) GetDashboardStatsWithRange(ctx context.Context, start, end time.Time) (*DashboardStats, error) {
  91. startUTC := start.UTC()
  92. endUTC := end.UTC()
  93. if !endUTC.After(startUTC) {
  94. return nil, errors.New("统计时间范围无效")
  95. }
  96. stats := &DashboardStats{}
  97. now := timezone.Now()
  98. todayStart := timezone.Today()
  99. if err := r.fillDashboardEntityStats(ctx, stats, todayStart, now); err != nil {
  100. return nil, err
  101. }
  102. if err := r.fillDashboardUsageStatsFromUsageLogs(ctx, stats, startUTC, endUTC, todayStart, now); err != nil {
  103. return nil, err
  104. }
  105. rpm, tpm, err := r.getPerformanceStats(ctx, 0)
  106. if err != nil {
  107. return nil, err
  108. }
  109. stats.Rpm = rpm
  110. stats.Tpm = tpm
  111. return stats, nil
  112. }
  113. func (r *usageLogRepository) fillDashboardEntityStats(ctx context.Context, stats *DashboardStats, todayUTC, now time.Time) error {
  114. userStatsQuery := `
  115. SELECT
  116. COUNT(*) as total_users,
  117. COUNT(CASE WHEN created_at >= $1 THEN 1 END) as today_new_users
  118. FROM users
  119. WHERE deleted_at IS NULL
  120. `
  121. if err := scanSingleRow(
  122. ctx,
  123. r.sql,
  124. userStatsQuery,
  125. []any{todayUTC},
  126. &stats.TotalUsers,
  127. &stats.TodayNewUsers,
  128. ); err != nil {
  129. return err
  130. }
  131. apiKeyStatsQuery := `
  132. SELECT
  133. COUNT(*) as total_api_keys,
  134. COUNT(CASE WHEN status = $1 THEN 1 END) as active_api_keys
  135. FROM api_keys
  136. WHERE deleted_at IS NULL
  137. `
  138. if err := scanSingleRow(
  139. ctx,
  140. r.sql,
  141. apiKeyStatsQuery,
  142. []any{service.StatusActive},
  143. &stats.TotalAPIKeys,
  144. &stats.ActiveAPIKeys,
  145. ); err != nil {
  146. return err
  147. }
  148. accountStatsQuery := `
  149. SELECT
  150. COUNT(*) as total_accounts,
  151. COUNT(CASE WHEN status = $1 AND schedulable = true THEN 1 END) as normal_accounts,
  152. COUNT(CASE WHEN status = $2 THEN 1 END) as error_accounts,
  153. COUNT(CASE WHEN rate_limited_at IS NOT NULL AND rate_limit_reset_at > $3 THEN 1 END) as ratelimit_accounts,
  154. COUNT(CASE WHEN overload_until IS NOT NULL AND overload_until > $4 THEN 1 END) as overload_accounts
  155. FROM accounts
  156. WHERE deleted_at IS NULL
  157. `
  158. if err := scanSingleRow(
  159. ctx,
  160. r.sql,
  161. accountStatsQuery,
  162. []any{service.StatusActive, service.StatusError, now, now},
  163. &stats.TotalAccounts,
  164. &stats.NormalAccounts,
  165. &stats.ErrorAccounts,
  166. &stats.RateLimitAccounts,
  167. &stats.OverloadAccounts,
  168. ); err != nil {
  169. return err
  170. }
  171. return nil
  172. }
  173. func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Context, stats *DashboardStats, todayUTC, now time.Time) error {
  174. totalStatsQuery := `
  175. SELECT
  176. COALESCE(SUM(total_requests), 0) as total_requests,
  177. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  178. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  179. COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
  180. COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
  181. COALESCE(SUM(total_cost), 0) as total_cost,
  182. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  183. COALESCE(SUM(account_cost), 0) as total_account_cost,
  184. COALESCE(SUM(total_duration_ms), 0) as total_duration_ms
  185. FROM usage_dashboard_daily
  186. `
  187. var totalDurationMs int64
  188. if err := scanSingleRow(
  189. ctx,
  190. r.sql,
  191. totalStatsQuery,
  192. nil,
  193. &stats.TotalRequests,
  194. &stats.TotalInputTokens,
  195. &stats.TotalOutputTokens,
  196. &stats.TotalCacheCreationTokens,
  197. &stats.TotalCacheReadTokens,
  198. &stats.TotalCost,
  199. &stats.TotalActualCost,
  200. &stats.TotalAccountCost,
  201. &totalDurationMs,
  202. ); err != nil {
  203. return err
  204. }
  205. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
  206. if stats.TotalRequests > 0 {
  207. stats.AverageDurationMs = float64(totalDurationMs) / float64(stats.TotalRequests)
  208. }
  209. todayStatsQuery := `
  210. SELECT
  211. total_requests as today_requests,
  212. input_tokens as today_input_tokens,
  213. output_tokens as today_output_tokens,
  214. cache_creation_tokens as today_cache_creation_tokens,
  215. cache_read_tokens as today_cache_read_tokens,
  216. total_cost as today_cost,
  217. actual_cost as today_actual_cost,
  218. account_cost as today_account_cost,
  219. active_users as active_users
  220. FROM usage_dashboard_daily
  221. WHERE bucket_date = $1::date
  222. `
  223. if err := scanSingleRow(
  224. ctx,
  225. r.sql,
  226. todayStatsQuery,
  227. []any{todayUTC},
  228. &stats.TodayRequests,
  229. &stats.TodayInputTokens,
  230. &stats.TodayOutputTokens,
  231. &stats.TodayCacheCreationTokens,
  232. &stats.TodayCacheReadTokens,
  233. &stats.TodayCost,
  234. &stats.TodayActualCost,
  235. &stats.TodayAccountCost,
  236. &stats.ActiveUsers,
  237. ); err != nil {
  238. if err != sql.ErrNoRows {
  239. return err
  240. }
  241. }
  242. stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
  243. hourlyActiveQuery := `
  244. SELECT active_users
  245. FROM usage_dashboard_hourly
  246. WHERE bucket_start = $1
  247. `
  248. hourStart := now.In(timezone.Location()).Truncate(time.Hour)
  249. if err := scanSingleRow(ctx, r.sql, hourlyActiveQuery, []any{hourStart}, &stats.HourlyActiveUsers); err != nil {
  250. if err != sql.ErrNoRows {
  251. return err
  252. }
  253. }
  254. return nil
  255. }
  256. func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Context, stats *DashboardStats, startUTC, endUTC, todayUTC, now time.Time) error {
  257. todayEnd := todayUTC.Add(24 * time.Hour)
  258. combinedStatsQuery := `
  259. WITH scoped AS (
  260. SELECT
  261. created_at,
  262. input_tokens,
  263. output_tokens,
  264. cache_creation_tokens,
  265. cache_read_tokens,
  266. total_cost,
  267. actual_cost,
  268. COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1) AS account_cost,
  269. COALESCE(duration_ms, 0) AS duration_ms
  270. FROM usage_logs
  271. WHERE created_at >= LEAST($1::timestamptz, $3::timestamptz)
  272. AND created_at < GREATEST($2::timestamptz, $4::timestamptz)
  273. )
  274. SELECT
  275. COUNT(*) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz) AS total_requests,
  276. COALESCE(SUM(input_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_input_tokens,
  277. COALESCE(SUM(output_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_output_tokens,
  278. COALESCE(SUM(cache_creation_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_cache_creation_tokens,
  279. COALESCE(SUM(cache_read_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_cache_read_tokens,
  280. COALESCE(SUM(total_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_cost,
  281. COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_actual_cost,
  282. COALESCE(SUM(account_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_account_cost,
  283. COALESCE(SUM(duration_ms) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_duration_ms,
  284. COUNT(*) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz) AS today_requests,
  285. COALESCE(SUM(input_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_input_tokens,
  286. COALESCE(SUM(output_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_output_tokens,
  287. COALESCE(SUM(cache_creation_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cache_creation_tokens,
  288. COALESCE(SUM(cache_read_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cache_read_tokens,
  289. COALESCE(SUM(total_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cost,
  290. COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_actual_cost,
  291. COALESCE(SUM(account_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_account_cost
  292. FROM scoped
  293. `
  294. var totalDurationMs int64
  295. if err := scanSingleRow(
  296. ctx,
  297. r.sql,
  298. combinedStatsQuery,
  299. []any{startUTC, endUTC, todayUTC, todayEnd},
  300. &stats.TotalRequests,
  301. &stats.TotalInputTokens,
  302. &stats.TotalOutputTokens,
  303. &stats.TotalCacheCreationTokens,
  304. &stats.TotalCacheReadTokens,
  305. &stats.TotalCost,
  306. &stats.TotalActualCost,
  307. &stats.TotalAccountCost,
  308. &totalDurationMs,
  309. &stats.TodayRequests,
  310. &stats.TodayInputTokens,
  311. &stats.TodayOutputTokens,
  312. &stats.TodayCacheCreationTokens,
  313. &stats.TodayCacheReadTokens,
  314. &stats.TodayCost,
  315. &stats.TodayActualCost,
  316. &stats.TodayAccountCost,
  317. ); err != nil {
  318. return err
  319. }
  320. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
  321. if stats.TotalRequests > 0 {
  322. stats.AverageDurationMs = float64(totalDurationMs) / float64(stats.TotalRequests)
  323. }
  324. stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
  325. hourStart := now.UTC().Truncate(time.Hour)
  326. hourEnd := hourStart.Add(time.Hour)
  327. activeUsersQuery := `
  328. WITH scoped AS (
  329. SELECT user_id, created_at
  330. FROM usage_logs
  331. WHERE created_at >= LEAST($1::timestamptz, $3::timestamptz)
  332. AND created_at < GREATEST($2::timestamptz, $4::timestamptz)
  333. )
  334. SELECT
  335. COUNT(DISTINCT CASE WHEN created_at >= $1::timestamptz AND created_at < $2::timestamptz THEN user_id END) AS active_users,
  336. COUNT(DISTINCT CASE WHEN created_at >= $3::timestamptz AND created_at < $4::timestamptz THEN user_id END) AS hourly_active_users
  337. FROM scoped
  338. `
  339. if err := scanSingleRow(ctx, r.sql, activeUsersQuery, []any{todayUTC, todayEnd, hourStart, hourEnd}, &stats.ActiveUsers, &stats.HourlyActiveUsers); err != nil {
  340. return err
  341. }
  342. return nil
  343. }
  344. // UserDashboardStats 用户仪表盘统计
  345. type UserDashboardStats = usagestats.UserDashboardStats
  346. // PlatformDashboardStats 单平台用量明细
  347. type PlatformDashboardStats = usagestats.PlatformDashboardStats
  348. // GetUserDashboardStats 获取用户专属的仪表盘统计
  349. func (r *usageLogRepository) GetUserDashboardStats(ctx context.Context, userID int64) (*UserDashboardStats, error) {
  350. stats := &UserDashboardStats{}
  351. today := timezone.Today()
  352. // API Key 统计
  353. if err := scanSingleRow(
  354. ctx,
  355. r.sql,
  356. "SELECT COUNT(*) FROM api_keys WHERE user_id = $1 AND deleted_at IS NULL",
  357. []any{userID},
  358. &stats.TotalAPIKeys,
  359. ); err != nil {
  360. return nil, err
  361. }
  362. if err := scanSingleRow(
  363. ctx,
  364. r.sql,
  365. "SELECT COUNT(*) FROM api_keys WHERE user_id = $1 AND status = $2 AND deleted_at IS NULL",
  366. []any{userID, service.StatusActive},
  367. &stats.ActiveAPIKeys,
  368. ); err != nil {
  369. return nil, err
  370. }
  371. // 累计 Token 统计
  372. totalStatsQuery := `
  373. SELECT
  374. COUNT(*) as total_requests,
  375. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  376. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  377. COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
  378. COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
  379. COALESCE(SUM(total_cost), 0) as total_cost,
  380. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  381. COALESCE(AVG(duration_ms), 0) as avg_duration_ms
  382. FROM usage_logs
  383. WHERE user_id = $1
  384. `
  385. if err := scanSingleRow(
  386. ctx,
  387. r.sql,
  388. totalStatsQuery,
  389. []any{userID},
  390. &stats.TotalRequests,
  391. &stats.TotalInputTokens,
  392. &stats.TotalOutputTokens,
  393. &stats.TotalCacheCreationTokens,
  394. &stats.TotalCacheReadTokens,
  395. &stats.TotalCost,
  396. &stats.TotalActualCost,
  397. &stats.AverageDurationMs,
  398. ); err != nil {
  399. return nil, err
  400. }
  401. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
  402. // 今日 Token 统计
  403. todayStatsQuery := `
  404. SELECT
  405. COUNT(*) as today_requests,
  406. COALESCE(SUM(input_tokens), 0) as today_input_tokens,
  407. COALESCE(SUM(output_tokens), 0) as today_output_tokens,
  408. COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens,
  409. COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens,
  410. COALESCE(SUM(total_cost), 0) as today_cost,
  411. COALESCE(SUM(actual_cost), 0) as today_actual_cost
  412. FROM usage_logs
  413. WHERE user_id = $1 AND created_at >= $2
  414. `
  415. if err := scanSingleRow(
  416. ctx,
  417. r.sql,
  418. todayStatsQuery,
  419. []any{userID, today},
  420. &stats.TodayRequests,
  421. &stats.TodayInputTokens,
  422. &stats.TodayOutputTokens,
  423. &stats.TodayCacheCreationTokens,
  424. &stats.TodayCacheReadTokens,
  425. &stats.TodayCost,
  426. &stats.TodayActualCost,
  427. ); err != nil {
  428. return nil, err
  429. }
  430. stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
  431. // 性能指标:RPM 和 TPM(最近1分钟,仅统计该用户的请求)
  432. rpm, tpm, err := r.getPerformanceStats(ctx, userID)
  433. if err != nil {
  434. return nil, err
  435. }
  436. stats.Rpm = rpm
  437. stats.Tpm = tpm
  438. // 按"有效平台"维度拆分(group.platform 优先,否则 account.platform)。
  439. // 与 ops 路径口径一致;HAVING 过滤掉无法确定平台的行(避免出现空字符串平台)。
  440. // 与上面 totalStatsQuery/todayStatsQuery 的总值可能略微差异,原因有二:
  441. // 1) 无平台归属的极少数行(group/account 都没 platform)会被 HAVING 排除;
  442. // 2) usageLogSuccessFilterUL 会把 actual_cost = 0 的失败 placeholder 行排除,
  443. // 而 totalStatsQuery/todayStatsQuery 没有这层过滤、会把这些行的 request 计数算进去。
  444. platformQuery := `
  445. SELECT
  446. ` + usageLogEffectivePlatformExpr + ` as platform,
  447. COUNT(*) as total_requests,
  448. COALESCE(SUM(ul.input_tokens + ul.output_tokens + ul.cache_creation_tokens + ul.cache_read_tokens), 0) as total_tokens,
  449. COALESCE(SUM(ul.actual_cost), 0) as total_actual_cost,
  450. COUNT(*) FILTER (WHERE ul.created_at >= $2) as today_requests,
  451. COALESCE(SUM(ul.input_tokens + ul.output_tokens + ul.cache_creation_tokens + ul.cache_read_tokens) FILTER (WHERE ul.created_at >= $2), 0) as today_tokens,
  452. COALESCE(SUM(ul.actual_cost) FILTER (WHERE ul.created_at >= $2), 0) as today_actual_cost
  453. FROM usage_logs ul
  454. LEFT JOIN groups g ON g.id = ul.group_id
  455. LEFT JOIN accounts a ON a.id = ul.account_id
  456. WHERE ul.user_id = $1
  457. AND ` + usageLogSuccessFilterUL + `
  458. GROUP BY ` + usageLogEffectivePlatformExpr + `
  459. HAVING ` + usageLogEffectivePlatformExpr + ` IS NOT NULL AND ` + usageLogEffectivePlatformExpr + ` <> ''
  460. ORDER BY total_actual_cost DESC
  461. `
  462. rows, err := r.sql.QueryContext(ctx, platformQuery, userID, today)
  463. if err != nil {
  464. return nil, err
  465. }
  466. for rows.Next() {
  467. var p PlatformDashboardStats
  468. if err := rows.Scan(
  469. &p.Platform,
  470. &p.TotalRequests,
  471. &p.TotalTokens,
  472. &p.TotalActualCost,
  473. &p.TodayRequests,
  474. &p.TodayTokens,
  475. &p.TodayActualCost,
  476. ); err != nil {
  477. _ = rows.Close()
  478. return nil, err
  479. }
  480. stats.ByPlatform = append(stats.ByPlatform, p)
  481. }
  482. if err := rows.Close(); err != nil {
  483. return nil, err
  484. }
  485. if err := rows.Err(); err != nil {
  486. return nil, err
  487. }
  488. return stats, nil
  489. }
  490. // getPerformanceStatsByAPIKey 获取指定 API Key 的 RPM 和 TPM(近5分钟平均值)
  491. func (r *usageLogRepository) getPerformanceStatsByAPIKey(ctx context.Context, apiKeyID int64) (rpm, tpm int64, err error) {
  492. fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
  493. query := `
  494. SELECT
  495. COUNT(*) as request_count,
  496. COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as token_count
  497. FROM usage_logs
  498. WHERE created_at >= $1 AND api_key_id = $2`
  499. args := []any{fiveMinutesAgo, apiKeyID}
  500. var requestCount int64
  501. var tokenCount int64
  502. if err := scanSingleRow(ctx, r.sql, query, args, &requestCount, &tokenCount); err != nil {
  503. return 0, 0, err
  504. }
  505. return requestCount / 5, tokenCount / 5, nil
  506. }
  507. // GetAPIKeyDashboardStats 获取指定 API Key 的仪表盘统计(按 api_key_id 过滤)
  508. func (r *usageLogRepository) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*UserDashboardStats, error) {
  509. stats := &UserDashboardStats{}
  510. today := timezone.Today()
  511. // API Key 维度不需要统计 key 数量,设为 1
  512. stats.TotalAPIKeys = 1
  513. stats.ActiveAPIKeys = 1
  514. // 累计 Token 统计
  515. totalStatsQuery := `
  516. SELECT
  517. COUNT(*) as total_requests,
  518. COALESCE(SUM(input_tokens), 0) as total_input_tokens,
  519. COALESCE(SUM(output_tokens), 0) as total_output_tokens,
  520. COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
  521. COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
  522. COALESCE(SUM(total_cost), 0) as total_cost,
  523. COALESCE(SUM(actual_cost), 0) as total_actual_cost,
  524. COALESCE(AVG(duration_ms), 0) as avg_duration_ms
  525. FROM usage_logs
  526. WHERE api_key_id = $1
  527. `
  528. if err := scanSingleRow(
  529. ctx,
  530. r.sql,
  531. totalStatsQuery,
  532. []any{apiKeyID},
  533. &stats.TotalRequests,
  534. &stats.TotalInputTokens,
  535. &stats.TotalOutputTokens,
  536. &stats.TotalCacheCreationTokens,
  537. &stats.TotalCacheReadTokens,
  538. &stats.TotalCost,
  539. &stats.TotalActualCost,
  540. &stats.AverageDurationMs,
  541. ); err != nil {
  542. return nil, err
  543. }
  544. stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
  545. // 今日 Token 统计
  546. todayStatsQuery := `
  547. SELECT
  548. COUNT(*) as today_requests,
  549. COALESCE(SUM(input_tokens), 0) as today_input_tokens,
  550. COALESCE(SUM(output_tokens), 0) as today_output_tokens,
  551. COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens,
  552. COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens,
  553. COALESCE(SUM(total_cost), 0) as today_cost,
  554. COALESCE(SUM(actual_cost), 0) as today_actual_cost
  555. FROM usage_logs
  556. WHERE api_key_id = $1 AND created_at >= $2
  557. `
  558. if err := scanSingleRow(
  559. ctx,
  560. r.sql,
  561. todayStatsQuery,
  562. []any{apiKeyID, today},
  563. &stats.TodayRequests,
  564. &stats.TodayInputTokens,
  565. &stats.TodayOutputTokens,
  566. &stats.TodayCacheCreationTokens,
  567. &stats.TodayCacheReadTokens,
  568. &stats.TodayCost,
  569. &stats.TodayActualCost,
  570. ); err != nil {
  571. return nil, err
  572. }
  573. stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
  574. // 性能指标:RPM 和 TPM(最近5分钟,按 API Key 过滤)
  575. rpm, tpm, err := r.getPerformanceStatsByAPIKey(ctx, apiKeyID)
  576. if err != nil {
  577. return nil, err
  578. }
  579. stats.Rpm = rpm
  580. stats.Tpm = tpm
  581. return stats, nil
  582. }