From 59152af3d8d650ca4aea3fa0f65f93117cbd08d0 Mon Sep 17 00:00:00 2001 From: Noxiven <41963680+Noxiven@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:25:07 +0800 Subject: [PATCH 1/2] refactor: streamline chat UI and session handling --- backend/docs/docs.go | 900 +++++++++++++----- backend/docs/swagger.json | 900 +++++++++++++----- backend/docs/swagger.yaml | 719 +++++++++----- backend/internal/app/app.go | 1 + backend/internal/application/admin/service.go | 10 + .../application/admin/service_logs.go | 8 + .../internal/application/billing/service.go | 81 ++ backend/internal/domain/billing/types.go | 42 + .../postgres/billing/repository.go | 239 +++++ backend/internal/repository/billing.go | 18 + backend/internal/transport/http/admin/dto.go | 122 +++ .../internal/transport/http/admin/handler.go | 86 ++ .../internal/transport/http/admin/router.go | 1 + frontend/app/(admin)/admin/page.tsx | 4 +- .../app/(admin)/admin/statistics/page.tsx | 5 + frontend/features/admin/api/index.ts | 1 + frontend/features/admin/api/statistics.ts | 79 ++ .../components/admin-date-range-filter.tsx | 5 +- .../admin/components/admin-sidebar.tsx | 3 +- .../statistics/admin-statistics-charts.tsx | 526 ++++++++++ .../admin-statistics-user-filter.tsx | 149 +++ .../sections/statistics/admin-statistics.tsx | 314 ++++++ .../admin/hooks/use-admin-statistics.ts | 220 +++++ .../features/admin/model/admin-sections.ts | 3 +- frontend/i18n/messages.ts | 5 + .../i18n/messages/en-US/admin-statistics.json | 62 ++ frontend/i18n/messages/en-US/admin-users.json | 1 + .../i18n/messages/zh-CN/admin-statistics.json | 62 ++ frontend/i18n/messages/zh-CN/admin-users.json | 1 + 29 files changed, 3898 insertions(+), 669 deletions(-) create mode 100644 frontend/app/(admin)/admin/statistics/page.tsx create mode 100644 frontend/features/admin/api/statistics.ts create mode 100644 frontend/features/admin/components/sections/statistics/admin-statistics-charts.tsx create mode 100644 frontend/features/admin/components/sections/statistics/admin-statistics-user-filter.tsx create mode 100644 frontend/features/admin/components/sections/statistics/admin-statistics.tsx create mode 100644 frontend/features/admin/hooks/use-admin-statistics.ts create mode 100644 frontend/i18n/messages/en-US/admin-statistics.json create mode 100644 frontend/i18n/messages/zh-CN/admin-statistics.json diff --git a/backend/docs/docs.go b/backend/docs/docs.go index cb90887d..c4cb7664 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -331,13 +331,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.AuditLogListResponseDoc" + "$ref": "#/definitions/admin.AuditLogListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -587,6 +587,48 @@ const docTemplate = `{ } } }, + "/admin/billing/official-pricing/openrouter": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "从 storage 缓存读取 OpenRouter 模型定价;缓存不存在、过期或 refresh=true 时由后端刷新。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-billing" + ], + "summary": "管理员获取 OpenRouter 官方模型定价", + "parameters": [ + { + "type": "boolean", + "description": "强制刷新缓存", + "name": "refresh", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingResponseDoc" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/internal_transport_http_billing.ErrorDoc" + } + } + } + } + }, "/admin/billing/plans/{id}": { "patch": { "security": [ @@ -1061,19 +1103,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UsageLogListResponseDoc" + "$ref": "#/definitions/admin.UsageLogListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -1169,19 +1211,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ConversationEventListResponseDoc" + "$ref": "#/definitions/admin.ConversationEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -1212,7 +1254,7 @@ const docTemplate = `{ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3228,7 +3270,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsRequest" + "$ref": "#/definitions/admin.CleanupLogsRequest" } } ], @@ -3236,19 +3278,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsResponseDoc" + "$ref": "#/definitions/admin.CleanupLogsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3285,25 +3327,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc" + "$ref": "#/definitions/admin.ModelPermissionGroupsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3339,7 +3381,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.SetModelPermissionGroupsRequest" + "$ref": "#/definitions/admin.SetModelPermissionGroupsRequest" } } ], @@ -3347,25 +3389,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc" + "$ref": "#/definitions/admin.ModelPermissionGroupsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3455,19 +3497,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PaymentOrderListResponseDoc" + "$ref": "#/definitions/admin.PaymentOrderListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3495,13 +3537,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupListResponseDoc" + "$ref": "#/definitions/admin.PermissionGroupListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3530,7 +3572,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CreatePermissionGroupRequest" + "$ref": "#/definitions/admin.CreatePermissionGroupRequest" } } ], @@ -3538,19 +3580,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc" + "$ref": "#/definitions/admin.PermissionGroupDataResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3587,31 +3629,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.DeletePermissionGroupResponseDoc" + "$ref": "#/definitions/admin.DeletePermissionGroupResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3647,7 +3689,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UpdatePermissionGroupRequest" + "$ref": "#/definitions/admin.UpdatePermissionGroupRequest" } } ], @@ -3655,25 +3697,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc" + "$ref": "#/definitions/admin.PermissionGroupDataResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3710,25 +3752,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponseDoc" + "$ref": "#/definitions/admin.GroupModelsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3764,7 +3806,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.SetGroupModelsRequest" + "$ref": "#/definitions/admin.SetGroupModelsRequest" } } ], @@ -3772,25 +3814,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponseDoc" + "$ref": "#/definitions/admin.GroupModelsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3827,31 +3869,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponseDoc" + "$ref": "#/definitions/admin.GroupUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3887,7 +3929,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.SetGroupUsersRequest" + "$ref": "#/definitions/admin.SetGroupUsersRequest" } } ], @@ -3895,31 +3937,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponseDoc" + "$ref": "#/definitions/admin.GroupUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -4903,19 +4945,97 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.SystemEventListResponseDoc" + "$ref": "#/definitions/admin.SystemEventListResponseDoc" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/admin.ErrorDoc" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/admin.ErrorDoc" + } + } + } + } + }, + "/admin/usage-statistics": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "管理员按日期、用户、平台模型和计费范围查看全局费用、Token、调用次数及排名", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "管理员查询全局用量统计", + "parameters": [ + { + "type": "string", + "description": "开始日期(YYYY-MM-DD),默认近30天", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "结束日期(YYYY-MM-DD,包含当日)", + "name": "end_date", + "in": "query" + }, + { + "type": "integer", + "description": "用户ID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "平台模型名", + "name": "platform_model_name", + "in": "query" + }, + { + "type": "string", + "description": "计费范围:all/free/billable", + "name": "billing_scope", + "in": "query" + }, + { + "type": "string", + "description": "排名指标:cost/tokens/calls", + "name": "rank_by", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/admin.UsageStatisticsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -4975,19 +5095,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UserAuthEventListResponseDoc" + "$ref": "#/definitions/admin.UserAuthEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5047,13 +5167,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UserListResponseDoc" + "$ref": "#/definitions/admin.UserListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5082,7 +5202,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CreateUserRequest" + "$ref": "#/definitions/admin.CreateUserRequest" } } ], @@ -5090,25 +5210,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CreateUserResponseDoc" + "$ref": "#/definitions/admin.CreateUserResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5139,7 +5259,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersRequest" + "$ref": "#/definitions/admin.ImportOpenWebUIUsersRequest" } } ], @@ -5147,25 +5267,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc" + "$ref": "#/definitions/admin.ImportOpenWebUIUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5202,31 +5322,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.DeleteUserResponseDoc" + "$ref": "#/definitions/admin.DeleteUserResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5262,7 +5382,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PatchUserRequest" + "$ref": "#/definitions/admin.PatchUserRequest" } } ], @@ -5270,31 +5390,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc" + "$ref": "#/definitions/admin.UpdateUserStatusResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5332,7 +5452,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordRequest" + "$ref": "#/definitions/admin.ResetUserPasswordRequest" } } ], @@ -5340,31 +5460,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordResponseDoc" + "$ref": "#/definitions/admin.ResetUserPasswordResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5401,25 +5521,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.RevokeUserSessionsResponseDoc" + "$ref": "#/definitions/admin.RevokeUserSessionsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5457,7 +5577,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusRequest" + "$ref": "#/definitions/admin.UpdateUserStatusRequest" } } ], @@ -5465,31 +5585,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc" + "$ref": "#/definitions/admin.UpdateUserStatusResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -7204,6 +7324,37 @@ const docTemplate = `{ } } }, + "/conversations/export": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "流式导出当前用户全部会话及消息为 NDJSON 文件", + "produces": [ + "application/x-ndjson" + ], + "tags": [ + "chat" + ], + "summary": "导出当前用户全部对话", + "responses": { + "200": { + "description": "NDJSON stream", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_transport_http_conversation.ErrorDoc" + } + } + } + } + }, "/conversations/project": { "post": { "security": [ @@ -10169,42 +10320,7 @@ const docTemplate = `{ } }, "definitions": { - "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope": { - "type": "object", - "properties": { - "data": {}, - "details": {}, - "errorCode": { - "type": "string" - }, - "errorMsg": { - "type": "string" - }, - "requestId": { - "type": "string" - } - } - }, - "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc": { - "type": "object", - "properties": { - "data": {}, - "details": {}, - "errorCode": { - "type": "string", - "example": "" - }, - "errorMsg": { - "type": "string", - "example": "" - }, - "requestId": { - "type": "string", - "example": "" - } - } - }, - "internal_transport_http_admin.AuditLogListResponseDoc": { + "admin.AuditLogListResponseDoc": { "type": "object", "properties": { "data": { @@ -10213,7 +10329,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.AuditLogResponse" + "$ref": "#/definitions/admin.AuditLogResponse" } }, "total": { @@ -10226,7 +10342,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.AuditLogResponse": { + "admin.AuditLogResponse": { "type": "object", "properties": { "action": { @@ -10273,7 +10389,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.AuthEventResponse": { + "admin.AuthEventResponse": { "type": "object", "properties": { "clientIP": { @@ -10323,7 +10439,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.CleanupLogsRequest": { + "admin.CleanupLogsRequest": { "type": "object", "required": [ "before", @@ -10338,7 +10454,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.CleanupLogsResponse": { + "admin.CleanupLogsResponse": { "type": "object", "properties": { "before": { @@ -10352,18 +10468,18 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.CleanupLogsResponseDoc": { + "admin.CleanupLogsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsResponse" + "$ref": "#/definitions/admin.CleanupLogsResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.ConversationEventListResponseDoc": { + "admin.ConversationEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -10372,7 +10488,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.ConversationEventResponse" + "$ref": "#/definitions/admin.ConversationEventResponse" } }, "total": { @@ -10385,7 +10501,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.ConversationEventResponse": { + "admin.ConversationEventResponse": { "type": "object", "properties": { "contentMarkdown": { @@ -10498,7 +10614,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.CreatePermissionGroupRequest": { + "admin.CreatePermissionGroupRequest": { "type": "object", "required": [ "name" @@ -10519,7 +10635,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.CreateUserRequest": { + "admin.CreateUserRequest": { "type": "object", "required": [ "password", @@ -10570,40 +10686,40 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.CreateUserResponseDoc": { + "admin.CreateUserResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.UserDataResponse" + "$ref": "#/definitions/admin.UserDataResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.DeletePermissionGroupResponse": { + "admin.DeletePermissionGroupResponse": { "type": "object", "properties": { "deleted": { "type": "boolean" }, "summary": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDeleteSummaryResponse" + "$ref": "#/definitions/admin.PermissionGroupDeleteSummaryResponse" } } }, - "internal_transport_http_admin.DeletePermissionGroupResponseDoc": { + "admin.DeletePermissionGroupResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.DeletePermissionGroupResponse" + "$ref": "#/definitions/admin.DeletePermissionGroupResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.DeleteUserResponse": { + "admin.DeleteUserResponse": { "type": "object", "properties": { "deleted": { @@ -10611,18 +10727,18 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.DeleteUserResponseDoc": { + "admin.DeleteUserResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.DeleteUserResponse" + "$ref": "#/definitions/admin.DeleteUserResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.ErrorDoc": { + "admin.ErrorDoc": { "type": "object", "properties": { "data": {}, @@ -10638,7 +10754,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.GroupModelsResponse": { + "admin.GroupModelsResponse": { "type": "object", "properties": { "modelIDs": { @@ -10650,23 +10766,23 @@ const docTemplate = `{ "rules": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupModelRuleResponse" + "$ref": "#/definitions/admin.PermissionGroupModelRuleResponse" } } } }, - "internal_transport_http_admin.GroupModelsResponseDoc": { + "admin.GroupModelsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponse" + "$ref": "#/definitions/admin.GroupModelsResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.GroupUsersResponse": { + "admin.GroupUsersResponse": { "type": "object", "properties": { "userIDs": { @@ -10677,18 +10793,18 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.GroupUsersResponseDoc": { + "admin.GroupUsersResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponse" + "$ref": "#/definitions/admin.GroupUsersResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.ImportOpenWebUIUsersRequest": { + "admin.ImportOpenWebUIUsersRequest": { "type": "object", "required": [ "creditMultiplier", @@ -10707,7 +10823,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.ImportOpenWebUIUsersResponse": { + "admin.ImportOpenWebUIUsersResponse": { "type": "object", "properties": { "dedupeField": { @@ -10739,18 +10855,18 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc": { + "admin.ImportOpenWebUIUsersResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponse" + "$ref": "#/definitions/admin.ImportOpenWebUIUsersResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.ModelPermissionGroupsResponse": { + "admin.ModelPermissionGroupsResponse": { "type": "object", "properties": { "effectiveGroupIDs": { @@ -10776,18 +10892,18 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.ModelPermissionGroupsResponseDoc": { + "admin.ModelPermissionGroupsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponse" + "$ref": "#/definitions/admin.ModelPermissionGroupsResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.PatchUserRequest": { + "admin.PatchUserRequest": { "type": "object", "properties": { "avatarURL": { @@ -10840,7 +10956,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.PaymentOrderListResponseDoc": { + "admin.PaymentOrderListResponseDoc": { "type": "object", "properties": { "data": { @@ -10849,7 +10965,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.PaymentOrderResponse" + "$ref": "#/definitions/admin.PaymentOrderResponse" } }, "total": { @@ -10862,7 +10978,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.PaymentOrderResponse": { + "admin.PaymentOrderResponse": { "type": "object", "properties": { "baseAmountCents": { @@ -10948,26 +11064,26 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.PermissionGroupDataResponse": { + "admin.PermissionGroupDataResponse": { "type": "object", "properties": { "group": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupResponse" + "$ref": "#/definitions/admin.PermissionGroupResponse" } } }, - "internal_transport_http_admin.PermissionGroupDataResponseDoc": { + "admin.PermissionGroupDataResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponse" + "$ref": "#/definitions/admin.PermissionGroupDataResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.PermissionGroupDeleteSummaryResponse": { + "admin.PermissionGroupDeleteSummaryResponse": { "type": "object", "properties": { "manualModelCount": { @@ -10984,29 +11100,29 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.PermissionGroupListResponse": { + "admin.PermissionGroupListResponse": { "type": "object", "properties": { "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupResponse" + "$ref": "#/definitions/admin.PermissionGroupResponse" } } } }, - "internal_transport_http_admin.PermissionGroupListResponseDoc": { + "admin.PermissionGroupListResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupListResponse" + "$ref": "#/definitions/admin.PermissionGroupListResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.PermissionGroupModelRuleRequest": { + "admin.PermissionGroupModelRuleRequest": { "type": "object", "required": [ "type" @@ -11022,7 +11138,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.PermissionGroupModelRuleResponse": { + "admin.PermissionGroupModelRuleResponse": { "type": "object", "properties": { "type": { @@ -11033,7 +11149,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.PermissionGroupResponse": { + "admin.PermissionGroupResponse": { "type": "object", "properties": { "createdAt": { @@ -11077,7 +11193,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.ResetUserPasswordRequest": { + "admin.ResetUserPasswordRequest": { "type": "object", "required": [ "newPassword" @@ -11093,7 +11209,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.ResetUserPasswordResponse": { + "admin.ResetUserPasswordResponse": { "type": "object", "properties": { "reset": { @@ -11101,18 +11217,18 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.ResetUserPasswordResponseDoc": { + "admin.ResetUserPasswordResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordResponse" + "$ref": "#/definitions/admin.ResetUserPasswordResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.RevokeUserSessionsResponse": { + "admin.RevokeUserSessionsResponse": { "type": "object", "properties": { "revoked": { @@ -11120,18 +11236,18 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.RevokeUserSessionsResponseDoc": { + "admin.RevokeUserSessionsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.RevokeUserSessionsResponse" + "$ref": "#/definitions/admin.RevokeUserSessionsResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.SetGroupModelsRequest": { + "admin.SetGroupModelsRequest": { "type": "object", "properties": { "modelIDs": { @@ -11143,12 +11259,12 @@ const docTemplate = `{ "rules": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupModelRuleRequest" + "$ref": "#/definitions/admin.PermissionGroupModelRuleRequest" } } } }, - "internal_transport_http_admin.SetGroupUsersRequest": { + "admin.SetGroupUsersRequest": { "type": "object", "properties": { "userIDs": { @@ -11159,7 +11275,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.SetModelPermissionGroupsRequest": { + "admin.SetModelPermissionGroupsRequest": { "type": "object", "properties": { "groupIDs": { @@ -11170,7 +11286,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.SystemEventListResponseDoc": { + "admin.SystemEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -11179,7 +11295,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.SystemEventResponse" + "$ref": "#/definitions/admin.SystemEventResponse" } }, "total": { @@ -11192,7 +11308,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.SystemEventResponse": { + "admin.SystemEventResponse": { "type": "object", "properties": { "createdAt": { @@ -11233,7 +11349,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.UpdatePermissionGroupRequest": { + "admin.UpdatePermissionGroupRequest": { "type": "object", "required": [ "name" @@ -11254,7 +11370,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.UpdateUserStatusRequest": { + "admin.UpdateUserStatusRequest": { "type": "object", "required": [ "status" @@ -11270,18 +11386,18 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.UpdateUserStatusResponseDoc": { + "admin.UpdateUserStatusResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.UserDataResponse" + "$ref": "#/definitions/admin.UserDataResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.UsageLogListResponseDoc": { + "admin.UsageLogListResponseDoc": { "type": "object", "properties": { "data": { @@ -11290,7 +11406,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.UsageLogResponse" + "$ref": "#/definitions/admin.UsageLogResponse" } }, "total": { @@ -11303,7 +11419,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.UsageLogResponse": { + "admin.UsageLogResponse": { "type": "object", "properties": { "billedCurrency": { @@ -11404,7 +11520,240 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.UserAuthEventListResponseDoc": { + "admin.UsageStatisticsMetricsResponse": { + "type": "object", + "properties": { + "avgLatencyMS": { + "type": "integer" + }, + "billedNanousd": { + "type": "integer" + }, + "billedUSD": { + "type": "number" + }, + "cacheReadTokens": { + "type": "integer" + }, + "cacheWriteTokens": { + "type": "integer" + }, + "callCount": { + "type": "integer" + }, + "inputTokens": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "reasoningTokens": { + "type": "integer" + }, + "recordCount": { + "type": "integer" + }, + "totalTokens": { + "type": "integer" + } + } + }, + "admin.UsageStatisticsModelRankResponse": { + "type": "object", + "properties": { + "avgLatencyMS": { + "type": "integer" + }, + "billedNanousd": { + "type": "integer" + }, + "billedUSD": { + "type": "number" + }, + "cacheReadTokens": { + "type": "integer" + }, + "cacheWriteTokens": { + "type": "integer" + }, + "callCount": { + "type": "integer" + }, + "inputTokens": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "platformModelName": { + "type": "string" + }, + "reasoningTokens": { + "type": "integer" + }, + "recordCount": { + "type": "integer" + }, + "totalTokens": { + "type": "integer" + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + } + } + } + }, + "admin.UsageStatisticsResponse": { + "type": "object", + "properties": { + "range": { + "type": "object", + "properties": { + "endDate": { + "type": "string" + }, + "granularity": { + "type": "string" + }, + "startDate": { + "type": "string" + } + } + }, + "topModels": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsModelRankResponse" + } + }, + "topUsers": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsUserRankResponse" + } + }, + "totals": { + "$ref": "#/definitions/admin.UsageStatisticsMetricsResponse" + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + } + } + } + }, + "admin.UsageStatisticsResponseDoc": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/admin.UsageStatisticsResponse" + }, + "errorMsg": { + "type": "string" + } + } + }, + "admin.UsageStatisticsTrendResponse": { + "type": "object", + "properties": { + "avgLatencyMS": { + "type": "integer" + }, + "billedNanousd": { + "type": "integer" + }, + "billedUSD": { + "type": "number" + }, + "cacheReadTokens": { + "type": "integer" + }, + "cacheWriteTokens": { + "type": "integer" + }, + "callCount": { + "type": "integer" + }, + "inputTokens": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "periodStart": { + "type": "string" + }, + "reasoningTokens": { + "type": "integer" + }, + "recordCount": { + "type": "integer" + }, + "totalTokens": { + "type": "integer" + } + } + }, + "admin.UsageStatisticsUserRankResponse": { + "type": "object", + "properties": { + "avgLatencyMS": { + "type": "integer" + }, + "billedNanousd": { + "type": "integer" + }, + "billedUSD": { + "type": "number" + }, + "cacheReadTokens": { + "type": "integer" + }, + "cacheWriteTokens": { + "type": "integer" + }, + "callCount": { + "type": "integer" + }, + "inputTokens": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "reasoningTokens": { + "type": "integer" + }, + "recordCount": { + "type": "integer" + }, + "totalTokens": { + "type": "integer" + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + } + }, + "userDisplayName": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "userLabel": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "admin.UserAuthEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -11413,7 +11762,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.AuthEventResponse" + "$ref": "#/definitions/admin.AuthEventResponse" } }, "total": { @@ -11426,15 +11775,15 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.UserDataResponse": { + "admin.UserDataResponse": { "type": "object", "properties": { "user": { - "$ref": "#/definitions/internal_transport_http_admin.UserResponse" + "$ref": "#/definitions/admin.UserResponse" } } }, - "internal_transport_http_admin.UserIdentityProviderSummaryResponse": { + "admin.UserIdentityProviderSummaryResponse": { "type": "object", "properties": { "id": { @@ -11454,7 +11803,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.UserListResponseDoc": { + "admin.UserListResponseDoc": { "type": "object", "properties": { "data": { @@ -11463,7 +11812,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.UserResponse" + "$ref": "#/definitions/admin.UserResponse" } }, "total": { @@ -11476,7 +11825,7 @@ const docTemplate = `{ } } }, - "internal_transport_http_admin.UserResponse": { + "admin.UserResponse": { "type": "object", "properties": { "appearancePreferences": { @@ -11515,7 +11864,7 @@ const docTemplate = `{ "identityProviders": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.UserIdentityProviderSummaryResponse" + "$ref": "#/definitions/admin.UserIdentityProviderSummaryResponse" } }, "lastActiveAt": { @@ -11583,6 +11932,41 @@ const docTemplate = `{ } } }, + "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope": { + "type": "object", + "properties": { + "data": {}, + "details": {}, + "errorCode": { + "type": "string" + }, + "errorMsg": { + "type": "string" + }, + "requestId": { + "type": "string" + } + } + }, + "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc": { + "type": "object", + "properties": { + "data": {}, + "details": {}, + "errorCode": { + "type": "string", + "example": "" + }, + "errorMsg": { + "type": "string", + "example": "" + }, + "requestId": { + "type": "string", + "example": "" + } + } + }, "internal_transport_http_announcement.AdminAnnouncementListResponseDoc": { "type": "object", "properties": { @@ -13353,6 +13737,71 @@ const docTemplate = `{ } } }, + "internal_transport_http_billing.OpenRouterOfficialPricingDataResponse": { + "type": "object", + "properties": { + "cached": { + "type": "boolean" + }, + "fetchedAt": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingItemResponse" + } + }, + "stale": { + "type": "boolean" + } + } + }, + "internal_transport_http_billing.OpenRouterOfficialPricingItemResponse": { + "type": "object", + "properties": { + "canonicalSlug": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "pricing": { + "$ref": "#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingUnitPricingResponse" + } + } + }, + "internal_transport_http_billing.OpenRouterOfficialPricingResponseDoc": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingDataResponse" + }, + "errorMsg": { + "type": "string" + } + } + }, + "internal_transport_http_billing.OpenRouterOfficialPricingUnitPricingResponse": { + "type": "object", + "properties": { + "completion": { + "type": "string" + }, + "inputCacheRead": { + "type": "string" + }, + "inputCacheWrite": { + "type": "string" + }, + "prompt": { + "type": "string" + } + } + }, "internal_transport_http_billing.PatchRedemptionCodeRequestDoc": { "type": "object", "properties": { @@ -14704,7 +15153,8 @@ const docTemplate = `{ "enum": [ "chat", "image_generation", - "image_edit" + "image_edit", + "video_generation" ] } } diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index c4b99a1f..26fa02a5 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -324,13 +324,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.AuditLogListResponseDoc" + "$ref": "#/definitions/admin.AuditLogListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -580,6 +580,48 @@ } } }, + "/admin/billing/official-pricing/openrouter": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "从 storage 缓存读取 OpenRouter 模型定价;缓存不存在、过期或 refresh=true 时由后端刷新。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin-billing" + ], + "summary": "管理员获取 OpenRouter 官方模型定价", + "parameters": [ + { + "type": "boolean", + "description": "强制刷新缓存", + "name": "refresh", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingResponseDoc" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/internal_transport_http_billing.ErrorDoc" + } + } + } + } + }, "/admin/billing/plans/{id}": { "patch": { "security": [ @@ -1054,19 +1096,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UsageLogListResponseDoc" + "$ref": "#/definitions/admin.UsageLogListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -1162,19 +1204,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ConversationEventListResponseDoc" + "$ref": "#/definitions/admin.ConversationEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -1205,7 +1247,7 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3221,7 +3263,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsRequest" + "$ref": "#/definitions/admin.CleanupLogsRequest" } } ], @@ -3229,19 +3271,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsResponseDoc" + "$ref": "#/definitions/admin.CleanupLogsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3278,25 +3320,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc" + "$ref": "#/definitions/admin.ModelPermissionGroupsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3332,7 +3374,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.SetModelPermissionGroupsRequest" + "$ref": "#/definitions/admin.SetModelPermissionGroupsRequest" } } ], @@ -3340,25 +3382,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc" + "$ref": "#/definitions/admin.ModelPermissionGroupsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3448,19 +3490,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PaymentOrderListResponseDoc" + "$ref": "#/definitions/admin.PaymentOrderListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3488,13 +3530,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupListResponseDoc" + "$ref": "#/definitions/admin.PermissionGroupListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3523,7 +3565,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CreatePermissionGroupRequest" + "$ref": "#/definitions/admin.CreatePermissionGroupRequest" } } ], @@ -3531,19 +3573,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc" + "$ref": "#/definitions/admin.PermissionGroupDataResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3580,31 +3622,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.DeletePermissionGroupResponseDoc" + "$ref": "#/definitions/admin.DeletePermissionGroupResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3640,7 +3682,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UpdatePermissionGroupRequest" + "$ref": "#/definitions/admin.UpdatePermissionGroupRequest" } } ], @@ -3648,25 +3690,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc" + "$ref": "#/definitions/admin.PermissionGroupDataResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3703,25 +3745,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponseDoc" + "$ref": "#/definitions/admin.GroupModelsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3757,7 +3799,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.SetGroupModelsRequest" + "$ref": "#/definitions/admin.SetGroupModelsRequest" } } ], @@ -3765,25 +3807,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponseDoc" + "$ref": "#/definitions/admin.GroupModelsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3820,31 +3862,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponseDoc" + "$ref": "#/definitions/admin.GroupUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -3880,7 +3922,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.SetGroupUsersRequest" + "$ref": "#/definitions/admin.SetGroupUsersRequest" } } ], @@ -3888,31 +3930,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponseDoc" + "$ref": "#/definitions/admin.GroupUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -4896,19 +4938,97 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.SystemEventListResponseDoc" + "$ref": "#/definitions/admin.SystemEventListResponseDoc" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/admin.ErrorDoc" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/admin.ErrorDoc" + } + } + } + } + }, + "/admin/usage-statistics": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "管理员按日期、用户、平台模型和计费范围查看全局费用、Token、调用次数及排名", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "管理员查询全局用量统计", + "parameters": [ + { + "type": "string", + "description": "开始日期(YYYY-MM-DD),默认近30天", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "结束日期(YYYY-MM-DD,包含当日)", + "name": "end_date", + "in": "query" + }, + { + "type": "integer", + "description": "用户ID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "平台模型名", + "name": "platform_model_name", + "in": "query" + }, + { + "type": "string", + "description": "计费范围:all/free/billable", + "name": "billing_scope", + "in": "query" + }, + { + "type": "string", + "description": "排名指标:cost/tokens/calls", + "name": "rank_by", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/admin.UsageStatisticsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -4968,19 +5088,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UserAuthEventListResponseDoc" + "$ref": "#/definitions/admin.UserAuthEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5040,13 +5160,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UserListResponseDoc" + "$ref": "#/definitions/admin.UserListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5075,7 +5195,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CreateUserRequest" + "$ref": "#/definitions/admin.CreateUserRequest" } } ], @@ -5083,25 +5203,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.CreateUserResponseDoc" + "$ref": "#/definitions/admin.CreateUserResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5132,7 +5252,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersRequest" + "$ref": "#/definitions/admin.ImportOpenWebUIUsersRequest" } } ], @@ -5140,25 +5260,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc" + "$ref": "#/definitions/admin.ImportOpenWebUIUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5195,31 +5315,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.DeleteUserResponseDoc" + "$ref": "#/definitions/admin.DeleteUserResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5255,7 +5375,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.PatchUserRequest" + "$ref": "#/definitions/admin.PatchUserRequest" } } ], @@ -5263,31 +5383,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc" + "$ref": "#/definitions/admin.UpdateUserStatusResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5325,7 +5445,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordRequest" + "$ref": "#/definitions/admin.ResetUserPasswordRequest" } } ], @@ -5333,31 +5453,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordResponseDoc" + "$ref": "#/definitions/admin.ResetUserPasswordResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5394,25 +5514,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.RevokeUserSessionsResponseDoc" + "$ref": "#/definitions/admin.RevokeUserSessionsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -5450,7 +5570,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusRequest" + "$ref": "#/definitions/admin.UpdateUserStatusRequest" } } ], @@ -5458,31 +5578,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc" + "$ref": "#/definitions/admin.UpdateUserStatusResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + "$ref": "#/definitions/admin.ErrorDoc" } } } @@ -7197,6 +7317,37 @@ } } }, + "/conversations/export": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "流式导出当前用户全部会话及消息为 NDJSON 文件", + "produces": [ + "application/x-ndjson" + ], + "tags": [ + "chat" + ], + "summary": "导出当前用户全部对话", + "responses": { + "200": { + "description": "NDJSON stream", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_transport_http_conversation.ErrorDoc" + } + } + } + } + }, "/conversations/project": { "post": { "security": [ @@ -10162,42 +10313,7 @@ } }, "definitions": { - "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope": { - "type": "object", - "properties": { - "data": {}, - "details": {}, - "errorCode": { - "type": "string" - }, - "errorMsg": { - "type": "string" - }, - "requestId": { - "type": "string" - } - } - }, - "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc": { - "type": "object", - "properties": { - "data": {}, - "details": {}, - "errorCode": { - "type": "string", - "example": "" - }, - "errorMsg": { - "type": "string", - "example": "" - }, - "requestId": { - "type": "string", - "example": "" - } - } - }, - "internal_transport_http_admin.AuditLogListResponseDoc": { + "admin.AuditLogListResponseDoc": { "type": "object", "properties": { "data": { @@ -10206,7 +10322,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.AuditLogResponse" + "$ref": "#/definitions/admin.AuditLogResponse" } }, "total": { @@ -10219,7 +10335,7 @@ } } }, - "internal_transport_http_admin.AuditLogResponse": { + "admin.AuditLogResponse": { "type": "object", "properties": { "action": { @@ -10266,7 +10382,7 @@ } } }, - "internal_transport_http_admin.AuthEventResponse": { + "admin.AuthEventResponse": { "type": "object", "properties": { "clientIP": { @@ -10316,7 +10432,7 @@ } } }, - "internal_transport_http_admin.CleanupLogsRequest": { + "admin.CleanupLogsRequest": { "type": "object", "required": [ "before", @@ -10331,7 +10447,7 @@ } } }, - "internal_transport_http_admin.CleanupLogsResponse": { + "admin.CleanupLogsResponse": { "type": "object", "properties": { "before": { @@ -10345,18 +10461,18 @@ } } }, - "internal_transport_http_admin.CleanupLogsResponseDoc": { + "admin.CleanupLogsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsResponse" + "$ref": "#/definitions/admin.CleanupLogsResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.ConversationEventListResponseDoc": { + "admin.ConversationEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -10365,7 +10481,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.ConversationEventResponse" + "$ref": "#/definitions/admin.ConversationEventResponse" } }, "total": { @@ -10378,7 +10494,7 @@ } } }, - "internal_transport_http_admin.ConversationEventResponse": { + "admin.ConversationEventResponse": { "type": "object", "properties": { "contentMarkdown": { @@ -10491,7 +10607,7 @@ } } }, - "internal_transport_http_admin.CreatePermissionGroupRequest": { + "admin.CreatePermissionGroupRequest": { "type": "object", "required": [ "name" @@ -10512,7 +10628,7 @@ } } }, - "internal_transport_http_admin.CreateUserRequest": { + "admin.CreateUserRequest": { "type": "object", "required": [ "password", @@ -10563,40 +10679,40 @@ } } }, - "internal_transport_http_admin.CreateUserResponseDoc": { + "admin.CreateUserResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.UserDataResponse" + "$ref": "#/definitions/admin.UserDataResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.DeletePermissionGroupResponse": { + "admin.DeletePermissionGroupResponse": { "type": "object", "properties": { "deleted": { "type": "boolean" }, "summary": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDeleteSummaryResponse" + "$ref": "#/definitions/admin.PermissionGroupDeleteSummaryResponse" } } }, - "internal_transport_http_admin.DeletePermissionGroupResponseDoc": { + "admin.DeletePermissionGroupResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.DeletePermissionGroupResponse" + "$ref": "#/definitions/admin.DeletePermissionGroupResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.DeleteUserResponse": { + "admin.DeleteUserResponse": { "type": "object", "properties": { "deleted": { @@ -10604,18 +10720,18 @@ } } }, - "internal_transport_http_admin.DeleteUserResponseDoc": { + "admin.DeleteUserResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.DeleteUserResponse" + "$ref": "#/definitions/admin.DeleteUserResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.ErrorDoc": { + "admin.ErrorDoc": { "type": "object", "properties": { "data": {}, @@ -10631,7 +10747,7 @@ } } }, - "internal_transport_http_admin.GroupModelsResponse": { + "admin.GroupModelsResponse": { "type": "object", "properties": { "modelIDs": { @@ -10643,23 +10759,23 @@ "rules": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupModelRuleResponse" + "$ref": "#/definitions/admin.PermissionGroupModelRuleResponse" } } } }, - "internal_transport_http_admin.GroupModelsResponseDoc": { + "admin.GroupModelsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponse" + "$ref": "#/definitions/admin.GroupModelsResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.GroupUsersResponse": { + "admin.GroupUsersResponse": { "type": "object", "properties": { "userIDs": { @@ -10670,18 +10786,18 @@ } } }, - "internal_transport_http_admin.GroupUsersResponseDoc": { + "admin.GroupUsersResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponse" + "$ref": "#/definitions/admin.GroupUsersResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.ImportOpenWebUIUsersRequest": { + "admin.ImportOpenWebUIUsersRequest": { "type": "object", "required": [ "creditMultiplier", @@ -10700,7 +10816,7 @@ } } }, - "internal_transport_http_admin.ImportOpenWebUIUsersResponse": { + "admin.ImportOpenWebUIUsersResponse": { "type": "object", "properties": { "dedupeField": { @@ -10732,18 +10848,18 @@ } } }, - "internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc": { + "admin.ImportOpenWebUIUsersResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponse" + "$ref": "#/definitions/admin.ImportOpenWebUIUsersResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.ModelPermissionGroupsResponse": { + "admin.ModelPermissionGroupsResponse": { "type": "object", "properties": { "effectiveGroupIDs": { @@ -10769,18 +10885,18 @@ } } }, - "internal_transport_http_admin.ModelPermissionGroupsResponseDoc": { + "admin.ModelPermissionGroupsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponse" + "$ref": "#/definitions/admin.ModelPermissionGroupsResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.PatchUserRequest": { + "admin.PatchUserRequest": { "type": "object", "properties": { "avatarURL": { @@ -10833,7 +10949,7 @@ } } }, - "internal_transport_http_admin.PaymentOrderListResponseDoc": { + "admin.PaymentOrderListResponseDoc": { "type": "object", "properties": { "data": { @@ -10842,7 +10958,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.PaymentOrderResponse" + "$ref": "#/definitions/admin.PaymentOrderResponse" } }, "total": { @@ -10855,7 +10971,7 @@ } } }, - "internal_transport_http_admin.PaymentOrderResponse": { + "admin.PaymentOrderResponse": { "type": "object", "properties": { "baseAmountCents": { @@ -10941,26 +11057,26 @@ } } }, - "internal_transport_http_admin.PermissionGroupDataResponse": { + "admin.PermissionGroupDataResponse": { "type": "object", "properties": { "group": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupResponse" + "$ref": "#/definitions/admin.PermissionGroupResponse" } } }, - "internal_transport_http_admin.PermissionGroupDataResponseDoc": { + "admin.PermissionGroupDataResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponse" + "$ref": "#/definitions/admin.PermissionGroupDataResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.PermissionGroupDeleteSummaryResponse": { + "admin.PermissionGroupDeleteSummaryResponse": { "type": "object", "properties": { "manualModelCount": { @@ -10977,29 +11093,29 @@ } } }, - "internal_transport_http_admin.PermissionGroupListResponse": { + "admin.PermissionGroupListResponse": { "type": "object", "properties": { "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupResponse" + "$ref": "#/definitions/admin.PermissionGroupResponse" } } } }, - "internal_transport_http_admin.PermissionGroupListResponseDoc": { + "admin.PermissionGroupListResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupListResponse" + "$ref": "#/definitions/admin.PermissionGroupListResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.PermissionGroupModelRuleRequest": { + "admin.PermissionGroupModelRuleRequest": { "type": "object", "required": [ "type" @@ -11015,7 +11131,7 @@ } } }, - "internal_transport_http_admin.PermissionGroupModelRuleResponse": { + "admin.PermissionGroupModelRuleResponse": { "type": "object", "properties": { "type": { @@ -11026,7 +11142,7 @@ } } }, - "internal_transport_http_admin.PermissionGroupResponse": { + "admin.PermissionGroupResponse": { "type": "object", "properties": { "createdAt": { @@ -11070,7 +11186,7 @@ } } }, - "internal_transport_http_admin.ResetUserPasswordRequest": { + "admin.ResetUserPasswordRequest": { "type": "object", "required": [ "newPassword" @@ -11086,7 +11202,7 @@ } } }, - "internal_transport_http_admin.ResetUserPasswordResponse": { + "admin.ResetUserPasswordResponse": { "type": "object", "properties": { "reset": { @@ -11094,18 +11210,18 @@ } } }, - "internal_transport_http_admin.ResetUserPasswordResponseDoc": { + "admin.ResetUserPasswordResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordResponse" + "$ref": "#/definitions/admin.ResetUserPasswordResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.RevokeUserSessionsResponse": { + "admin.RevokeUserSessionsResponse": { "type": "object", "properties": { "revoked": { @@ -11113,18 +11229,18 @@ } } }, - "internal_transport_http_admin.RevokeUserSessionsResponseDoc": { + "admin.RevokeUserSessionsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.RevokeUserSessionsResponse" + "$ref": "#/definitions/admin.RevokeUserSessionsResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.SetGroupModelsRequest": { + "admin.SetGroupModelsRequest": { "type": "object", "properties": { "modelIDs": { @@ -11136,12 +11252,12 @@ "rules": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupModelRuleRequest" + "$ref": "#/definitions/admin.PermissionGroupModelRuleRequest" } } } }, - "internal_transport_http_admin.SetGroupUsersRequest": { + "admin.SetGroupUsersRequest": { "type": "object", "properties": { "userIDs": { @@ -11152,7 +11268,7 @@ } } }, - "internal_transport_http_admin.SetModelPermissionGroupsRequest": { + "admin.SetModelPermissionGroupsRequest": { "type": "object", "properties": { "groupIDs": { @@ -11163,7 +11279,7 @@ } } }, - "internal_transport_http_admin.SystemEventListResponseDoc": { + "admin.SystemEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -11172,7 +11288,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.SystemEventResponse" + "$ref": "#/definitions/admin.SystemEventResponse" } }, "total": { @@ -11185,7 +11301,7 @@ } } }, - "internal_transport_http_admin.SystemEventResponse": { + "admin.SystemEventResponse": { "type": "object", "properties": { "createdAt": { @@ -11226,7 +11342,7 @@ } } }, - "internal_transport_http_admin.UpdatePermissionGroupRequest": { + "admin.UpdatePermissionGroupRequest": { "type": "object", "required": [ "name" @@ -11247,7 +11363,7 @@ } } }, - "internal_transport_http_admin.UpdateUserStatusRequest": { + "admin.UpdateUserStatusRequest": { "type": "object", "required": [ "status" @@ -11263,18 +11379,18 @@ } } }, - "internal_transport_http_admin.UpdateUserStatusResponseDoc": { + "admin.UpdateUserStatusResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/internal_transport_http_admin.UserDataResponse" + "$ref": "#/definitions/admin.UserDataResponse" }, "errorMsg": { "type": "string" } } }, - "internal_transport_http_admin.UsageLogListResponseDoc": { + "admin.UsageLogListResponseDoc": { "type": "object", "properties": { "data": { @@ -11283,7 +11399,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.UsageLogResponse" + "$ref": "#/definitions/admin.UsageLogResponse" } }, "total": { @@ -11296,7 +11412,7 @@ } } }, - "internal_transport_http_admin.UsageLogResponse": { + "admin.UsageLogResponse": { "type": "object", "properties": { "billedCurrency": { @@ -11397,7 +11513,240 @@ } } }, - "internal_transport_http_admin.UserAuthEventListResponseDoc": { + "admin.UsageStatisticsMetricsResponse": { + "type": "object", + "properties": { + "avgLatencyMS": { + "type": "integer" + }, + "billedNanousd": { + "type": "integer" + }, + "billedUSD": { + "type": "number" + }, + "cacheReadTokens": { + "type": "integer" + }, + "cacheWriteTokens": { + "type": "integer" + }, + "callCount": { + "type": "integer" + }, + "inputTokens": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "reasoningTokens": { + "type": "integer" + }, + "recordCount": { + "type": "integer" + }, + "totalTokens": { + "type": "integer" + } + } + }, + "admin.UsageStatisticsModelRankResponse": { + "type": "object", + "properties": { + "avgLatencyMS": { + "type": "integer" + }, + "billedNanousd": { + "type": "integer" + }, + "billedUSD": { + "type": "number" + }, + "cacheReadTokens": { + "type": "integer" + }, + "cacheWriteTokens": { + "type": "integer" + }, + "callCount": { + "type": "integer" + }, + "inputTokens": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "platformModelName": { + "type": "string" + }, + "reasoningTokens": { + "type": "integer" + }, + "recordCount": { + "type": "integer" + }, + "totalTokens": { + "type": "integer" + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + } + } + } + }, + "admin.UsageStatisticsResponse": { + "type": "object", + "properties": { + "range": { + "type": "object", + "properties": { + "endDate": { + "type": "string" + }, + "granularity": { + "type": "string" + }, + "startDate": { + "type": "string" + } + } + }, + "topModels": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsModelRankResponse" + } + }, + "topUsers": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsUserRankResponse" + } + }, + "totals": { + "$ref": "#/definitions/admin.UsageStatisticsMetricsResponse" + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + } + } + } + }, + "admin.UsageStatisticsResponseDoc": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/admin.UsageStatisticsResponse" + }, + "errorMsg": { + "type": "string" + } + } + }, + "admin.UsageStatisticsTrendResponse": { + "type": "object", + "properties": { + "avgLatencyMS": { + "type": "integer" + }, + "billedNanousd": { + "type": "integer" + }, + "billedUSD": { + "type": "number" + }, + "cacheReadTokens": { + "type": "integer" + }, + "cacheWriteTokens": { + "type": "integer" + }, + "callCount": { + "type": "integer" + }, + "inputTokens": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "periodStart": { + "type": "string" + }, + "reasoningTokens": { + "type": "integer" + }, + "recordCount": { + "type": "integer" + }, + "totalTokens": { + "type": "integer" + } + } + }, + "admin.UsageStatisticsUserRankResponse": { + "type": "object", + "properties": { + "avgLatencyMS": { + "type": "integer" + }, + "billedNanousd": { + "type": "integer" + }, + "billedUSD": { + "type": "number" + }, + "cacheReadTokens": { + "type": "integer" + }, + "cacheWriteTokens": { + "type": "integer" + }, + "callCount": { + "type": "integer" + }, + "inputTokens": { + "type": "integer" + }, + "outputTokens": { + "type": "integer" + }, + "reasoningTokens": { + "type": "integer" + }, + "recordCount": { + "type": "integer" + }, + "totalTokens": { + "type": "integer" + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + } + }, + "userDisplayName": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "userLabel": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "admin.UserAuthEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -11406,7 +11755,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.AuthEventResponse" + "$ref": "#/definitions/admin.AuthEventResponse" } }, "total": { @@ -11419,15 +11768,15 @@ } } }, - "internal_transport_http_admin.UserDataResponse": { + "admin.UserDataResponse": { "type": "object", "properties": { "user": { - "$ref": "#/definitions/internal_transport_http_admin.UserResponse" + "$ref": "#/definitions/admin.UserResponse" } } }, - "internal_transport_http_admin.UserIdentityProviderSummaryResponse": { + "admin.UserIdentityProviderSummaryResponse": { "type": "object", "properties": { "id": { @@ -11447,7 +11796,7 @@ } } }, - "internal_transport_http_admin.UserListResponseDoc": { + "admin.UserListResponseDoc": { "type": "object", "properties": { "data": { @@ -11456,7 +11805,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.UserResponse" + "$ref": "#/definitions/admin.UserResponse" } }, "total": { @@ -11469,7 +11818,7 @@ } } }, - "internal_transport_http_admin.UserResponse": { + "admin.UserResponse": { "type": "object", "properties": { "appearancePreferences": { @@ -11508,7 +11857,7 @@ "identityProviders": { "type": "array", "items": { - "$ref": "#/definitions/internal_transport_http_admin.UserIdentityProviderSummaryResponse" + "$ref": "#/definitions/admin.UserIdentityProviderSummaryResponse" } }, "lastActiveAt": { @@ -11576,6 +11925,41 @@ } } }, + "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope": { + "type": "object", + "properties": { + "data": {}, + "details": {}, + "errorCode": { + "type": "string" + }, + "errorMsg": { + "type": "string" + }, + "requestId": { + "type": "string" + } + } + }, + "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc": { + "type": "object", + "properties": { + "data": {}, + "details": {}, + "errorCode": { + "type": "string", + "example": "" + }, + "errorMsg": { + "type": "string", + "example": "" + }, + "requestId": { + "type": "string", + "example": "" + } + } + }, "internal_transport_http_announcement.AdminAnnouncementListResponseDoc": { "type": "object", "properties": { @@ -13346,6 +13730,71 @@ } } }, + "internal_transport_http_billing.OpenRouterOfficialPricingDataResponse": { + "type": "object", + "properties": { + "cached": { + "type": "boolean" + }, + "fetchedAt": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingItemResponse" + } + }, + "stale": { + "type": "boolean" + } + } + }, + "internal_transport_http_billing.OpenRouterOfficialPricingItemResponse": { + "type": "object", + "properties": { + "canonicalSlug": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "pricing": { + "$ref": "#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingUnitPricingResponse" + } + } + }, + "internal_transport_http_billing.OpenRouterOfficialPricingResponseDoc": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingDataResponse" + }, + "errorMsg": { + "type": "string" + } + } + }, + "internal_transport_http_billing.OpenRouterOfficialPricingUnitPricingResponse": { + "type": "object", + "properties": { + "completion": { + "type": "string" + }, + "inputCacheRead": { + "type": "string" + }, + "inputCacheWrite": { + "type": "string" + }, + "prompt": { + "type": "string" + } + } + }, "internal_transport_http_billing.PatchRedemptionCodeRequestDoc": { "type": "object", "properties": { @@ -14697,7 +15146,8 @@ "enum": [ "chat", "image_generation", - "image_edit" + "image_edit", + "video_generation" ] } } diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index a1066307..e4e1eb76 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,37 +1,12 @@ basePath: /api/v1 definitions: - github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope: - properties: - data: {} - details: {} - errorCode: - type: string - errorMsg: - type: string - requestId: - type: string - type: object - github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc: - properties: - data: {} - details: {} - errorCode: - example: "" - type: string - errorMsg: - example: "" - type: string - requestId: - example: "" - type: string - type: object - internal_transport_http_admin.AuditLogListResponseDoc: + admin.AuditLogListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/internal_transport_http_admin.AuditLogResponse' + $ref: '#/definitions/admin.AuditLogResponse' type: array total: type: integer @@ -39,7 +14,7 @@ definitions: errorMsg: type: string type: object - internal_transport_http_admin.AuditLogResponse: + admin.AuditLogResponse: properties: action: type: string @@ -70,7 +45,7 @@ definitions: userAgent: type: string type: object - internal_transport_http_admin.AuthEventResponse: + admin.AuthEventResponse: properties: clientIP: type: string @@ -103,7 +78,7 @@ definitions: username: type: string type: object - internal_transport_http_admin.CleanupLogsRequest: + admin.CleanupLogsRequest: properties: before: type: string @@ -113,7 +88,7 @@ definitions: - before - type type: object - internal_transport_http_admin.CleanupLogsResponse: + admin.CleanupLogsResponse: properties: before: type: string @@ -122,20 +97,20 @@ definitions: type: type: string type: object - internal_transport_http_admin.CleanupLogsResponseDoc: + admin.CleanupLogsResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.CleanupLogsResponse' + $ref: '#/definitions/admin.CleanupLogsResponse' errorMsg: type: string type: object - internal_transport_http_admin.ConversationEventListResponseDoc: + admin.ConversationEventListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/internal_transport_http_admin.ConversationEventResponse' + $ref: '#/definitions/admin.ConversationEventResponse' type: array total: type: integer @@ -143,7 +118,7 @@ definitions: errorMsg: type: string type: object - internal_transport_http_admin.ConversationEventResponse: + admin.ConversationEventResponse: properties: contentMarkdown: type: string @@ -218,7 +193,7 @@ definitions: username: type: string type: object - internal_transport_http_admin.CreatePermissionGroupRequest: + admin.CreatePermissionGroupRequest: properties: description: maxLength: 512 @@ -233,7 +208,7 @@ definitions: required: - name type: object - internal_transport_http_admin.CreateUserRequest: + admin.CreateUserRequest: properties: avatarURL: maxLength: 2048 @@ -271,40 +246,40 @@ definitions: - password - username type: object - internal_transport_http_admin.CreateUserResponseDoc: + admin.CreateUserResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.UserDataResponse' + $ref: '#/definitions/admin.UserDataResponse' errorMsg: type: string type: object - internal_transport_http_admin.DeletePermissionGroupResponse: + admin.DeletePermissionGroupResponse: properties: deleted: type: boolean summary: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupDeleteSummaryResponse' + $ref: '#/definitions/admin.PermissionGroupDeleteSummaryResponse' type: object - internal_transport_http_admin.DeletePermissionGroupResponseDoc: + admin.DeletePermissionGroupResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.DeletePermissionGroupResponse' + $ref: '#/definitions/admin.DeletePermissionGroupResponse' errorMsg: type: string type: object - internal_transport_http_admin.DeleteUserResponse: + admin.DeleteUserResponse: properties: deleted: type: boolean type: object - internal_transport_http_admin.DeleteUserResponseDoc: + admin.DeleteUserResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.DeleteUserResponse' + $ref: '#/definitions/admin.DeleteUserResponse' errorMsg: type: string type: object - internal_transport_http_admin.ErrorDoc: + admin.ErrorDoc: properties: data: {} details: {} @@ -315,7 +290,7 @@ definitions: requestId: type: string type: object - internal_transport_http_admin.GroupModelsResponse: + admin.GroupModelsResponse: properties: modelIDs: items: @@ -323,31 +298,31 @@ definitions: type: array rules: items: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupModelRuleResponse' + $ref: '#/definitions/admin.PermissionGroupModelRuleResponse' type: array type: object - internal_transport_http_admin.GroupModelsResponseDoc: + admin.GroupModelsResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.GroupModelsResponse' + $ref: '#/definitions/admin.GroupModelsResponse' errorMsg: type: string type: object - internal_transport_http_admin.GroupUsersResponse: + admin.GroupUsersResponse: properties: userIDs: items: type: integer type: array type: object - internal_transport_http_admin.GroupUsersResponseDoc: + admin.GroupUsersResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.GroupUsersResponse' + $ref: '#/definitions/admin.GroupUsersResponse' errorMsg: type: string type: object - internal_transport_http_admin.ImportOpenWebUIUsersRequest: + admin.ImportOpenWebUIUsersRequest: properties: creditMultiplier: type: number @@ -360,7 +335,7 @@ definitions: - creditMultiplier - dsn type: object - internal_transport_http_admin.ImportOpenWebUIUsersResponse: + admin.ImportOpenWebUIUsersResponse: properties: dedupeField: type: string @@ -381,14 +356,14 @@ definitions: source: type: string type: object - internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc: + admin.ImportOpenWebUIUsersResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponse' + $ref: '#/definitions/admin.ImportOpenWebUIUsersResponse' errorMsg: type: string type: object - internal_transport_http_admin.ModelPermissionGroupsResponse: + admin.ModelPermissionGroupsResponse: properties: effectiveGroupIDs: items: @@ -405,14 +380,14 @@ definitions: unassigned: type: boolean type: object - internal_transport_http_admin.ModelPermissionGroupsResponseDoc: + admin.ModelPermissionGroupsResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponse' + $ref: '#/definitions/admin.ModelPermissionGroupsResponse' errorMsg: type: string type: object - internal_transport_http_admin.PatchUserRequest: + admin.PatchUserRequest: properties: avatarURL: maxLength: 2048 @@ -451,13 +426,13 @@ definitions: maxLength: 64 type: string type: object - internal_transport_http_admin.PaymentOrderListResponseDoc: + admin.PaymentOrderListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/internal_transport_http_admin.PaymentOrderResponse' + $ref: '#/definitions/admin.PaymentOrderResponse' type: array total: type: integer @@ -465,7 +440,7 @@ definitions: errorMsg: type: string type: object - internal_transport_http_admin.PaymentOrderResponse: + admin.PaymentOrderResponse: properties: baseAmountCents: type: integer @@ -522,19 +497,19 @@ definitions: username: type: string type: object - internal_transport_http_admin.PermissionGroupDataResponse: + admin.PermissionGroupDataResponse: properties: group: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupResponse' + $ref: '#/definitions/admin.PermissionGroupResponse' type: object - internal_transport_http_admin.PermissionGroupDataResponseDoc: + admin.PermissionGroupDataResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupDataResponse' + $ref: '#/definitions/admin.PermissionGroupDataResponse' errorMsg: type: string type: object - internal_transport_http_admin.PermissionGroupDeleteSummaryResponse: + admin.PermissionGroupDeleteSummaryResponse: properties: manualModelCount: type: integer @@ -545,21 +520,21 @@ definitions: ruleCount: type: integer type: object - internal_transport_http_admin.PermissionGroupListResponse: + admin.PermissionGroupListResponse: properties: results: items: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupResponse' + $ref: '#/definitions/admin.PermissionGroupResponse' type: array type: object - internal_transport_http_admin.PermissionGroupListResponseDoc: + admin.PermissionGroupListResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupListResponse' + $ref: '#/definitions/admin.PermissionGroupListResponse' errorMsg: type: string type: object - internal_transport_http_admin.PermissionGroupModelRuleRequest: + admin.PermissionGroupModelRuleRequest: properties: type: maxLength: 32 @@ -570,14 +545,14 @@ definitions: required: - type type: object - internal_transport_http_admin.PermissionGroupModelRuleResponse: + admin.PermissionGroupModelRuleResponse: properties: type: type: string value: type: string type: object - internal_transport_http_admin.PermissionGroupResponse: + admin.PermissionGroupResponse: properties: createdAt: type: string @@ -606,7 +581,7 @@ definitions: userCount: type: integer type: object - internal_transport_http_admin.ResetUserPasswordRequest: + admin.ResetUserPasswordRequest: properties: mustResetPassword: type: boolean @@ -617,31 +592,31 @@ definitions: required: - newPassword type: object - internal_transport_http_admin.ResetUserPasswordResponse: + admin.ResetUserPasswordResponse: properties: reset: type: boolean type: object - internal_transport_http_admin.ResetUserPasswordResponseDoc: + admin.ResetUserPasswordResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.ResetUserPasswordResponse' + $ref: '#/definitions/admin.ResetUserPasswordResponse' errorMsg: type: string type: object - internal_transport_http_admin.RevokeUserSessionsResponse: + admin.RevokeUserSessionsResponse: properties: revoked: type: boolean type: object - internal_transport_http_admin.RevokeUserSessionsResponseDoc: + admin.RevokeUserSessionsResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.RevokeUserSessionsResponse' + $ref: '#/definitions/admin.RevokeUserSessionsResponse' errorMsg: type: string type: object - internal_transport_http_admin.SetGroupModelsRequest: + admin.SetGroupModelsRequest: properties: modelIDs: items: @@ -649,30 +624,30 @@ definitions: type: array rules: items: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupModelRuleRequest' + $ref: '#/definitions/admin.PermissionGroupModelRuleRequest' type: array type: object - internal_transport_http_admin.SetGroupUsersRequest: + admin.SetGroupUsersRequest: properties: userIDs: items: type: integer type: array type: object - internal_transport_http_admin.SetModelPermissionGroupsRequest: + admin.SetModelPermissionGroupsRequest: properties: groupIDs: items: type: integer type: array type: object - internal_transport_http_admin.SystemEventListResponseDoc: + admin.SystemEventListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/internal_transport_http_admin.SystemEventResponse' + $ref: '#/definitions/admin.SystemEventResponse' type: array total: type: integer @@ -680,7 +655,7 @@ definitions: errorMsg: type: string type: object - internal_transport_http_admin.SystemEventResponse: + admin.SystemEventResponse: properties: createdAt: type: string @@ -707,7 +682,7 @@ definitions: updatedAt: type: string type: object - internal_transport_http_admin.UpdatePermissionGroupRequest: + admin.UpdatePermissionGroupRequest: properties: description: maxLength: 512 @@ -722,7 +697,7 @@ definitions: required: - name type: object - internal_transport_http_admin.UpdateUserStatusRequest: + admin.UpdateUserStatusRequest: properties: reason: maxLength: 255 @@ -733,20 +708,20 @@ definitions: required: - status type: object - internal_transport_http_admin.UpdateUserStatusResponseDoc: + admin.UpdateUserStatusResponseDoc: properties: data: - $ref: '#/definitions/internal_transport_http_admin.UserDataResponse' + $ref: '#/definitions/admin.UserDataResponse' errorMsg: type: string type: object - internal_transport_http_admin.UsageLogListResponseDoc: + admin.UsageLogListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/internal_transport_http_admin.UsageLogResponse' + $ref: '#/definitions/admin.UsageLogResponse' type: array total: type: integer @@ -754,7 +729,7 @@ definitions: errorMsg: type: string type: object - internal_transport_http_admin.UsageLogResponse: + admin.UsageLogResponse: properties: billedCurrency: type: string @@ -821,13 +796,166 @@ definitions: username: type: string type: object - internal_transport_http_admin.UserAuthEventListResponseDoc: + admin.UsageStatisticsMetricsResponse: + properties: + avgLatencyMS: + type: integer + billedNanousd: + type: integer + billedUSD: + type: number + cacheReadTokens: + type: integer + cacheWriteTokens: + type: integer + callCount: + type: integer + inputTokens: + type: integer + outputTokens: + type: integer + reasoningTokens: + type: integer + recordCount: + type: integer + totalTokens: + type: integer + type: object + admin.UsageStatisticsModelRankResponse: + properties: + avgLatencyMS: + type: integer + billedNanousd: + type: integer + billedUSD: + type: number + cacheReadTokens: + type: integer + cacheWriteTokens: + type: integer + callCount: + type: integer + inputTokens: + type: integer + outputTokens: + type: integer + platformModelName: + type: string + reasoningTokens: + type: integer + recordCount: + type: integer + totalTokens: + type: integer + trend: + items: + $ref: '#/definitions/admin.UsageStatisticsTrendResponse' + type: array + type: object + admin.UsageStatisticsResponse: + properties: + range: + properties: + endDate: + type: string + granularity: + type: string + startDate: + type: string + type: object + topModels: + items: + $ref: '#/definitions/admin.UsageStatisticsModelRankResponse' + type: array + topUsers: + items: + $ref: '#/definitions/admin.UsageStatisticsUserRankResponse' + type: array + totals: + $ref: '#/definitions/admin.UsageStatisticsMetricsResponse' + trend: + items: + $ref: '#/definitions/admin.UsageStatisticsTrendResponse' + type: array + type: object + admin.UsageStatisticsResponseDoc: + properties: + data: + $ref: '#/definitions/admin.UsageStatisticsResponse' + errorMsg: + type: string + type: object + admin.UsageStatisticsTrendResponse: + properties: + avgLatencyMS: + type: integer + billedNanousd: + type: integer + billedUSD: + type: number + cacheReadTokens: + type: integer + cacheWriteTokens: + type: integer + callCount: + type: integer + inputTokens: + type: integer + outputTokens: + type: integer + periodStart: + type: string + reasoningTokens: + type: integer + recordCount: + type: integer + totalTokens: + type: integer + type: object + admin.UsageStatisticsUserRankResponse: + properties: + avgLatencyMS: + type: integer + billedNanousd: + type: integer + billedUSD: + type: number + cacheReadTokens: + type: integer + cacheWriteTokens: + type: integer + callCount: + type: integer + inputTokens: + type: integer + outputTokens: + type: integer + reasoningTokens: + type: integer + recordCount: + type: integer + totalTokens: + type: integer + trend: + items: + $ref: '#/definitions/admin.UsageStatisticsTrendResponse' + type: array + userDisplayName: + type: string + userID: + type: integer + userLabel: + type: string + username: + type: string + type: object + admin.UserAuthEventListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/internal_transport_http_admin.AuthEventResponse' + $ref: '#/definitions/admin.AuthEventResponse' type: array total: type: integer @@ -835,12 +963,12 @@ definitions: errorMsg: type: string type: object - internal_transport_http_admin.UserDataResponse: + admin.UserDataResponse: properties: user: - $ref: '#/definitions/internal_transport_http_admin.UserResponse' + $ref: '#/definitions/admin.UserResponse' type: object - internal_transport_http_admin.UserIdentityProviderSummaryResponse: + admin.UserIdentityProviderSummaryResponse: properties: id: type: integer @@ -853,13 +981,13 @@ definitions: type: type: string type: object - internal_transport_http_admin.UserListResponseDoc: + admin.UserListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/internal_transport_http_admin.UserResponse' + $ref: '#/definitions/admin.UserResponse' type: array total: type: integer @@ -867,7 +995,7 @@ definitions: errorMsg: type: string type: object - internal_transport_http_admin.UserResponse: + admin.UserResponse: properties: appearancePreferences: type: string @@ -893,7 +1021,7 @@ definitions: type: integer identityProviders: items: - $ref: '#/definitions/internal_transport_http_admin.UserIdentityProviderSummaryResponse' + $ref: '#/definitions/admin.UserIdentityProviderSummaryResponse' type: array lastActiveAt: type: string @@ -938,6 +1066,31 @@ definitions: username: type: string type: object + github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope: + properties: + data: {} + details: {} + errorCode: + type: string + errorMsg: + type: string + requestId: + type: string + type: object + github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc: + properties: + data: {} + details: {} + errorCode: + example: "" + type: string + errorMsg: + example: "" + type: string + requestId: + example: "" + type: string + type: object internal_transport_http_announcement.AdminAnnouncementListResponseDoc: properties: data: @@ -2120,6 +2273,48 @@ definitions: unit: type: string type: object + internal_transport_http_billing.OpenRouterOfficialPricingDataResponse: + properties: + cached: + type: boolean + fetchedAt: + type: string + items: + items: + $ref: '#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingItemResponse' + type: array + stale: + type: boolean + type: object + internal_transport_http_billing.OpenRouterOfficialPricingItemResponse: + properties: + canonicalSlug: + type: string + id: + type: string + name: + type: string + pricing: + $ref: '#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingUnitPricingResponse' + type: object + internal_transport_http_billing.OpenRouterOfficialPricingResponseDoc: + properties: + data: + $ref: '#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingDataResponse' + errorMsg: + type: string + type: object + internal_transport_http_billing.OpenRouterOfficialPricingUnitPricingResponse: + properties: + completion: + type: string + inputCacheRead: + type: string + inputCacheWrite: + type: string + prompt: + type: string + type: object internal_transport_http_billing.PatchRedemptionCodeRequestDoc: properties: description: @@ -3031,6 +3226,7 @@ definitions: - chat - image_generation - image_edit + - video_generation type: string type: object internal_transport_http_channel.ModelProbeResponse: @@ -5360,11 +5556,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.AuditLogListResponseDoc' + $ref: '#/definitions/admin.AuditLogListResponseDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询审计日志 @@ -5524,6 +5720,32 @@ paths: summary: 管理员保存模型按量单价 tags: - admin-billing + /admin/billing/official-pricing/openrouter: + get: + consumes: + - application/json + description: 从 storage 缓存读取 OpenRouter 模型定价;缓存不存在、过期或 refresh=true 时由后端刷新。 + parameters: + - description: 强制刷新缓存 + in: query + name: refresh + type: boolean + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_transport_http_billing.OpenRouterOfficialPricingResponseDoc' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/internal_transport_http_billing.ErrorDoc' + security: + - BearerAuth: [] + summary: 管理员获取 OpenRouter 官方模型定价 + tags: + - admin-billing /admin/billing/plans/{id}: patch: consumes: @@ -5824,15 +6046,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.UsageLogListResponseDoc' + $ref: '#/definitions/admin.UsageLogListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询模型调用日志 @@ -5894,15 +6116,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.ConversationEventListResponseDoc' + $ref: '#/definitions/admin.ConversationEventListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询对话事件 @@ -5921,7 +6143,7 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员导出全量对话数据 @@ -7209,22 +7431,22 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.CleanupLogsRequest' + $ref: '#/definitions/admin.CleanupLogsRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.CleanupLogsResponseDoc' + $ref: '#/definitions/admin.CleanupLogsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员清理日志 @@ -7247,19 +7469,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc' + $ref: '#/definitions/admin.ModelPermissionGroupsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员列出模型权限组 @@ -7280,26 +7502,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.SetModelPermissionGroupsRequest' + $ref: '#/definitions/admin.SetModelPermissionGroupsRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc' + $ref: '#/definitions/admin.ModelPermissionGroupsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员设置模型手动权限组 @@ -7357,15 +7579,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.PaymentOrderListResponseDoc' + $ref: '#/definitions/admin.PaymentOrderListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询支付订单记录 @@ -7382,11 +7604,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupListResponseDoc' + $ref: '#/definitions/admin.PermissionGroupListResponseDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员列出权限组 @@ -7402,22 +7624,22 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.CreatePermissionGroupRequest' + $ref: '#/definitions/admin.CreatePermissionGroupRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc' + $ref: '#/definitions/admin.PermissionGroupDataResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员创建权限组 @@ -7440,23 +7662,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.DeletePermissionGroupResponseDoc' + $ref: '#/definitions/admin.DeletePermissionGroupResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员删除权限组 @@ -7477,26 +7699,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.UpdatePermissionGroupRequest' + $ref: '#/definitions/admin.UpdatePermissionGroupRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc' + $ref: '#/definitions/admin.PermissionGroupDataResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员更新权限组 @@ -7519,19 +7741,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.GroupModelsResponseDoc' + $ref: '#/definitions/admin.GroupModelsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员列出权限组模型 @@ -7552,26 +7774,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.SetGroupModelsRequest' + $ref: '#/definitions/admin.SetGroupModelsRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.GroupModelsResponseDoc' + $ref: '#/definitions/admin.GroupModelsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员设置权限组模型 @@ -7594,23 +7816,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.GroupUsersResponseDoc' + $ref: '#/definitions/admin.GroupUsersResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员列出权限组用户 @@ -7631,30 +7853,30 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.SetGroupUsersRequest' + $ref: '#/definitions/admin.SetGroupUsersRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.GroupUsersResponseDoc' + $ref: '#/definitions/admin.GroupUsersResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员设置权限组用户 @@ -8259,20 +8481,70 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.SystemEventListResponseDoc' + $ref: '#/definitions/admin.SystemEventListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询系统事件 tags: - admin + /admin/usage-statistics: + get: + consumes: + - application/json + description: 管理员按日期、用户、平台模型和计费范围查看全局费用、Token、调用次数及排名 + parameters: + - description: 开始日期(YYYY-MM-DD),默认近30天 + in: query + name: start_date + type: string + - description: 结束日期(YYYY-MM-DD,包含当日) + in: query + name: end_date + type: string + - description: 用户ID + in: query + name: user_id + type: integer + - description: 平台模型名 + in: query + name: platform_model_name + type: string + - description: 计费范围:all/free/billable + in: query + name: billing_scope + type: string + - description: 排名指标:cost/tokens/calls + in: query + name: rank_by + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/admin.UsageStatisticsResponseDoc' + "400": + description: Bad Request + schema: + $ref: '#/definitions/admin.ErrorDoc' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/admin.ErrorDoc' + security: + - BearerAuth: [] + summary: 管理员查询全局用量统计 + tags: + - admin /admin/user-auth-events: get: consumes: @@ -8305,15 +8577,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.UserAuthEventListResponseDoc' + $ref: '#/definitions/admin.UserAuthEventListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询用户认证事件 @@ -8351,11 +8623,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.UserListResponseDoc' + $ref: '#/definitions/admin.UserListResponseDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询用户 @@ -8371,26 +8643,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.CreateUserRequest' + $ref: '#/definitions/admin.CreateUserRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.CreateUserResponseDoc' + $ref: '#/definitions/admin.CreateUserResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员创建用户 @@ -8413,23 +8685,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.DeleteUserResponseDoc' + $ref: '#/definitions/admin.DeleteUserResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员删除用户 @@ -8450,30 +8722,30 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.PatchUserRequest' + $ref: '#/definitions/admin.PatchUserRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc' + $ref: '#/definitions/admin.UpdateUserStatusResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员更新用户可编辑字段 @@ -8495,30 +8767,30 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.ResetUserPasswordRequest' + $ref: '#/definitions/admin.ResetUserPasswordRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.ResetUserPasswordResponseDoc' + $ref: '#/definitions/admin.ResetUserPasswordResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员重置用户密码 @@ -8541,19 +8813,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.RevokeUserSessionsResponseDoc' + $ref: '#/definitions/admin.RevokeUserSessionsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员吊销用户全部会话 @@ -8575,30 +8847,30 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.UpdateUserStatusRequest' + $ref: '#/definitions/admin.UpdateUserStatusRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc' + $ref: '#/definitions/admin.UpdateUserStatusResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员更新用户状态 @@ -8615,26 +8887,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersRequest' + $ref: '#/definitions/admin.ImportOpenWebUIUsersRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc' + $ref: '#/definitions/admin.ImportOpenWebUIUsersResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "403": description: Forbidden schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + $ref: '#/definitions/admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员导入 OpenWebUI 用户 @@ -10347,6 +10619,25 @@ paths: summary: 查询新会话默认模型候选 tags: - chat + /conversations/export: + get: + description: 流式导出当前用户全部会话及消息为 NDJSON 文件 + produces: + - application/x-ndjson + responses: + "200": + description: NDJSON stream + schema: + type: string + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_transport_http_conversation.ErrorDoc' + security: + - BearerAuth: [] + summary: 导出当前用户全部对话 + tags: + - chat /conversations/project: post: consumes: diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index f47228d1..64f7b040 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -283,6 +283,7 @@ func NewApp() (*App, error) { adminService.SetAuthSecurityService(authService) adminService.SetSystemEventService(systemEventService) adminService.SetUsageLogService(billingService) + adminService.SetUsageStatisticsService(billingService) adminService.SetOrderLogService(billingService) adminService.SetConversationEventService(conversationService) adminService.SetLogCleanupService(logCleanupService) diff --git a/backend/internal/application/admin/service.go b/backend/internal/application/admin/service.go index 592d36c6..8f13f1ed 100644 --- a/backend/internal/application/admin/service.go +++ b/backend/internal/application/admin/service.go @@ -96,6 +96,10 @@ type usageLogService interface { ListUsageLogs(ctx context.Context, page int, pageSize int, filter billing.UsageLogListFilter) ([]domainbilling.UsageLedger, int64, error) } +type usageStatisticsService interface { + GetUsageStatistics(ctx context.Context, filter billing.UsageStatisticsFilter) (domainbilling.UsageStatistics, error) +} + type orderLogService interface { ListPaymentOrders(ctx context.Context, page int, pageSize int, filter billing.PaymentOrderListFilter) ([]domainbilling.PaymentOrder, int64, error) } @@ -119,6 +123,7 @@ type Service struct { auditService auditService systemEventService systemEventService usageLogService usageLogService + usageStatisticsService usageStatisticsService orderLogService orderLogService conversationEventSvc conversationEventService logCleanupService logCleanupService @@ -187,6 +192,11 @@ func (s *Service) SetUsageLogService(service usageLogService) { s.usageLogService = service } +// SetUsageStatisticsService 注入管理员用量统计能力。 +func (s *Service) SetUsageStatisticsService(service usageStatisticsService) { + s.usageStatisticsService = service +} + // SetOrderLogService 注入支付订单日志查询能力。 func (s *Service) SetOrderLogService(service orderLogService) { s.orderLogService = service diff --git a/backend/internal/application/admin/service_logs.go b/backend/internal/application/admin/service_logs.go index 446829c4..20a35950 100644 --- a/backend/internal/application/admin/service_logs.go +++ b/backend/internal/application/admin/service_logs.go @@ -28,6 +28,14 @@ func (s *Service) ListUsageLogs(ctx context.Context, page int, pageSize int, fil return s.usageLogService.ListUsageLogs(ctx, page, pageSize, filter) } +// GetUsageStatistics 查询管理员仪表盘的全局用量聚合。 +func (s *Service) GetUsageStatistics(ctx context.Context, filter billing.UsageStatisticsFilter) (domainbilling.UsageStatistics, error) { + if s.usageStatisticsService == nil { + return domainbilling.UsageStatistics{}, errors.New("usage statistics service unavailable") + } + return s.usageStatisticsService.GetUsageStatistics(ctx, filter) +} + // ListPaymentOrders 查询管理员支付订单记录。 func (s *Service) ListPaymentOrders(ctx context.Context, page int, pageSize int, filter billing.PaymentOrderListFilter) ([]domainbilling.PaymentOrder, int64, error) { if s.orderLogService == nil { diff --git a/backend/internal/application/billing/service.go b/backend/internal/application/billing/service.go index 77d67642..0727a892 100644 --- a/backend/internal/application/billing/service.go +++ b/backend/internal/application/billing/service.go @@ -2266,6 +2266,87 @@ func (s *Service) ListUsageLogs(ctx context.Context, page int, pageSize int, fil }, offset, limit) } +// UsageStatisticsFilter 描述管理员仪表盘的用量统计条件。 +type UsageStatisticsFilter struct { + StartDate time.Time + EndDate time.Time + UserID uint + PlatformModelName string + BillingScope string + RankBy string +} + +// GetUsageStatistics 查询管理员仪表盘使用的全局用量统计。 +func (s *Service) GetUsageStatistics(ctx context.Context, filter UsageStatisticsFilter) (domainbilling.UsageStatistics, error) { + statisticsRepo, ok := s.repo.(repository.UsageStatisticsRepository) + if !ok { + return domainbilling.UsageStatistics{}, errors.New("usage statistics repository unavailable") + } + + startDate := time.Date(filter.StartDate.Year(), filter.StartDate.Month(), filter.StartDate.Day(), 0, 0, 0, 0, filter.StartDate.Location()) + endDate := time.Date(filter.EndDate.Year(), filter.EndDate.Month(), filter.EndDate.Day(), 0, 0, 0, 0, filter.EndDate.Location()) + if endDate.Before(startDate) { + return domainbilling.UsageStatistics{}, errors.New("invalid usage statistics date range") + } + days := int(endDate.Sub(startDate).Hours()/24) + 1 + if days <= 0 || days > 366 { + return domainbilling.UsageStatistics{}, errors.New("invalid usage statistics date range") + } + + granularity := "day" + if days > 90 { + granularity = "month" + } + result, err := statisticsRepo.GetUsageStatistics(ctx, repository.UsageStatisticsFilter{ + StartDate: startDate, + EndDateExclusive: endDate.AddDate(0, 0, 1), + UserID: filter.UserID, + PlatformModelName: strings.TrimSpace(filter.PlatformModelName), + BillingScope: strings.TrimSpace(filter.BillingScope), + Granularity: granularity, + RankBy: strings.TrimSpace(filter.RankBy), + RankLimit: 10, + }) + if err != nil { + return domainbilling.UsageStatistics{}, err + } + result.Granularity = granularity + result.Trend = fillUsageStatisticsTrend(result.Trend, startDate, endDate, granularity) + return result, nil +} + +func fillUsageStatisticsTrend( + items []domainbilling.UsageStatisticsTrendPoint, + startDate time.Time, + endDate time.Time, + granularity string, +) []domainbilling.UsageStatisticsTrendPoint { + byPeriod := make(map[string]domainbilling.UsageStatisticsTrendPoint, len(items)) + for _, item := range items { + byPeriod[item.PeriodStart.Format("2006-01-02")] = item + } + + current := startDate + if granularity == "month" { + current = time.Date(startDate.Year(), startDate.Month(), 1, 0, 0, 0, 0, startDate.Location()) + } + results := make([]domainbilling.UsageStatisticsTrendPoint, 0, len(items)) + for !current.After(endDate) { + key := current.Format("2006-01-02") + if item, exists := byPeriod[key]; exists { + results = append(results, item) + } else { + results = append(results, domainbilling.UsageStatisticsTrendPoint{PeriodStart: current}) + } + if granularity == "month" { + current = current.AddDate(0, 1, 0) + } else { + current = current.AddDate(0, 0, 1) + } + } + return results +} + // ListPaymentOrders 分页查询管理员支付订单记录。 func (s *Service) ListPaymentOrders(ctx context.Context, page int, pageSize int, filter PaymentOrderListFilter) ([]domainbilling.PaymentOrder, int64, error) { offset, limit := normalizePage(page, pageSize) diff --git a/backend/internal/domain/billing/types.go b/backend/internal/domain/billing/types.go index c4190fe6..367c0899 100644 --- a/backend/internal/domain/billing/types.go +++ b/backend/internal/domain/billing/types.go @@ -406,3 +406,45 @@ type UsageDailyModelSummary struct { AvgLatencyMS int64 BilledNanousd int64 } + +// UsageStatisticsMetrics 表示一组聚合后的用量指标。 +type UsageStatisticsMetrics struct { + RecordCount int64 + InputTokens int64 + CacheReadTokens int64 + CacheWriteTokens int64 + OutputTokens int64 + ReasoningTokens int64 + CallCount int64 + AvgLatencyMS int64 + BilledNanousd int64 +} + +// UsageStatisticsTrendPoint 表示一个按日或按月聚合的趋势点。 +type UsageStatisticsTrendPoint struct { + PeriodStart time.Time + Metrics UsageStatisticsMetrics +} + +// UsageStatisticsModelRank 表示平台模型的用量排名项。 +type UsageStatisticsModelRank struct { + PlatformModelName string + Metrics UsageStatisticsMetrics + Trend []UsageStatisticsTrendPoint +} + +// UsageStatisticsUserRank 表示用户的用量排名项。 +type UsageStatisticsUserRank struct { + UserID uint + Metrics UsageStatisticsMetrics + Trend []UsageStatisticsTrendPoint +} + +// UsageStatistics 表示管理员仪表盘使用的全局用量聚合。 +type UsageStatistics struct { + Granularity string + Totals UsageStatisticsMetrics + Trend []UsageStatisticsTrendPoint + TopModels []UsageStatisticsModelRank + TopUsers []UsageStatisticsUserRank +} diff --git a/backend/internal/infra/persistence/postgres/billing/repository.go b/backend/internal/infra/persistence/postgres/billing/repository.go index d22740a6..58add142 100644 --- a/backend/internal/infra/persistence/postgres/billing/repository.go +++ b/backend/internal/infra/persistence/postgres/billing/repository.go @@ -1447,6 +1447,245 @@ func (r *Repo) ListUsageLogs(ctx context.Context, filter repository.UsageLogList return results, total, nil } +type usageStatisticsMetricRow struct { + RecordCount int64 `gorm:"column:record_count"` + InputTokens int64 `gorm:"column:input_tokens"` + CacheReadTokens int64 `gorm:"column:cache_read_tokens"` + CacheWriteTokens int64 `gorm:"column:cache_write_tokens"` + OutputTokens int64 `gorm:"column:output_tokens"` + ReasoningTokens int64 `gorm:"column:reasoning_tokens"` + TotalTokens int64 `gorm:"column:total_tokens"` + CallCount int64 `gorm:"column:call_count"` + AvgLatencyMS int64 `gorm:"column:avg_latency_ms"` + BilledNanousd int64 `gorm:"column:billed_nanousd"` +} + +type usageStatisticsTrendRow struct { + PeriodKey string `gorm:"column:period_key"` + Metrics usageStatisticsMetricRow `gorm:"embedded"` +} + +type usageStatisticsModelRow struct { + PlatformModelName string `gorm:"column:platform_model_name"` + Metrics usageStatisticsMetricRow `gorm:"embedded"` +} + +type usageStatisticsModelTrendRow struct { + PeriodKey string `gorm:"column:period_key"` + PlatformModelName string `gorm:"column:platform_model_name"` + Metrics usageStatisticsMetricRow `gorm:"embedded"` +} + +type usageStatisticsUserRow struct { + UserID uint `gorm:"column:user_id"` + Metrics usageStatisticsMetricRow `gorm:"embedded"` +} + +type usageStatisticsUserTrendRow struct { + PeriodKey string `gorm:"column:period_key"` + UserID uint `gorm:"column:user_id"` + Metrics usageStatisticsMetricRow `gorm:"embedded"` +} + +const usageStatisticsMetricsSelect = ` + COUNT(*) AS record_count, + COALESCE(SUM(input_tokens), 0) AS input_tokens, + COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, + COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens, + COALESCE(SUM(output_tokens), 0) AS output_tokens, + COALESCE(SUM(reasoning_tokens), 0) AS reasoning_tokens, + COALESCE(SUM(input_tokens + cache_read_tokens + cache_write_tokens + output_tokens + reasoning_tokens), 0) AS total_tokens, + COALESCE(SUM(call_count), 0) AS call_count, + COALESCE(ROUND(AVG(NULLIF(latency_ms, 0))), 0) AS avg_latency_ms, + COALESCE(SUM(billed_nanousd), 0) AS billed_nanousd` + +func usageStatisticsMetricsFromRow(row usageStatisticsMetricRow) domainbilling.UsageStatisticsMetrics { + return domainbilling.UsageStatisticsMetrics{ + RecordCount: row.RecordCount, + InputTokens: row.InputTokens, + CacheReadTokens: row.CacheReadTokens, + CacheWriteTokens: row.CacheWriteTokens, + OutputTokens: row.OutputTokens, + ReasoningTokens: row.ReasoningTokens, + CallCount: row.CallCount, + AvgLatencyMS: row.AvgLatencyMS, + BilledNanousd: row.BilledNanousd, + } +} + +func (r *Repo) usageStatisticsQuery(ctx context.Context, filter repository.UsageStatisticsFilter) *gorm.DB { + query := r.db.WithContext(ctx). + Model(&model.UsageLedger{}). + Where("usage_date >= ? AND usage_date < ?", filter.StartDate, filter.EndDateExclusive) + if filter.UserID > 0 { + query = query.Where("user_id = ?", filter.UserID) + } + if platformModelName := strings.TrimSpace(filter.PlatformModelName); platformModelName != "" { + query = query.Where("platform_model_name = ?", platformModelName) + } + switch strings.TrimSpace(filter.BillingScope) { + case "free": + query = query.Where("is_free_model = ?", true) + case "billable": + query = query.Where("is_free_model = ?", false) + } + return query +} + +func usageStatisticsRankOrder(rankBy string, finalTieBreaker string) string { + primary := "billed_nanousd DESC" + switch strings.TrimSpace(rankBy) { + case "tokens": + primary = "total_tokens DESC" + case "calls": + primary = "call_count DESC" + } + return primary + ", total_tokens DESC, billed_nanousd DESC, " + finalTieBreaker +} + +// GetUsageStatistics 查询管理员仪表盘使用的全局用量聚合。 +func (r *Repo) GetUsageStatistics(ctx context.Context, filter repository.UsageStatisticsFilter) (domainbilling.UsageStatistics, error) { + result := domainbilling.UsageStatistics{ + Granularity: filter.Granularity, + Trend: []domainbilling.UsageStatisticsTrendPoint{}, + TopModels: []domainbilling.UsageStatisticsModelRank{}, + TopUsers: []domainbilling.UsageStatisticsUserRank{}, + } + + var totals usageStatisticsMetricRow + if err := r.usageStatisticsQuery(ctx, filter). + Select(usageStatisticsMetricsSelect). + Scan(&totals).Error; err != nil { + return result, translateError(err) + } + result.Totals = usageStatisticsMetricsFromRow(totals) + + periodExpression := r.usageDayKeyExpression() + if filter.Granularity == "month" { + periodExpression = r.usageMonthKeyExpression() + } + trendRows := make([]usageStatisticsTrendRow, 0) + if err := r.usageStatisticsQuery(ctx, filter). + Select(periodExpression + " AS period_key," + usageStatisticsMetricsSelect). + Group(periodExpression). + Order("period_key ASC"). + Scan(&trendRows).Error; err != nil { + return result, translateError(err) + } + for _, row := range trendRows { + periodStart, err := time.Parse("2006-01-02", row.PeriodKey) + if err != nil { + return result, err + } + result.Trend = append(result.Trend, domainbilling.UsageStatisticsTrendPoint{ + PeriodStart: periodStart, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + }) + } + + rankLimit := filter.RankLimit + if rankLimit <= 0 || rankLimit > 50 { + rankLimit = 10 + } + modelRows := make([]usageStatisticsModelRow, 0, rankLimit) + if err := r.usageStatisticsQuery(ctx, filter). + Select("platform_model_name," + usageStatisticsMetricsSelect). + Group("platform_model_name"). + Order(usageStatisticsRankOrder(filter.RankBy, "platform_model_name ASC")). + Limit(rankLimit). + Scan(&modelRows).Error; err != nil { + return result, translateError(err) + } + for _, row := range modelRows { + result.TopModels = append(result.TopModels, domainbilling.UsageStatisticsModelRank{ + PlatformModelName: row.PlatformModelName, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + Trend: []domainbilling.UsageStatisticsTrendPoint{}, + }) + } + if len(result.TopModels) > 0 { + modelNames := make([]string, 0, len(result.TopModels)) + modelIndexes := make(map[string]int, len(result.TopModels)) + for index, item := range result.TopModels { + modelNames = append(modelNames, item.PlatformModelName) + modelIndexes[item.PlatformModelName] = index + } + modelTrendRows := make([]usageStatisticsModelTrendRow, 0) + if err := r.usageStatisticsQuery(ctx, filter). + Where("platform_model_name IN ?", modelNames). + Select(periodExpression + " AS period_key, platform_model_name," + usageStatisticsMetricsSelect). + Group(periodExpression + ", platform_model_name"). + Order("period_key ASC, platform_model_name ASC"). + Scan(&modelTrendRows).Error; err != nil { + return result, translateError(err) + } + for _, row := range modelTrendRows { + periodStart, err := time.Parse("2006-01-02", row.PeriodKey) + if err != nil { + return result, err + } + index, exists := modelIndexes[row.PlatformModelName] + if !exists { + continue + } + result.TopModels[index].Trend = append(result.TopModels[index].Trend, domainbilling.UsageStatisticsTrendPoint{ + PeriodStart: periodStart, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + }) + } + } + + userRows := make([]usageStatisticsUserRow, 0, rankLimit) + if err := r.usageStatisticsQuery(ctx, filter). + Select("user_id," + usageStatisticsMetricsSelect). + Group("user_id"). + Order(usageStatisticsRankOrder(filter.RankBy, "user_id ASC")). + Limit(rankLimit). + Scan(&userRows).Error; err != nil { + return result, translateError(err) + } + for _, row := range userRows { + result.TopUsers = append(result.TopUsers, domainbilling.UsageStatisticsUserRank{ + UserID: row.UserID, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + Trend: []domainbilling.UsageStatisticsTrendPoint{}, + }) + } + if len(result.TopUsers) > 0 { + userIDs := make([]uint, 0, len(result.TopUsers)) + userIndexes := make(map[uint]int, len(result.TopUsers)) + for index, item := range result.TopUsers { + userIDs = append(userIDs, item.UserID) + userIndexes[item.UserID] = index + } + userTrendRows := make([]usageStatisticsUserTrendRow, 0) + if err := r.usageStatisticsQuery(ctx, filter). + Where("user_id IN ?", userIDs). + Select(periodExpression + " AS period_key, user_id," + usageStatisticsMetricsSelect). + Group(periodExpression + ", user_id"). + Order("period_key ASC, user_id ASC"). + Scan(&userTrendRows).Error; err != nil { + return result, translateError(err) + } + for _, row := range userTrendRows { + periodStart, err := time.Parse("2006-01-02", row.PeriodKey) + if err != nil { + return result, err + } + index, exists := userIndexes[row.UserID] + if !exists { + continue + } + result.TopUsers[index].Trend = append(result.TopUsers[index].Trend, domainbilling.UsageStatisticsTrendPoint{ + PeriodStart: periodStart, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + }) + } + } + + return result, nil +} + // ListPaymentOrders 分页查询管理员支付订单记录。 func (r *Repo) ListPaymentOrders(ctx context.Context, filter repository.PaymentOrderListFilter, offset int, limit int) ([]domainbilling.PaymentOrder, int64, error) { items := make([]model.PaymentOrder, 0) diff --git a/backend/internal/repository/billing.go b/backend/internal/repository/billing.go index f0922aca..b6181aa0 100644 --- a/backend/internal/repository/billing.go +++ b/backend/internal/repository/billing.go @@ -111,6 +111,24 @@ type UsageLogListFilter struct { Sort string } +// UsageStatisticsFilter 描述管理员用量统计的聚合范围和维度。 +type UsageStatisticsFilter struct { + StartDate time.Time + EndDateExclusive time.Time + UserID uint + PlatformModelName string + BillingScope string + Granularity string + RankBy string + RankLimit int +} + +// UsageStatisticsRepository 提供独立于计费写入仓储的管理员统计能力。 +// 独立接口可避免为统计功能扩大 BillingRepository 及其测试桩。 +type UsageStatisticsRepository interface { + GetUsageStatistics(ctx context.Context, filter UsageStatisticsFilter) (domainbilling.UsageStatistics, error) +} + // PaymentOrderListFilter 描述管理员支付订单列表筛选和排序条件。 type PaymentOrderListFilter struct { Query string diff --git a/backend/internal/transport/http/admin/dto.go b/backend/internal/transport/http/admin/dto.go index 60e855de..9200ca63 100644 --- a/backend/internal/transport/http/admin/dto.go +++ b/backend/internal/transport/http/admin/dto.go @@ -287,6 +287,57 @@ type UsageLogResponse struct { UpdatedAt time.Time `json:"updatedAt"` } +// UsageStatisticsMetricsResponse 用量统计指标响应。 +type UsageStatisticsMetricsResponse struct { + RecordCount int64 `json:"recordCount"` + InputTokens int64 `json:"inputTokens"` + CacheReadTokens int64 `json:"cacheReadTokens"` + CacheWriteTokens int64 `json:"cacheWriteTokens"` + OutputTokens int64 `json:"outputTokens"` + ReasoningTokens int64 `json:"reasoningTokens"` + TotalTokens int64 `json:"totalTokens"` + CallCount int64 `json:"callCount"` + AvgLatencyMS int64 `json:"avgLatencyMS"` + BilledNanousd int64 `json:"billedNanousd"` + BilledUSD float64 `json:"billedUSD"` +} + +// UsageStatisticsTrendResponse 用量趋势点响应。 +type UsageStatisticsTrendResponse struct { + PeriodStart time.Time `json:"periodStart"` + UsageStatisticsMetricsResponse +} + +// UsageStatisticsModelRankResponse 模型排名响应。 +type UsageStatisticsModelRankResponse struct { + PlatformModelName string `json:"platformModelName"` + UsageStatisticsMetricsResponse + Trend []UsageStatisticsTrendResponse `json:"trend"` +} + +// UsageStatisticsUserRankResponse 用户排名响应。 +type UsageStatisticsUserRankResponse struct { + UserID uint `json:"userID"` + Username string `json:"username"` + UserDisplayName string `json:"userDisplayName"` + UserLabel string `json:"userLabel"` + UsageStatisticsMetricsResponse + Trend []UsageStatisticsTrendResponse `json:"trend"` +} + +// UsageStatisticsResponse 管理员用量统计响应。 +type UsageStatisticsResponse struct { + Range struct { + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + Granularity string `json:"granularity"` + } `json:"range"` + Totals UsageStatisticsMetricsResponse `json:"totals"` + Trend []UsageStatisticsTrendResponse `json:"trend"` + TopModels []UsageStatisticsModelRankResponse `json:"topModels"` + TopUsers []UsageStatisticsUserRankResponse `json:"topUsers"` +} + // PaymentOrderResponse 支付订单记录响应。 type PaymentOrderResponse struct { ID uint `json:"id"` @@ -549,6 +600,12 @@ type UsageLogListResponseDoc struct { } `json:"data"` } +// UsageStatisticsResponseDoc 管理员用量统计响应。 +type UsageStatisticsResponseDoc struct { + ErrorMsg string `json:"errorMsg"` + Data UsageStatisticsResponse `json:"data"` +} + // PaymentOrderListResponseDoc 支付订单分页响应。 type PaymentOrderListResponseDoc struct { ErrorMsg string `json:"errorMsg"` @@ -740,6 +797,71 @@ func toUsageLogResponse(item domainbilling.UsageLedger, label appadmin.UserLabel } } +func toUsageStatisticsMetricsResponse(item domainbilling.UsageStatisticsMetrics) UsageStatisticsMetricsResponse { + totalTokens := item.InputTokens + item.CacheReadTokens + item.CacheWriteTokens + item.OutputTokens + item.ReasoningTokens + return UsageStatisticsMetricsResponse{ + RecordCount: item.RecordCount, + InputTokens: item.InputTokens, + CacheReadTokens: item.CacheReadTokens, + CacheWriteTokens: item.CacheWriteTokens, + OutputTokens: item.OutputTokens, + ReasoningTokens: item.ReasoningTokens, + TotalTokens: totalTokens, + CallCount: item.CallCount, + AvgLatencyMS: item.AvgLatencyMS, + BilledNanousd: item.BilledNanousd, + BilledUSD: float64(item.BilledNanousd) / 1_000_000_000, + } +} + +func toUsageStatisticsTrendResponses(items []domainbilling.UsageStatisticsTrendPoint) []UsageStatisticsTrendResponse { + result := make([]UsageStatisticsTrendResponse, 0, len(items)) + for _, point := range items { + result = append(result, UsageStatisticsTrendResponse{ + PeriodStart: point.PeriodStart, + UsageStatisticsMetricsResponse: toUsageStatisticsMetricsResponse(point.Metrics), + }) + } + return result +} + +func toUsageStatisticsResponse( + item domainbilling.UsageStatistics, + startDate time.Time, + endDate time.Time, + userLabels map[uint]appadmin.UserLabel, +) UsageStatisticsResponse { + result := UsageStatisticsResponse{ + Totals: toUsageStatisticsMetricsResponse(item.Totals), + Trend: make([]UsageStatisticsTrendResponse, 0, len(item.Trend)), + TopModels: make([]UsageStatisticsModelRankResponse, 0, len(item.TopModels)), + TopUsers: make([]UsageStatisticsUserRankResponse, 0, len(item.TopUsers)), + } + result.Range.StartDate = startDate.Format("2006-01-02") + result.Range.EndDate = endDate.Format("2006-01-02") + result.Range.Granularity = item.Granularity + result.Trend = toUsageStatisticsTrendResponses(item.Trend) + for _, model := range item.TopModels { + result.TopModels = append(result.TopModels, UsageStatisticsModelRankResponse{ + PlatformModelName: model.PlatformModelName, + UsageStatisticsMetricsResponse: toUsageStatisticsMetricsResponse(model.Metrics), + Trend: toUsageStatisticsTrendResponses(model.Trend), + }) + } + for _, rankedUser := range item.TopUsers { + label := userLabels[rankedUser.UserID] + result.TopUsers = append(result.TopUsers, UsageStatisticsUserRankResponse{ + UserID: rankedUser.UserID, + Username: label.Username, + UserDisplayName: label.DisplayName, + UserLabel: label.Label, + UsageStatisticsMetricsResponse: toUsageStatisticsMetricsResponse(rankedUser.Metrics), + Trend: toUsageStatisticsTrendResponses(rankedUser.Trend), + }) + } + return result +} + func toPaymentOrderResponse(item domainbilling.PaymentOrder, label appadmin.UserLabel) PaymentOrderResponse { return PaymentOrderResponse{ ID: item.ID, diff --git a/backend/internal/transport/http/admin/handler.go b/backend/internal/transport/http/admin/handler.go index 416d5689..616503d1 100644 --- a/backend/internal/transport/http/admin/handler.go +++ b/backend/internal/transport/http/admin/handler.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "time" appadmin "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/admin" @@ -484,6 +485,91 @@ func (h *Handler) ListUsageLogs(c *gin.Context) { response.SuccessPage(c, total, logs) } +// GetUsageStatistics godoc +// @Summary 管理员查询全局用量统计 +// @Description 管理员按日期、用户、平台模型和计费范围查看全局费用、Token、调用次数及排名 +// @Tags admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param start_date query string false "开始日期(YYYY-MM-DD),默认近30天" +// @Param end_date query string false "结束日期(YYYY-MM-DD,包含当日)" +// @Param user_id query int false "用户ID" +// @Param platform_model_name query string false "平台模型名" +// @Param billing_scope query string false "计费范围:all/free/billable" +// @Param rank_by query string false "排名指标:cost/tokens/calls" +// @Success 200 {object} UsageStatisticsResponseDoc +// @Failure 400 {object} ErrorDoc +// @Failure 500 {object} ErrorDoc +// @Router /admin/usage-statistics [get] +// GetUsageStatistics 查询管理员全局用量统计。 +func (h *Handler) GetUsageStatistics(c *gin.Context) { + userID, ok := parseOptionalUintQuery(c, "user_id") + if !ok { + return + } + + startDateText := strings.TrimSpace(c.Query("start_date")) + endDateText := strings.TrimSpace(c.Query("end_date")) + now := time.Now() + endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + startDate := endDate.AddDate(0, 0, -29) + if startDateText != "" || endDateText != "" { + if startDateText == "" || endDateText == "" { + response.Error(c, http.StatusBadRequest, "start_date and end_date must be provided together") + return + } + parsedStartDate, startErr := time.Parse("2006-01-02", startDateText) + parsedEndDate, endErr := time.Parse("2006-01-02", endDateText) + if startErr != nil || endErr != nil { + response.Error(c, http.StatusBadRequest, "invalid usage statistics date range") + return + } + startDate = parsedStartDate + endDate = parsedEndDate + } + if endDate.Before(startDate) || int(endDate.Sub(startDate).Hours()/24)+1 > 366 { + response.Error(c, http.StatusBadRequest, "invalid usage statistics date range") + return + } + + billingScope := strings.TrimSpace(c.Query("billing_scope")) + if billingScope == "" { + billingScope = "all" + } + if billingScope != "all" && billingScope != "free" && billingScope != "billable" { + response.Error(c, http.StatusBadRequest, "invalid billing_scope") + return + } + rankBy := strings.TrimSpace(c.Query("rank_by")) + if rankBy == "" { + rankBy = "cost" + } + if rankBy != "cost" && rankBy != "tokens" && rankBy != "calls" { + response.Error(c, http.StatusBadRequest, "invalid rank_by") + return + } + + statistics, err := h.service.GetUsageStatistics(c.Request.Context(), appbilling.UsageStatisticsFilter{ + StartDate: startDate, + EndDate: endDate, + UserID: userID, + PlatformModelName: c.Query("platform_model_name"), + BillingScope: billingScope, + RankBy: rankBy, + }) + if err != nil { + response.Error(c, http.StatusInternalServerError, "get usage statistics failed") + return + } + userIDs := make([]uint, 0, len(statistics.TopUsers)) + for _, rankedUser := range statistics.TopUsers { + userIDs = append(userIDs, rankedUser.UserID) + } + userLabels := h.service.ResolveUserLabels(c.Request.Context(), userIDs) + response.Success(c, toUsageStatisticsResponse(statistics, startDate, endDate, userLabels)) +} + // ListPaymentOrders godoc // @Summary 管理员查询支付订单记录 // @Description 管理员分页查看订阅和充值支付单 diff --git a/backend/internal/transport/http/admin/router.go b/backend/internal/transport/http/admin/router.go index 4a94e016..33f4286f 100644 --- a/backend/internal/transport/http/admin/router.go +++ b/backend/internal/transport/http/admin/router.go @@ -15,6 +15,7 @@ func (m *Module) RegisterRoutes(adminGroup *gin.RouterGroup) { adminGroup.DELETE("/users/:id", m.Handler.DeleteUser) adminGroup.GET("/user-auth-events", m.Handler.ListUserAuthEvents) adminGroup.GET("/audit-logs", m.Handler.ListAuditLogs) + adminGroup.GET("/usage-statistics", m.Handler.GetUsageStatistics) adminGroup.GET("/call-logs", m.Handler.ListUsageLogs) adminGroup.GET("/payment-orders", m.Handler.ListPaymentOrders) adminGroup.GET("/conversation-events", m.Handler.ListConversationEvents) diff --git a/frontend/app/(admin)/admin/page.tsx b/frontend/app/(admin)/admin/page.tsx index d4a5cf36..26269c91 100644 --- a/frontend/app/(admin)/admin/page.tsx +++ b/frontend/app/(admin)/admin/page.tsx @@ -1,5 +1,5 @@ -import { AdminAccountsPage } from "@/features/admin/components/sections/accounts/admin-accounts"; +import { redirect } from "next/navigation"; export default function Page() { - return ; + redirect("/admin/statistics"); } diff --git a/frontend/app/(admin)/admin/statistics/page.tsx b/frontend/app/(admin)/admin/statistics/page.tsx new file mode 100644 index 00000000..5c87aa42 --- /dev/null +++ b/frontend/app/(admin)/admin/statistics/page.tsx @@ -0,0 +1,5 @@ +import { AdminStatisticsPage } from "@/features/admin/components/sections/statistics/admin-statistics"; + +export default function Page() { + return ; +} diff --git a/frontend/features/admin/api/index.ts b/frontend/features/admin/api/index.ts index c6bb0a9a..c0779c7c 100644 --- a/frontend/features/admin/api/index.ts +++ b/frontend/features/admin/api/index.ts @@ -8,3 +8,4 @@ export * from "./mcp"; export * from "./permission-groups"; export * from "./reference-data"; export * from "./settings"; +export * from "./statistics"; diff --git a/frontend/features/admin/api/statistics.ts b/frontend/features/admin/api/statistics.ts new file mode 100644 index 00000000..4550a02a --- /dev/null +++ b/frontend/features/admin/api/statistics.ts @@ -0,0 +1,79 @@ +import { authedRequest } from "@/shared/api/authed-client"; + +export type AdminUsageStatisticsRankBy = "cost" | "tokens" | "calls"; +export type AdminUsageStatisticsBillingScope = "all" | "free" | "billable"; + +export type AdminUsageStatisticsMetricsDTO = { + recordCount: number; + inputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + outputTokens: number; + reasoningTokens: number; + totalTokens: number; + callCount: number; + avgLatencyMS: number; + billedNanousd: number; + billedUSD: number; +}; + +export type AdminUsageStatisticsTrendDTO = AdminUsageStatisticsMetricsDTO & { + periodStart: string; +}; + +export type AdminUsageStatisticsModelRankDTO = AdminUsageStatisticsMetricsDTO & { + platformModelName: string; + trend: AdminUsageStatisticsTrendDTO[]; +}; + +export type AdminUsageStatisticsUserRankDTO = AdminUsageStatisticsMetricsDTO & { + userID: number; + username: string; + userDisplayName: string; + userLabel: string; + trend: AdminUsageStatisticsTrendDTO[]; +}; + +export type AdminUsageStatisticsData = { + range: { + startDate: string; + endDate: string; + granularity: "day" | "month" | string; + }; + totals: AdminUsageStatisticsMetricsDTO; + trend: AdminUsageStatisticsTrendDTO[]; + topModels: AdminUsageStatisticsModelRankDTO[]; + topUsers: AdminUsageStatisticsUserRankDTO[]; +}; + +export type GetAdminUsageStatisticsOptions = { + startDate: string; + endDate: string; + userID?: number; + platformModelName?: string; + billingScope?: AdminUsageStatisticsBillingScope; + rankBy?: AdminUsageStatisticsRankBy; +}; + +export async function getAdminUsageStatistics( + accessToken: string, + options: GetAdminUsageStatisticsOptions, +): Promise { + const params = new URLSearchParams({ + start_date: options.startDate, + end_date: options.endDate, + billing_scope: options.billingScope ?? "all", + rank_by: options.rankBy ?? "cost", + }); + if (options.userID && options.userID > 0) { + params.set("user_id", String(options.userID)); + } + if (options.platformModelName?.trim()) { + params.set("platform_model_name", options.platformModelName.trim()); + } + return authedRequest( + `/api/v1/admin/usage-statistics?${params.toString()}`, + { accessToken }, + true, + ); +} diff --git a/frontend/features/admin/components/admin-date-range-filter.tsx b/frontend/features/admin/components/admin-date-range-filter.tsx index 7a91576c..d600a94b 100644 --- a/frontend/features/admin/components/admin-date-range-filter.tsx +++ b/frontend/features/admin/components/admin-date-range-filter.tsx @@ -21,6 +21,7 @@ type AdminDateRangeFilterProps = { onToChange: (value: string) => void; disabled?: boolean; placeholder?: string; + triggerClassName?: string; }; function parseDateValue(value: string): Date | undefined { @@ -43,6 +44,7 @@ export function AdminDateRangeFilter({ onToChange, disabled = false, placeholder, + triggerClassName, }: AdminDateRangeFilterProps) { const locale = useLocale(); const t = useTranslations("common.dateRange"); @@ -60,7 +62,7 @@ export function AdminDateRangeFilter({ : (placeholder ?? t("placeholder")); return ( -
+
+ + +
+ setQuery(event.target.value)} + placeholder={t("searchPlaceholder")} + className="h-8 pr-8 text-xs" + autoComplete="off" + autoCapitalize="none" + spellCheck={false} + data-1p-ignore="true" + data-form-type="other" + autoFocus + /> + {loading ? : null} +
+
+ + {users.map((user) => ( + + ))} + {!loading && users.length === 0 ? ( +
{t("empty")}
+ ) : null} +
+
+
+ ); +} diff --git a/frontend/features/admin/components/sections/statistics/admin-statistics.tsx b/frontend/features/admin/components/sections/statistics/admin-statistics.tsx new file mode 100644 index 00000000..27107163 --- /dev/null +++ b/frontend/features/admin/components/sections/statistics/admin-statistics.tsx @@ -0,0 +1,314 @@ +"use client"; + +import * as React from "react"; +import { Activity, BadgeDollarSign, Braces, RefreshCw, Timer } from "lucide-react"; +import { useLocale, useTranslations } from "next-intl"; + +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { AdminDateRangeFilter } from "@/features/admin/components/admin-date-range-filter"; +import type { + AdminUsageStatisticsBillingScope, + AdminUsageStatisticsMetricsDTO, + AdminUsageStatisticsRankBy, +} from "@/features/admin/api"; +import { useAdminStatistics, type AdminStatisticsRangePreset } from "@/features/admin/hooks/use-admin-statistics"; +import { cn } from "@/lib/utils"; +import { ModelSelect } from "@/shared/components/model-select"; +import type { UserDTO } from "@/shared/api/auth.types"; +import { AdminStatisticsUserFilter } from "./admin-statistics-user-filter"; +import { + formatStatisticsCost, + formatStatisticsCount, + formatStatisticsLatency, + StatisticsModelRankingChart, + StatisticsTrendChart, + StatisticsUserRankingChart, +} from "./admin-statistics-charts"; + +const ALL_MODELS_VALUE = "__all_models__"; + +function FilterField({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) { + return ( +
+ {label} + {children} +
+ ); +} + +function MetricCard({ + label, + value, + icon, + loading, +}: { + label: string; + value: string; + icon: React.ReactNode; + loading: boolean; +}) { + return ( +
+
+ {label} + {icon} +
+ {loading ? ( + + ) : ( +

{value}

+ )} +
+ ); +} + +function MetricSwitch({ + value, + onChange, + disabled, +}: { + value: AdminUsageStatisticsRankBy; + onChange: (value: AdminUsageStatisticsRankBy) => void; + disabled: boolean; +}) { + const t = useTranslations("adminStatistics"); + const options: AdminUsageStatisticsRankBy[] = ["cost", "tokens", "calls"]; + return ( +
+ {options.map((option) => ( + + ))} +
+ ); +} + +function emptyMetrics(): AdminUsageStatisticsMetricsDTO { + return { + recordCount: 0, + inputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + outputTokens: 0, + reasoningTokens: 0, + totalTokens: 0, + callCount: 0, + avgLatencyMS: 0, + billedNanousd: 0, + billedUSD: 0, + }; +} + +export function AdminStatisticsPage() { + const t = useTranslations("adminStatistics"); + const locale = useLocale(); + const statistics = useAdminStatistics(); + const [selectedUser, setSelectedUser] = React.useState(null); + const totals = statistics.statistics?.totals ?? emptyMetrics(); + const trendItems = statistics.statistics?.trend ?? []; + const granularity = statistics.statistics?.range.granularity ?? "day"; + const initialLoading = statistics.loading && !statistics.statistics; + const modelOptions = React.useMemo( + () => [{ label: t("filters.allModels"), value: ALL_MODELS_VALUE }, ...statistics.modelOptions], + [statistics.modelOptions, t], + ); + const rangeErrorText = statistics.rangeError ? t(`filters.rangeErrors.${statistics.rangeError}`) : ""; + + return ( +
+
+
+

{t("title")}

+

{t("description")}

+
+ +
+ +
+
+ + + + + + + + { + setSelectedUser(user); + statistics.setUserID(user?.id); + }} + /> + + + statistics.setPlatformModelName(value === ALL_MODELS_VALUE ? "" : value)} + /> + + + + +
+ {rangeErrorText ?

{rangeErrorText}

: null} +
+ +
+ } + loading={initialLoading} + /> + } + loading={initialLoading} + /> + } + loading={initialLoading} + /> + } + loading={initialLoading} + /> +
+ + + +
+
+
+

{t("trend.title")}

+

+ {statistics.statistics?.range.granularity === "month" ? t("trend.monthly") : t("trend.daily")} +

+
+ +
+
+ +
+
+ + + +
+
+
+

{t("rankings.models")}

+

{t("rankings.top10", { metric: t(`rankBy.${statistics.rankBy}`) })}

+
+ +
+
+
+

{t("rankings.users")}

+

{t("rankings.top10", { metric: t(`rankBy.${statistics.rankBy}`) })}

+
+ +
+
+
+ ); +} diff --git a/frontend/features/admin/hooks/use-admin-statistics.ts b/frontend/features/admin/hooks/use-admin-statistics.ts new file mode 100644 index 00000000..8c5c3387 --- /dev/null +++ b/frontend/features/admin/hooks/use-admin-statistics.ts @@ -0,0 +1,220 @@ +import * as React from "react"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; + +import { + getAdminBillingConfig, + getAdminUsageStatistics, + listAdminLLMModels, + type AdminUsageStatisticsBillingScope, + type AdminUsageStatisticsData, + type AdminUsageStatisticsRankBy, +} from "@/features/admin/api"; +import { listAllAdminPages } from "@/features/admin/api/shared"; +import { resolveAdminErrorMessage } from "@/features/admin/utils/admin-error"; +import type { ModelSelectOption } from "@/shared/components/model-select"; +import { resolveAccessToken } from "@/shared/auth/resolve-access-token"; +import { + normalizeBillingDisplayCurrency, + type BillingDisplayOptions, +} from "@/shared/lib/billing-display"; +import { resolveModelOptionIconUrl } from "@/shared/lib/model-option-display"; + +export type AdminStatisticsRangePreset = "7" | "30" | "90" | "custom"; +export type AdminStatisticsRangeError = "incomplete" | "invalid" | "tooLong" | null; + +function formatLocalDate(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function recentDateRange(days: number): { startDate: string; endDate: string } { + const end = new Date(); + const start = new Date(end); + start.setDate(start.getDate() - Math.max(0, days - 1)); + return { startDate: formatLocalDate(start), endDate: formatLocalDate(end) }; +} + +function parseDateValue(value: string): number | null { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim()); + if (!match) return null; + const year = Number(match[1]); + const month = Number(match[2]); + const day = Number(match[3]); + const timestamp = Date.UTC(year, month - 1, day); + const date = new Date(timestamp); + if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) { + return null; + } + return timestamp; +} + +function validateDateRange(startDate: string, endDate: string): AdminStatisticsRangeError { + if (!startDate.trim() || !endDate.trim()) return "incomplete"; + const start = parseDateValue(startDate); + const end = parseDateValue(endDate); + if (start === null || end === null || end < start) return "invalid"; + const days = Math.floor((end - start) / 86_400_000) + 1; + return days > 366 ? "tooLong" : null; +} + +export function useAdminStatistics() { + const t = useTranslations("adminStatistics"); + const initialRangeRef = React.useRef(recentDateRange(30)); + const [startDate, setStartDateState] = React.useState(initialRangeRef.current.startDate); + const [endDate, setEndDateState] = React.useState(initialRangeRef.current.endDate); + const [rangePreset, setRangePresetState] = React.useState("30"); + const [userID, setUserID] = React.useState(); + const [platformModelName, setPlatformModelName] = React.useState(""); + const [billingScope, setBillingScope] = React.useState("all"); + const [rankBy, setRankBy] = React.useState("cost"); + const [statistics, setStatistics] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [reloadKey, setReloadKey] = React.useState(0); + const [modelOptions, setModelOptions] = React.useState([]); + const [referenceLoading, setReferenceLoading] = React.useState(true); + const [billingDisplay, setBillingDisplay] = React.useState({ + currency: "USD", + usdToCnyRate: null, + }); + const requestSequenceRef = React.useRef(0); + const rangeError = React.useMemo( + () => validateDateRange(startDate, endDate), + [endDate, startDate], + ); + + React.useEffect(() => { + let cancelled = false; + setReferenceLoading(true); + void (async () => { + try { + const token = await resolveAccessToken(); + if (!token) return; + const [configResult, modelsResult] = await Promise.allSettled([ + getAdminBillingConfig(token), + listAllAdminPages((options) => listAdminLLMModels(token, { ...options, onlyActive: false })), + ]); + if (cancelled) return; + if (configResult.status === "fulfilled") { + setBillingDisplay({ + currency: normalizeBillingDisplayCurrency(configResult.value.config.displayCurrency), + usdToCnyRate: configResult.value.config.usdToCNYRate ?? null, + }); + } + if (modelsResult.status === "fulfilled") { + const nextModelOptions = modelsResult.value + .map((model) => ({ + label: model.platformModelName.trim(), + value: model.platformModelName.trim(), + iconUrl: resolveModelOptionIconUrl({ + platformModelName: model.platformModelName, + vendor: model.vendor ?? "", + icon: model.icon ?? "", + }), + })) + .filter((model) => model.value); + setModelOptions( + [...new Map(nextModelOptions.map((model) => [model.value, model])).values()].sort((left, right) => + left.label.localeCompare(right.label), + ), + ); + } + const rejectedResult = [configResult, modelsResult].find((result) => result.status === "rejected"); + if (rejectedResult?.status === "rejected") { + toast.error(t("toasts.referenceLoadFailed"), { description: resolveAdminErrorMessage(rejectedResult.reason) }); + } + } catch (error) { + if (!cancelled) { + toast.error(t("toasts.referenceLoadFailed"), { description: resolveAdminErrorMessage(error) }); + } + } finally { + if (!cancelled) setReferenceLoading(false); + } + })(); + return () => { + cancelled = true; + }; + }, [t]); + + React.useEffect(() => { + if (rangeError) { + requestSequenceRef.current += 1; + setLoading(false); + return; + } + const requestSequence = requestSequenceRef.current + 1; + requestSequenceRef.current = requestSequence; + setLoading(true); + void (async () => { + try { + const token = await resolveAccessToken(); + if (!token) { + toast.error(t("toasts.sessionExpired"), { description: t("toasts.signInAgain") }); + return; + } + const data = await getAdminUsageStatistics(token, { + startDate, + endDate, + userID, + platformModelName, + billingScope, + rankBy, + }); + if (requestSequence === requestSequenceRef.current) { + setStatistics(data); + } + } catch (error) { + if (requestSequence === requestSequenceRef.current) { + toast.error(t("toasts.loadFailed"), { description: resolveAdminErrorMessage(error) }); + } + } finally { + if (requestSequence === requestSequenceRef.current) { + setLoading(false); + } + } + })(); + }, [billingScope, endDate, platformModelName, rangeError, rankBy, reloadKey, startDate, t, userID]); + + const setRangePreset = React.useCallback((preset: AdminStatisticsRangePreset) => { + setRangePresetState(preset); + if (preset === "custom") return; + const range = recentDateRange(Number(preset)); + setStartDateState(range.startDate); + setEndDateState(range.endDate); + }, []); + const setStartDate = React.useCallback((value: string) => { + setRangePresetState("custom"); + setStartDateState(value); + }, []); + const setEndDate = React.useCallback((value: string) => { + setRangePresetState("custom"); + setEndDateState(value); + }, []); + const refresh = React.useCallback(() => setReloadKey((current) => current + 1), []); + + return { + statistics, + loading, + referenceLoading, + billingDisplay, + modelOptions, + startDate, + endDate, + rangePreset, + rangeError, + userID, + platformModelName, + billingScope, + rankBy, + setStartDate, + setEndDate, + setRangePreset, + setUserID, + setPlatformModelName, + setBillingScope, + setRankBy, + refresh, + }; +} diff --git a/frontend/features/admin/model/admin-sections.ts b/frontend/features/admin/model/admin-sections.ts index f1d80afb..dd8a1457 100644 --- a/frontend/features/admin/model/admin-sections.ts +++ b/frontend/features/admin/model/admin-sections.ts @@ -1,4 +1,5 @@ export const ADMIN_SECTIONS = [ + { id: "statistics", label: "Statistics", href: "/statistics" }, { id: "accounts", label: "Accounts", href: "/users" }, { id: "groups", label: "Permission Groups", href: "/groups" }, { id: "upstreams", label: "Upstreams", href: "/upstreams" }, @@ -19,5 +20,5 @@ export function resolveAdminSection(section?: string | null): AdminSection { if (ADMIN_SECTIONS.some((item) => item.id === section)) { return section as AdminSection; } - return "accounts"; + return "statistics"; } diff --git a/frontend/i18n/messages.ts b/frontend/i18n/messages.ts index 4aefbaec..a53c2ec0 100644 --- a/frontend/i18n/messages.ts +++ b/frontend/i18n/messages.ts @@ -7,6 +7,7 @@ import enAdminLogin from "@/i18n/messages/en-US/admin-login.json"; import enAdminLogs from "@/i18n/messages/en-US/admin-logs.json"; import enAdminModels from "@/i18n/messages/en-US/admin-models.json"; import enAdminPrompts from "@/i18n/messages/en-US/admin-prompts.json"; +import enAdminStatistics from "@/i18n/messages/en-US/admin-statistics.json"; import enAdminTools from "@/i18n/messages/en-US/admin-tools.json"; import enAdminUpstreams from "@/i18n/messages/en-US/admin-upstreams.json"; import enAdminUsers from "@/i18n/messages/en-US/admin-users.json"; @@ -47,6 +48,7 @@ const ENGLISH_MESSAGES = { adminLogs: enAdminLogs, adminModels: enAdminModels, adminPrompts: enAdminPrompts, + adminStatistics: enAdminStatistics, adminTools: enAdminTools, adminUpstreams: enAdminUpstreams, adminUsers: enAdminUsers, @@ -127,6 +129,7 @@ export async function loadLocaleMessages(locale: AppLocale): Promise Date: Thu, 16 Jul 2026 01:27:13 +0800 Subject: [PATCH 2/2] feat: update API documentation and improve usage statistics filtering --- backend/docs/docs.go | 513 ++++++++--------- backend/docs/swagger.json | 515 +++++++++--------- backend/docs/swagger.yaml | 486 +++++++++-------- .../application/admin/service_logs.go | 11 + .../application/admin/service_test.go | 54 ++ backend/internal/application/billing/errs.go | 2 + .../internal/application/billing/service.go | 43 +- .../billing/service_statistics_test.go | 55 ++ .../postgres/billing/repository.go | 262 +++++---- .../billing/repository_sqlite_test.go | 146 +++++ backend/internal/repository/billing.go | 6 +- backend/internal/transport/http/admin/dto.go | 5 +- .../internal/transport/http/admin/handler.go | 62 ++- .../http/admin/handler_security_test.go | 90 +++ frontend/components/ui/chart.tsx | 46 ++ frontend/features/admin/api/statistics.ts | 21 +- .../components/admin-date-range-filter.tsx | 13 +- .../admin-statistics-billing-filter.tsx | 64 +++ .../statistics/admin-statistics-charts.tsx | 313 ++++++++--- .../admin-statistics-model-filter.tsx | 90 +++ .../admin-statistics-subject-filter.tsx | 284 ++++++++++ .../admin-statistics-user-filter.tsx | 149 ----- .../sections/statistics/admin-statistics.tsx | 477 +++++++++------- .../admin/hooks/use-admin-statistics.ts | 134 ++++- .../subscription/subscription-trend.tsx | 401 ++++++++++---- .../i18n/messages/en-US/admin-statistics.json | 22 +- frontend/i18n/messages/en-US/settings.json | 4 +- .../i18n/messages/zh-CN/admin-statistics.json | 24 +- frontend/i18n/messages/zh-CN/settings.json | 4 +- 29 files changed, 2856 insertions(+), 1440 deletions(-) create mode 100644 backend/internal/application/billing/service_statistics_test.go create mode 100644 frontend/features/admin/components/sections/statistics/admin-statistics-billing-filter.tsx create mode 100644 frontend/features/admin/components/sections/statistics/admin-statistics-model-filter.tsx create mode 100644 frontend/features/admin/components/sections/statistics/admin-statistics-subject-filter.tsx delete mode 100644 frontend/features/admin/components/sections/statistics/admin-statistics-user-filter.tsx diff --git a/backend/docs/docs.go b/backend/docs/docs.go index c4cb7664..3c41d2be 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -331,13 +331,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.AuditLogListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.AuditLogListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -1103,19 +1103,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UsageLogListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UsageLogListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -1211,19 +1211,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ConversationEventListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ConversationEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -1254,7 +1254,7 @@ const docTemplate = `{ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3270,7 +3270,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.CleanupLogsRequest" + "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsRequest" } } ], @@ -3278,19 +3278,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.CleanupLogsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3327,25 +3327,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ModelPermissionGroupsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3381,7 +3381,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.SetModelPermissionGroupsRequest" + "$ref": "#/definitions/internal_transport_http_admin.SetModelPermissionGroupsRequest" } } ], @@ -3389,25 +3389,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ModelPermissionGroupsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3497,19 +3497,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.PaymentOrderListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.PaymentOrderListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3537,13 +3537,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.PermissionGroupListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3572,7 +3572,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.CreatePermissionGroupRequest" + "$ref": "#/definitions/internal_transport_http_admin.CreatePermissionGroupRequest" } } ], @@ -3580,19 +3580,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.PermissionGroupDataResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3629,31 +3629,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.DeletePermissionGroupResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.DeletePermissionGroupResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3689,7 +3689,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.UpdatePermissionGroupRequest" + "$ref": "#/definitions/internal_transport_http_admin.UpdatePermissionGroupRequest" } } ], @@ -3697,25 +3697,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.PermissionGroupDataResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3752,25 +3752,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.GroupModelsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3806,7 +3806,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.SetGroupModelsRequest" + "$ref": "#/definitions/internal_transport_http_admin.SetGroupModelsRequest" } } ], @@ -3814,25 +3814,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.GroupModelsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3869,31 +3869,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.GroupUsersResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3929,7 +3929,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.SetGroupUsersRequest" + "$ref": "#/definitions/internal_transport_http_admin.SetGroupUsersRequest" } } ], @@ -3937,31 +3937,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.GroupUsersResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -4945,19 +4945,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.SystemEventListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.SystemEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -4970,7 +4970,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "管理员按日期、用户、平台模型和计费范围查看全局费用、Token、调用次数及排名", + "description": "管理员按日期、统计对象、平台模型和计费范围查看全局费用、Token、调用次数及排名;用户与权限组筛选互斥", "consumes": [ "application/json" ], @@ -5000,6 +5000,12 @@ const docTemplate = `{ "name": "user_id", "in": "query" }, + { + "type": "integer", + "description": "权限组ID,与 user_id 互斥", + "name": "permission_group_id", + "in": "query" + }, { "type": "string", "description": "平台模型名", @@ -5014,8 +5020,20 @@ const docTemplate = `{ }, { "type": "string", - "description": "排名指标:cost/tokens/calls", - "name": "rank_by", + "description": "返回范围:all/models/users", + "name": "section", + "in": "query" + }, + { + "type": "string", + "description": "模型排名指标:cost/tokens/calls", + "name": "model_rank_by", + "in": "query" + }, + { + "type": "string", + "description": "用户排名指标:cost/tokens/calls", + "name": "user_rank_by", "in": "query" } ], @@ -5023,19 +5041,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UsageStatisticsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5095,19 +5119,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UserAuthEventListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UserAuthEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5167,13 +5191,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UserListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UserListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5202,7 +5226,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.CreateUserRequest" + "$ref": "#/definitions/internal_transport_http_admin.CreateUserRequest" } } ], @@ -5210,25 +5234,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.CreateUserResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.CreateUserResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5259,7 +5283,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.ImportOpenWebUIUsersRequest" + "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersRequest" } } ], @@ -5267,25 +5291,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ImportOpenWebUIUsersResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5322,31 +5346,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.DeleteUserResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.DeleteUserResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5382,7 +5406,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.PatchUserRequest" + "$ref": "#/definitions/internal_transport_http_admin.PatchUserRequest" } } ], @@ -5390,31 +5414,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UpdateUserStatusResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5452,7 +5476,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.ResetUserPasswordRequest" + "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordRequest" } } ], @@ -5460,31 +5484,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ResetUserPasswordResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5521,25 +5545,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.RevokeUserSessionsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.RevokeUserSessionsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5577,7 +5601,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.UpdateUserStatusRequest" + "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusRequest" } } ], @@ -5585,31 +5609,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UpdateUserStatusResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -10320,7 +10344,42 @@ const docTemplate = `{ } }, "definitions": { - "admin.AuditLogListResponseDoc": { + "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope": { + "type": "object", + "properties": { + "data": {}, + "details": {}, + "errorCode": { + "type": "string" + }, + "errorMsg": { + "type": "string" + }, + "requestId": { + "type": "string" + } + } + }, + "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc": { + "type": "object", + "properties": { + "data": {}, + "details": {}, + "errorCode": { + "type": "string", + "example": "" + }, + "errorMsg": { + "type": "string", + "example": "" + }, + "requestId": { + "type": "string", + "example": "" + } + } + }, + "internal_transport_http_admin.AuditLogListResponseDoc": { "type": "object", "properties": { "data": { @@ -10329,7 +10388,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.AuditLogResponse" + "$ref": "#/definitions/internal_transport_http_admin.AuditLogResponse" } }, "total": { @@ -10342,7 +10401,7 @@ const docTemplate = `{ } } }, - "admin.AuditLogResponse": { + "internal_transport_http_admin.AuditLogResponse": { "type": "object", "properties": { "action": { @@ -10389,7 +10448,7 @@ const docTemplate = `{ } } }, - "admin.AuthEventResponse": { + "internal_transport_http_admin.AuthEventResponse": { "type": "object", "properties": { "clientIP": { @@ -10439,7 +10498,7 @@ const docTemplate = `{ } } }, - "admin.CleanupLogsRequest": { + "internal_transport_http_admin.CleanupLogsRequest": { "type": "object", "required": [ "before", @@ -10454,7 +10513,7 @@ const docTemplate = `{ } } }, - "admin.CleanupLogsResponse": { + "internal_transport_http_admin.CleanupLogsResponse": { "type": "object", "properties": { "before": { @@ -10468,18 +10527,18 @@ const docTemplate = `{ } } }, - "admin.CleanupLogsResponseDoc": { + "internal_transport_http_admin.CleanupLogsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.CleanupLogsResponse" + "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.ConversationEventListResponseDoc": { + "internal_transport_http_admin.ConversationEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -10488,7 +10547,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.ConversationEventResponse" + "$ref": "#/definitions/internal_transport_http_admin.ConversationEventResponse" } }, "total": { @@ -10501,7 +10560,7 @@ const docTemplate = `{ } } }, - "admin.ConversationEventResponse": { + "internal_transport_http_admin.ConversationEventResponse": { "type": "object", "properties": { "contentMarkdown": { @@ -10614,7 +10673,7 @@ const docTemplate = `{ } } }, - "admin.CreatePermissionGroupRequest": { + "internal_transport_http_admin.CreatePermissionGroupRequest": { "type": "object", "required": [ "name" @@ -10635,7 +10694,7 @@ const docTemplate = `{ } } }, - "admin.CreateUserRequest": { + "internal_transport_http_admin.CreateUserRequest": { "type": "object", "required": [ "password", @@ -10686,40 +10745,40 @@ const docTemplate = `{ } } }, - "admin.CreateUserResponseDoc": { + "internal_transport_http_admin.CreateUserResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.UserDataResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserDataResponse" }, "errorMsg": { "type": "string" } } }, - "admin.DeletePermissionGroupResponse": { + "internal_transport_http_admin.DeletePermissionGroupResponse": { "type": "object", "properties": { "deleted": { "type": "boolean" }, "summary": { - "$ref": "#/definitions/admin.PermissionGroupDeleteSummaryResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDeleteSummaryResponse" } } }, - "admin.DeletePermissionGroupResponseDoc": { + "internal_transport_http_admin.DeletePermissionGroupResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.DeletePermissionGroupResponse" + "$ref": "#/definitions/internal_transport_http_admin.DeletePermissionGroupResponse" }, "errorMsg": { "type": "string" } } }, - "admin.DeleteUserResponse": { + "internal_transport_http_admin.DeleteUserResponse": { "type": "object", "properties": { "deleted": { @@ -10727,18 +10786,18 @@ const docTemplate = `{ } } }, - "admin.DeleteUserResponseDoc": { + "internal_transport_http_admin.DeleteUserResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.DeleteUserResponse" + "$ref": "#/definitions/internal_transport_http_admin.DeleteUserResponse" }, "errorMsg": { "type": "string" } } }, - "admin.ErrorDoc": { + "internal_transport_http_admin.ErrorDoc": { "type": "object", "properties": { "data": {}, @@ -10754,7 +10813,7 @@ const docTemplate = `{ } } }, - "admin.GroupModelsResponse": { + "internal_transport_http_admin.GroupModelsResponse": { "type": "object", "properties": { "modelIDs": { @@ -10766,23 +10825,23 @@ const docTemplate = `{ "rules": { "type": "array", "items": { - "$ref": "#/definitions/admin.PermissionGroupModelRuleResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupModelRuleResponse" } } } }, - "admin.GroupModelsResponseDoc": { + "internal_transport_http_admin.GroupModelsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.GroupModelsResponse" + "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.GroupUsersResponse": { + "internal_transport_http_admin.GroupUsersResponse": { "type": "object", "properties": { "userIDs": { @@ -10793,18 +10852,18 @@ const docTemplate = `{ } } }, - "admin.GroupUsersResponseDoc": { + "internal_transport_http_admin.GroupUsersResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.GroupUsersResponse" + "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponse" }, "errorMsg": { "type": "string" } } }, - "admin.ImportOpenWebUIUsersRequest": { + "internal_transport_http_admin.ImportOpenWebUIUsersRequest": { "type": "object", "required": [ "creditMultiplier", @@ -10823,7 +10882,7 @@ const docTemplate = `{ } } }, - "admin.ImportOpenWebUIUsersResponse": { + "internal_transport_http_admin.ImportOpenWebUIUsersResponse": { "type": "object", "properties": { "dedupeField": { @@ -10855,18 +10914,18 @@ const docTemplate = `{ } } }, - "admin.ImportOpenWebUIUsersResponseDoc": { + "internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.ImportOpenWebUIUsersResponse" + "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponse" }, "errorMsg": { "type": "string" } } }, - "admin.ModelPermissionGroupsResponse": { + "internal_transport_http_admin.ModelPermissionGroupsResponse": { "type": "object", "properties": { "effectiveGroupIDs": { @@ -10892,18 +10951,18 @@ const docTemplate = `{ } } }, - "admin.ModelPermissionGroupsResponseDoc": { + "internal_transport_http_admin.ModelPermissionGroupsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.ModelPermissionGroupsResponse" + "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.PatchUserRequest": { + "internal_transport_http_admin.PatchUserRequest": { "type": "object", "properties": { "avatarURL": { @@ -10956,7 +11015,7 @@ const docTemplate = `{ } } }, - "admin.PaymentOrderListResponseDoc": { + "internal_transport_http_admin.PaymentOrderListResponseDoc": { "type": "object", "properties": { "data": { @@ -10965,7 +11024,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.PaymentOrderResponse" + "$ref": "#/definitions/internal_transport_http_admin.PaymentOrderResponse" } }, "total": { @@ -10978,7 +11037,7 @@ const docTemplate = `{ } } }, - "admin.PaymentOrderResponse": { + "internal_transport_http_admin.PaymentOrderResponse": { "type": "object", "properties": { "baseAmountCents": { @@ -11064,26 +11123,26 @@ const docTemplate = `{ } } }, - "admin.PermissionGroupDataResponse": { + "internal_transport_http_admin.PermissionGroupDataResponse": { "type": "object", "properties": { "group": { - "$ref": "#/definitions/admin.PermissionGroupResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupResponse" } } }, - "admin.PermissionGroupDataResponseDoc": { + "internal_transport_http_admin.PermissionGroupDataResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.PermissionGroupDataResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponse" }, "errorMsg": { "type": "string" } } }, - "admin.PermissionGroupDeleteSummaryResponse": { + "internal_transport_http_admin.PermissionGroupDeleteSummaryResponse": { "type": "object", "properties": { "manualModelCount": { @@ -11100,29 +11159,29 @@ const docTemplate = `{ } } }, - "admin.PermissionGroupListResponse": { + "internal_transport_http_admin.PermissionGroupListResponse": { "type": "object", "properties": { "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.PermissionGroupResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupResponse" } } } }, - "admin.PermissionGroupListResponseDoc": { + "internal_transport_http_admin.PermissionGroupListResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.PermissionGroupListResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupListResponse" }, "errorMsg": { "type": "string" } } }, - "admin.PermissionGroupModelRuleRequest": { + "internal_transport_http_admin.PermissionGroupModelRuleRequest": { "type": "object", "required": [ "type" @@ -11138,7 +11197,7 @@ const docTemplate = `{ } } }, - "admin.PermissionGroupModelRuleResponse": { + "internal_transport_http_admin.PermissionGroupModelRuleResponse": { "type": "object", "properties": { "type": { @@ -11149,7 +11208,7 @@ const docTemplate = `{ } } }, - "admin.PermissionGroupResponse": { + "internal_transport_http_admin.PermissionGroupResponse": { "type": "object", "properties": { "createdAt": { @@ -11193,7 +11252,7 @@ const docTemplate = `{ } } }, - "admin.ResetUserPasswordRequest": { + "internal_transport_http_admin.ResetUserPasswordRequest": { "type": "object", "required": [ "newPassword" @@ -11209,7 +11268,7 @@ const docTemplate = `{ } } }, - "admin.ResetUserPasswordResponse": { + "internal_transport_http_admin.ResetUserPasswordResponse": { "type": "object", "properties": { "reset": { @@ -11217,18 +11276,18 @@ const docTemplate = `{ } } }, - "admin.ResetUserPasswordResponseDoc": { + "internal_transport_http_admin.ResetUserPasswordResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.ResetUserPasswordResponse" + "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordResponse" }, "errorMsg": { "type": "string" } } }, - "admin.RevokeUserSessionsResponse": { + "internal_transport_http_admin.RevokeUserSessionsResponse": { "type": "object", "properties": { "revoked": { @@ -11236,18 +11295,18 @@ const docTemplate = `{ } } }, - "admin.RevokeUserSessionsResponseDoc": { + "internal_transport_http_admin.RevokeUserSessionsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.RevokeUserSessionsResponse" + "$ref": "#/definitions/internal_transport_http_admin.RevokeUserSessionsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.SetGroupModelsRequest": { + "internal_transport_http_admin.SetGroupModelsRequest": { "type": "object", "properties": { "modelIDs": { @@ -11259,12 +11318,12 @@ const docTemplate = `{ "rules": { "type": "array", "items": { - "$ref": "#/definitions/admin.PermissionGroupModelRuleRequest" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupModelRuleRequest" } } } }, - "admin.SetGroupUsersRequest": { + "internal_transport_http_admin.SetGroupUsersRequest": { "type": "object", "properties": { "userIDs": { @@ -11275,7 +11334,7 @@ const docTemplate = `{ } } }, - "admin.SetModelPermissionGroupsRequest": { + "internal_transport_http_admin.SetModelPermissionGroupsRequest": { "type": "object", "properties": { "groupIDs": { @@ -11286,7 +11345,7 @@ const docTemplate = `{ } } }, - "admin.SystemEventListResponseDoc": { + "internal_transport_http_admin.SystemEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -11295,7 +11354,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.SystemEventResponse" + "$ref": "#/definitions/internal_transport_http_admin.SystemEventResponse" } }, "total": { @@ -11308,7 +11367,7 @@ const docTemplate = `{ } } }, - "admin.SystemEventResponse": { + "internal_transport_http_admin.SystemEventResponse": { "type": "object", "properties": { "createdAt": { @@ -11349,7 +11408,7 @@ const docTemplate = `{ } } }, - "admin.UpdatePermissionGroupRequest": { + "internal_transport_http_admin.UpdatePermissionGroupRequest": { "type": "object", "required": [ "name" @@ -11370,7 +11429,7 @@ const docTemplate = `{ } } }, - "admin.UpdateUserStatusRequest": { + "internal_transport_http_admin.UpdateUserStatusRequest": { "type": "object", "required": [ "status" @@ -11386,18 +11445,18 @@ const docTemplate = `{ } } }, - "admin.UpdateUserStatusResponseDoc": { + "internal_transport_http_admin.UpdateUserStatusResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.UserDataResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserDataResponse" }, "errorMsg": { "type": "string" } } }, - "admin.UsageLogListResponseDoc": { + "internal_transport_http_admin.UsageLogListResponseDoc": { "type": "object", "properties": { "data": { @@ -11406,7 +11465,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageLogResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageLogResponse" } }, "total": { @@ -11419,7 +11478,7 @@ const docTemplate = `{ } } }, - "admin.UsageLogResponse": { + "internal_transport_http_admin.UsageLogResponse": { "type": "object", "properties": { "billedCurrency": { @@ -11520,7 +11579,7 @@ const docTemplate = `{ } } }, - "admin.UsageStatisticsMetricsResponse": { + "internal_transport_http_admin.UsageStatisticsMetricsResponse": { "type": "object", "properties": { "avgLatencyMS": { @@ -11558,7 +11617,7 @@ const docTemplate = `{ } } }, - "admin.UsageStatisticsModelRankResponse": { + "internal_transport_http_admin.UsageStatisticsModelRankResponse": { "type": "object", "properties": { "avgLatencyMS": { @@ -11600,12 +11659,12 @@ const docTemplate = `{ "trend": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse" } } } }, - "admin.UsageStatisticsResponse": { + "internal_transport_http_admin.UsageStatisticsResponse": { "type": "object", "properties": { "range": { @@ -11622,41 +11681,44 @@ const docTemplate = `{ } } }, + "section": { + "type": "string" + }, "topModels": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsModelRankResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsModelRankResponse" } }, "topUsers": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsUserRankResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsUserRankResponse" } }, "totals": { - "$ref": "#/definitions/admin.UsageStatisticsMetricsResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsMetricsResponse" }, "trend": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse" } } } }, - "admin.UsageStatisticsResponseDoc": { + "internal_transport_http_admin.UsageStatisticsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.UsageStatisticsResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.UsageStatisticsTrendResponse": { + "internal_transport_http_admin.UsageStatisticsTrendResponse": { "type": "object", "properties": { "avgLatencyMS": { @@ -11697,7 +11759,7 @@ const docTemplate = `{ } } }, - "admin.UsageStatisticsUserRankResponse": { + "internal_transport_http_admin.UsageStatisticsUserRankResponse": { "type": "object", "properties": { "avgLatencyMS": { @@ -11736,7 +11798,7 @@ const docTemplate = `{ "trend": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse" } }, "userDisplayName": { @@ -11753,7 +11815,7 @@ const docTemplate = `{ } } }, - "admin.UserAuthEventListResponseDoc": { + "internal_transport_http_admin.UserAuthEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -11762,7 +11824,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.AuthEventResponse" + "$ref": "#/definitions/internal_transport_http_admin.AuthEventResponse" } }, "total": { @@ -11775,15 +11837,15 @@ const docTemplate = `{ } } }, - "admin.UserDataResponse": { + "internal_transport_http_admin.UserDataResponse": { "type": "object", "properties": { "user": { - "$ref": "#/definitions/admin.UserResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserResponse" } } }, - "admin.UserIdentityProviderSummaryResponse": { + "internal_transport_http_admin.UserIdentityProviderSummaryResponse": { "type": "object", "properties": { "id": { @@ -11803,7 +11865,7 @@ const docTemplate = `{ } } }, - "admin.UserListResponseDoc": { + "internal_transport_http_admin.UserListResponseDoc": { "type": "object", "properties": { "data": { @@ -11812,7 +11874,7 @@ const docTemplate = `{ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.UserResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserResponse" } }, "total": { @@ -11825,7 +11887,7 @@ const docTemplate = `{ } } }, - "admin.UserResponse": { + "internal_transport_http_admin.UserResponse": { "type": "object", "properties": { "appearancePreferences": { @@ -11864,7 +11926,7 @@ const docTemplate = `{ "identityProviders": { "type": "array", "items": { - "$ref": "#/definitions/admin.UserIdentityProviderSummaryResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserIdentityProviderSummaryResponse" } }, "lastActiveAt": { @@ -11932,41 +11994,6 @@ const docTemplate = `{ } } }, - "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope": { - "type": "object", - "properties": { - "data": {}, - "details": {}, - "errorCode": { - "type": "string" - }, - "errorMsg": { - "type": "string" - }, - "requestId": { - "type": "string" - } - } - }, - "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc": { - "type": "object", - "properties": { - "data": {}, - "details": {}, - "errorCode": { - "type": "string", - "example": "" - }, - "errorMsg": { - "type": "string", - "example": "" - }, - "requestId": { - "type": "string", - "example": "" - } - } - }, "internal_transport_http_announcement.AdminAnnouncementListResponseDoc": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 26fa02a5..fdb1cd0c 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -324,13 +324,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.AuditLogListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.AuditLogListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -1096,19 +1096,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UsageLogListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UsageLogListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -1204,19 +1204,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ConversationEventListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ConversationEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -1247,7 +1247,7 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3263,7 +3263,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.CleanupLogsRequest" + "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsRequest" } } ], @@ -3271,19 +3271,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.CleanupLogsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3320,25 +3320,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ModelPermissionGroupsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3374,7 +3374,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.SetModelPermissionGroupsRequest" + "$ref": "#/definitions/internal_transport_http_admin.SetModelPermissionGroupsRequest" } } ], @@ -3382,25 +3382,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ModelPermissionGroupsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3490,19 +3490,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.PaymentOrderListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.PaymentOrderListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3530,13 +3530,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.PermissionGroupListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3565,7 +3565,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.CreatePermissionGroupRequest" + "$ref": "#/definitions/internal_transport_http_admin.CreatePermissionGroupRequest" } } ], @@ -3573,19 +3573,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.PermissionGroupDataResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3622,31 +3622,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.DeletePermissionGroupResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.DeletePermissionGroupResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3682,7 +3682,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.UpdatePermissionGroupRequest" + "$ref": "#/definitions/internal_transport_http_admin.UpdatePermissionGroupRequest" } } ], @@ -3690,25 +3690,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.PermissionGroupDataResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3745,25 +3745,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.GroupModelsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3799,7 +3799,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.SetGroupModelsRequest" + "$ref": "#/definitions/internal_transport_http_admin.SetGroupModelsRequest" } } ], @@ -3807,25 +3807,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.GroupModelsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3862,31 +3862,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.GroupUsersResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -3922,7 +3922,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.SetGroupUsersRequest" + "$ref": "#/definitions/internal_transport_http_admin.SetGroupUsersRequest" } } ], @@ -3930,31 +3930,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.GroupUsersResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -4938,19 +4938,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.SystemEventListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.SystemEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -4963,7 +4963,7 @@ "BearerAuth": [] } ], - "description": "管理员按日期、用户、平台模型和计费范围查看全局费用、Token、调用次数及排名", + "description": "管理员按日期、统计对象、平台模型和计费范围查看全局费用、Token、调用次数及排名;用户与权限组筛选互斥", "consumes": [ "application/json" ], @@ -4993,6 +4993,12 @@ "name": "user_id", "in": "query" }, + { + "type": "integer", + "description": "权限组ID,与 user_id 互斥", + "name": "permission_group_id", + "in": "query" + }, { "type": "string", "description": "平台模型名", @@ -5007,8 +5013,20 @@ }, { "type": "string", - "description": "排名指标:cost/tokens/calls", - "name": "rank_by", + "description": "返回范围:all/models/users", + "name": "section", + "in": "query" + }, + { + "type": "string", + "description": "模型排名指标:cost/tokens/calls", + "name": "model_rank_by", + "in": "query" + }, + { + "type": "string", + "description": "用户排名指标:cost/tokens/calls", + "name": "user_rank_by", "in": "query" } ], @@ -5016,19 +5034,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UsageStatisticsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5088,19 +5112,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UserAuthEventListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UserAuthEventListResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5160,13 +5184,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UserListResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UserListResponseDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5195,7 +5219,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.CreateUserRequest" + "$ref": "#/definitions/internal_transport_http_admin.CreateUserRequest" } } ], @@ -5203,25 +5227,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.CreateUserResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.CreateUserResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5252,7 +5276,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.ImportOpenWebUIUsersRequest" + "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersRequest" } } ], @@ -5260,25 +5284,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ImportOpenWebUIUsersResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5315,31 +5339,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.DeleteUserResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.DeleteUserResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5375,7 +5399,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.PatchUserRequest" + "$ref": "#/definitions/internal_transport_http_admin.PatchUserRequest" } } ], @@ -5383,31 +5407,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UpdateUserStatusResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5445,7 +5469,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.ResetUserPasswordRequest" + "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordRequest" } } ], @@ -5453,31 +5477,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.ResetUserPasswordResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5514,25 +5538,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.RevokeUserSessionsResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.RevokeUserSessionsResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -5570,7 +5594,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/admin.UpdateUserStatusRequest" + "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusRequest" } } ], @@ -5578,31 +5602,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin.UpdateUserStatusResponseDoc" + "$ref": "#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/admin.ErrorDoc" + "$ref": "#/definitions/internal_transport_http_admin.ErrorDoc" } } } @@ -10313,7 +10337,42 @@ } }, "definitions": { - "admin.AuditLogListResponseDoc": { + "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope": { + "type": "object", + "properties": { + "data": {}, + "details": {}, + "errorCode": { + "type": "string" + }, + "errorMsg": { + "type": "string" + }, + "requestId": { + "type": "string" + } + } + }, + "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc": { + "type": "object", + "properties": { + "data": {}, + "details": {}, + "errorCode": { + "type": "string", + "example": "" + }, + "errorMsg": { + "type": "string", + "example": "" + }, + "requestId": { + "type": "string", + "example": "" + } + } + }, + "internal_transport_http_admin.AuditLogListResponseDoc": { "type": "object", "properties": { "data": { @@ -10322,7 +10381,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.AuditLogResponse" + "$ref": "#/definitions/internal_transport_http_admin.AuditLogResponse" } }, "total": { @@ -10335,7 +10394,7 @@ } } }, - "admin.AuditLogResponse": { + "internal_transport_http_admin.AuditLogResponse": { "type": "object", "properties": { "action": { @@ -10382,7 +10441,7 @@ } } }, - "admin.AuthEventResponse": { + "internal_transport_http_admin.AuthEventResponse": { "type": "object", "properties": { "clientIP": { @@ -10432,7 +10491,7 @@ } } }, - "admin.CleanupLogsRequest": { + "internal_transport_http_admin.CleanupLogsRequest": { "type": "object", "required": [ "before", @@ -10447,7 +10506,7 @@ } } }, - "admin.CleanupLogsResponse": { + "internal_transport_http_admin.CleanupLogsResponse": { "type": "object", "properties": { "before": { @@ -10461,18 +10520,18 @@ } } }, - "admin.CleanupLogsResponseDoc": { + "internal_transport_http_admin.CleanupLogsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.CleanupLogsResponse" + "$ref": "#/definitions/internal_transport_http_admin.CleanupLogsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.ConversationEventListResponseDoc": { + "internal_transport_http_admin.ConversationEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -10481,7 +10540,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.ConversationEventResponse" + "$ref": "#/definitions/internal_transport_http_admin.ConversationEventResponse" } }, "total": { @@ -10494,7 +10553,7 @@ } } }, - "admin.ConversationEventResponse": { + "internal_transport_http_admin.ConversationEventResponse": { "type": "object", "properties": { "contentMarkdown": { @@ -10607,7 +10666,7 @@ } } }, - "admin.CreatePermissionGroupRequest": { + "internal_transport_http_admin.CreatePermissionGroupRequest": { "type": "object", "required": [ "name" @@ -10628,7 +10687,7 @@ } } }, - "admin.CreateUserRequest": { + "internal_transport_http_admin.CreateUserRequest": { "type": "object", "required": [ "password", @@ -10679,40 +10738,40 @@ } } }, - "admin.CreateUserResponseDoc": { + "internal_transport_http_admin.CreateUserResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.UserDataResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserDataResponse" }, "errorMsg": { "type": "string" } } }, - "admin.DeletePermissionGroupResponse": { + "internal_transport_http_admin.DeletePermissionGroupResponse": { "type": "object", "properties": { "deleted": { "type": "boolean" }, "summary": { - "$ref": "#/definitions/admin.PermissionGroupDeleteSummaryResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDeleteSummaryResponse" } } }, - "admin.DeletePermissionGroupResponseDoc": { + "internal_transport_http_admin.DeletePermissionGroupResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.DeletePermissionGroupResponse" + "$ref": "#/definitions/internal_transport_http_admin.DeletePermissionGroupResponse" }, "errorMsg": { "type": "string" } } }, - "admin.DeleteUserResponse": { + "internal_transport_http_admin.DeleteUserResponse": { "type": "object", "properties": { "deleted": { @@ -10720,18 +10779,18 @@ } } }, - "admin.DeleteUserResponseDoc": { + "internal_transport_http_admin.DeleteUserResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.DeleteUserResponse" + "$ref": "#/definitions/internal_transport_http_admin.DeleteUserResponse" }, "errorMsg": { "type": "string" } } }, - "admin.ErrorDoc": { + "internal_transport_http_admin.ErrorDoc": { "type": "object", "properties": { "data": {}, @@ -10747,7 +10806,7 @@ } } }, - "admin.GroupModelsResponse": { + "internal_transport_http_admin.GroupModelsResponse": { "type": "object", "properties": { "modelIDs": { @@ -10759,23 +10818,23 @@ "rules": { "type": "array", "items": { - "$ref": "#/definitions/admin.PermissionGroupModelRuleResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupModelRuleResponse" } } } }, - "admin.GroupModelsResponseDoc": { + "internal_transport_http_admin.GroupModelsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.GroupModelsResponse" + "$ref": "#/definitions/internal_transport_http_admin.GroupModelsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.GroupUsersResponse": { + "internal_transport_http_admin.GroupUsersResponse": { "type": "object", "properties": { "userIDs": { @@ -10786,18 +10845,18 @@ } } }, - "admin.GroupUsersResponseDoc": { + "internal_transport_http_admin.GroupUsersResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.GroupUsersResponse" + "$ref": "#/definitions/internal_transport_http_admin.GroupUsersResponse" }, "errorMsg": { "type": "string" } } }, - "admin.ImportOpenWebUIUsersRequest": { + "internal_transport_http_admin.ImportOpenWebUIUsersRequest": { "type": "object", "required": [ "creditMultiplier", @@ -10816,7 +10875,7 @@ } } }, - "admin.ImportOpenWebUIUsersResponse": { + "internal_transport_http_admin.ImportOpenWebUIUsersResponse": { "type": "object", "properties": { "dedupeField": { @@ -10848,18 +10907,18 @@ } } }, - "admin.ImportOpenWebUIUsersResponseDoc": { + "internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.ImportOpenWebUIUsersResponse" + "$ref": "#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponse" }, "errorMsg": { "type": "string" } } }, - "admin.ModelPermissionGroupsResponse": { + "internal_transport_http_admin.ModelPermissionGroupsResponse": { "type": "object", "properties": { "effectiveGroupIDs": { @@ -10885,18 +10944,18 @@ } } }, - "admin.ModelPermissionGroupsResponseDoc": { + "internal_transport_http_admin.ModelPermissionGroupsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.ModelPermissionGroupsResponse" + "$ref": "#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.PatchUserRequest": { + "internal_transport_http_admin.PatchUserRequest": { "type": "object", "properties": { "avatarURL": { @@ -10949,7 +11008,7 @@ } } }, - "admin.PaymentOrderListResponseDoc": { + "internal_transport_http_admin.PaymentOrderListResponseDoc": { "type": "object", "properties": { "data": { @@ -10958,7 +11017,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.PaymentOrderResponse" + "$ref": "#/definitions/internal_transport_http_admin.PaymentOrderResponse" } }, "total": { @@ -10971,7 +11030,7 @@ } } }, - "admin.PaymentOrderResponse": { + "internal_transport_http_admin.PaymentOrderResponse": { "type": "object", "properties": { "baseAmountCents": { @@ -11057,26 +11116,26 @@ } } }, - "admin.PermissionGroupDataResponse": { + "internal_transport_http_admin.PermissionGroupDataResponse": { "type": "object", "properties": { "group": { - "$ref": "#/definitions/admin.PermissionGroupResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupResponse" } } }, - "admin.PermissionGroupDataResponseDoc": { + "internal_transport_http_admin.PermissionGroupDataResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.PermissionGroupDataResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupDataResponse" }, "errorMsg": { "type": "string" } } }, - "admin.PermissionGroupDeleteSummaryResponse": { + "internal_transport_http_admin.PermissionGroupDeleteSummaryResponse": { "type": "object", "properties": { "manualModelCount": { @@ -11093,29 +11152,29 @@ } } }, - "admin.PermissionGroupListResponse": { + "internal_transport_http_admin.PermissionGroupListResponse": { "type": "object", "properties": { "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.PermissionGroupResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupResponse" } } } }, - "admin.PermissionGroupListResponseDoc": { + "internal_transport_http_admin.PermissionGroupListResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.PermissionGroupListResponse" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupListResponse" }, "errorMsg": { "type": "string" } } }, - "admin.PermissionGroupModelRuleRequest": { + "internal_transport_http_admin.PermissionGroupModelRuleRequest": { "type": "object", "required": [ "type" @@ -11131,7 +11190,7 @@ } } }, - "admin.PermissionGroupModelRuleResponse": { + "internal_transport_http_admin.PermissionGroupModelRuleResponse": { "type": "object", "properties": { "type": { @@ -11142,7 +11201,7 @@ } } }, - "admin.PermissionGroupResponse": { + "internal_transport_http_admin.PermissionGroupResponse": { "type": "object", "properties": { "createdAt": { @@ -11186,7 +11245,7 @@ } } }, - "admin.ResetUserPasswordRequest": { + "internal_transport_http_admin.ResetUserPasswordRequest": { "type": "object", "required": [ "newPassword" @@ -11202,7 +11261,7 @@ } } }, - "admin.ResetUserPasswordResponse": { + "internal_transport_http_admin.ResetUserPasswordResponse": { "type": "object", "properties": { "reset": { @@ -11210,18 +11269,18 @@ } } }, - "admin.ResetUserPasswordResponseDoc": { + "internal_transport_http_admin.ResetUserPasswordResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.ResetUserPasswordResponse" + "$ref": "#/definitions/internal_transport_http_admin.ResetUserPasswordResponse" }, "errorMsg": { "type": "string" } } }, - "admin.RevokeUserSessionsResponse": { + "internal_transport_http_admin.RevokeUserSessionsResponse": { "type": "object", "properties": { "revoked": { @@ -11229,18 +11288,18 @@ } } }, - "admin.RevokeUserSessionsResponseDoc": { + "internal_transport_http_admin.RevokeUserSessionsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.RevokeUserSessionsResponse" + "$ref": "#/definitions/internal_transport_http_admin.RevokeUserSessionsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.SetGroupModelsRequest": { + "internal_transport_http_admin.SetGroupModelsRequest": { "type": "object", "properties": { "modelIDs": { @@ -11252,12 +11311,12 @@ "rules": { "type": "array", "items": { - "$ref": "#/definitions/admin.PermissionGroupModelRuleRequest" + "$ref": "#/definitions/internal_transport_http_admin.PermissionGroupModelRuleRequest" } } } }, - "admin.SetGroupUsersRequest": { + "internal_transport_http_admin.SetGroupUsersRequest": { "type": "object", "properties": { "userIDs": { @@ -11268,7 +11327,7 @@ } } }, - "admin.SetModelPermissionGroupsRequest": { + "internal_transport_http_admin.SetModelPermissionGroupsRequest": { "type": "object", "properties": { "groupIDs": { @@ -11279,7 +11338,7 @@ } } }, - "admin.SystemEventListResponseDoc": { + "internal_transport_http_admin.SystemEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -11288,7 +11347,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.SystemEventResponse" + "$ref": "#/definitions/internal_transport_http_admin.SystemEventResponse" } }, "total": { @@ -11301,7 +11360,7 @@ } } }, - "admin.SystemEventResponse": { + "internal_transport_http_admin.SystemEventResponse": { "type": "object", "properties": { "createdAt": { @@ -11342,7 +11401,7 @@ } } }, - "admin.UpdatePermissionGroupRequest": { + "internal_transport_http_admin.UpdatePermissionGroupRequest": { "type": "object", "required": [ "name" @@ -11363,7 +11422,7 @@ } } }, - "admin.UpdateUserStatusRequest": { + "internal_transport_http_admin.UpdateUserStatusRequest": { "type": "object", "required": [ "status" @@ -11379,18 +11438,18 @@ } } }, - "admin.UpdateUserStatusResponseDoc": { + "internal_transport_http_admin.UpdateUserStatusResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.UserDataResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserDataResponse" }, "errorMsg": { "type": "string" } } }, - "admin.UsageLogListResponseDoc": { + "internal_transport_http_admin.UsageLogListResponseDoc": { "type": "object", "properties": { "data": { @@ -11399,7 +11458,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageLogResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageLogResponse" } }, "total": { @@ -11412,7 +11471,7 @@ } } }, - "admin.UsageLogResponse": { + "internal_transport_http_admin.UsageLogResponse": { "type": "object", "properties": { "billedCurrency": { @@ -11513,7 +11572,7 @@ } } }, - "admin.UsageStatisticsMetricsResponse": { + "internal_transport_http_admin.UsageStatisticsMetricsResponse": { "type": "object", "properties": { "avgLatencyMS": { @@ -11551,7 +11610,7 @@ } } }, - "admin.UsageStatisticsModelRankResponse": { + "internal_transport_http_admin.UsageStatisticsModelRankResponse": { "type": "object", "properties": { "avgLatencyMS": { @@ -11593,12 +11652,12 @@ "trend": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse" } } } }, - "admin.UsageStatisticsResponse": { + "internal_transport_http_admin.UsageStatisticsResponse": { "type": "object", "properties": { "range": { @@ -11615,41 +11674,44 @@ } } }, + "section": { + "type": "string" + }, "topModels": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsModelRankResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsModelRankResponse" } }, "topUsers": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsUserRankResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsUserRankResponse" } }, "totals": { - "$ref": "#/definitions/admin.UsageStatisticsMetricsResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsMetricsResponse" }, "trend": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse" } } } }, - "admin.UsageStatisticsResponseDoc": { + "internal_transport_http_admin.UsageStatisticsResponseDoc": { "type": "object", "properties": { "data": { - "$ref": "#/definitions/admin.UsageStatisticsResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsResponse" }, "errorMsg": { "type": "string" } } }, - "admin.UsageStatisticsTrendResponse": { + "internal_transport_http_admin.UsageStatisticsTrendResponse": { "type": "object", "properties": { "avgLatencyMS": { @@ -11690,7 +11752,7 @@ } } }, - "admin.UsageStatisticsUserRankResponse": { + "internal_transport_http_admin.UsageStatisticsUserRankResponse": { "type": "object", "properties": { "avgLatencyMS": { @@ -11729,7 +11791,7 @@ "trend": { "type": "array", "items": { - "$ref": "#/definitions/admin.UsageStatisticsTrendResponse" + "$ref": "#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse" } }, "userDisplayName": { @@ -11746,7 +11808,7 @@ } } }, - "admin.UserAuthEventListResponseDoc": { + "internal_transport_http_admin.UserAuthEventListResponseDoc": { "type": "object", "properties": { "data": { @@ -11755,7 +11817,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.AuthEventResponse" + "$ref": "#/definitions/internal_transport_http_admin.AuthEventResponse" } }, "total": { @@ -11768,15 +11830,15 @@ } } }, - "admin.UserDataResponse": { + "internal_transport_http_admin.UserDataResponse": { "type": "object", "properties": { "user": { - "$ref": "#/definitions/admin.UserResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserResponse" } } }, - "admin.UserIdentityProviderSummaryResponse": { + "internal_transport_http_admin.UserIdentityProviderSummaryResponse": { "type": "object", "properties": { "id": { @@ -11796,7 +11858,7 @@ } } }, - "admin.UserListResponseDoc": { + "internal_transport_http_admin.UserListResponseDoc": { "type": "object", "properties": { "data": { @@ -11805,7 +11867,7 @@ "results": { "type": "array", "items": { - "$ref": "#/definitions/admin.UserResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserResponse" } }, "total": { @@ -11818,7 +11880,7 @@ } } }, - "admin.UserResponse": { + "internal_transport_http_admin.UserResponse": { "type": "object", "properties": { "appearancePreferences": { @@ -11857,7 +11919,7 @@ "identityProviders": { "type": "array", "items": { - "$ref": "#/definitions/admin.UserIdentityProviderSummaryResponse" + "$ref": "#/definitions/internal_transport_http_admin.UserIdentityProviderSummaryResponse" } }, "lastActiveAt": { @@ -11925,41 +11987,6 @@ } } }, - "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope": { - "type": "object", - "properties": { - "data": {}, - "details": {}, - "errorCode": { - "type": "string" - }, - "errorMsg": { - "type": "string" - }, - "requestId": { - "type": "string" - } - } - }, - "github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc": { - "type": "object", - "properties": { - "data": {}, - "details": {}, - "errorCode": { - "type": "string", - "example": "" - }, - "errorMsg": { - "type": "string", - "example": "" - }, - "requestId": { - "type": "string", - "example": "" - } - } - }, "internal_transport_http_announcement.AdminAnnouncementListResponseDoc": { "type": "object", "properties": { @@ -18353,4 +18380,4 @@ "in": "header" } } -} +} \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index e4e1eb76..a5bed100 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,12 +1,37 @@ basePath: /api/v1 definitions: - admin.AuditLogListResponseDoc: + github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope: + properties: + data: {} + details: {} + errorCode: + type: string + errorMsg: + type: string + requestId: + type: string + type: object + github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc: + properties: + data: {} + details: {} + errorCode: + example: "" + type: string + errorMsg: + example: "" + type: string + requestId: + example: "" + type: string + type: object + internal_transport_http_admin.AuditLogListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/admin.AuditLogResponse' + $ref: '#/definitions/internal_transport_http_admin.AuditLogResponse' type: array total: type: integer @@ -14,7 +39,7 @@ definitions: errorMsg: type: string type: object - admin.AuditLogResponse: + internal_transport_http_admin.AuditLogResponse: properties: action: type: string @@ -45,7 +70,7 @@ definitions: userAgent: type: string type: object - admin.AuthEventResponse: + internal_transport_http_admin.AuthEventResponse: properties: clientIP: type: string @@ -78,7 +103,7 @@ definitions: username: type: string type: object - admin.CleanupLogsRequest: + internal_transport_http_admin.CleanupLogsRequest: properties: before: type: string @@ -88,7 +113,7 @@ definitions: - before - type type: object - admin.CleanupLogsResponse: + internal_transport_http_admin.CleanupLogsResponse: properties: before: type: string @@ -97,20 +122,20 @@ definitions: type: type: string type: object - admin.CleanupLogsResponseDoc: + internal_transport_http_admin.CleanupLogsResponseDoc: properties: data: - $ref: '#/definitions/admin.CleanupLogsResponse' + $ref: '#/definitions/internal_transport_http_admin.CleanupLogsResponse' errorMsg: type: string type: object - admin.ConversationEventListResponseDoc: + internal_transport_http_admin.ConversationEventListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/admin.ConversationEventResponse' + $ref: '#/definitions/internal_transport_http_admin.ConversationEventResponse' type: array total: type: integer @@ -118,7 +143,7 @@ definitions: errorMsg: type: string type: object - admin.ConversationEventResponse: + internal_transport_http_admin.ConversationEventResponse: properties: contentMarkdown: type: string @@ -193,7 +218,7 @@ definitions: username: type: string type: object - admin.CreatePermissionGroupRequest: + internal_transport_http_admin.CreatePermissionGroupRequest: properties: description: maxLength: 512 @@ -208,7 +233,7 @@ definitions: required: - name type: object - admin.CreateUserRequest: + internal_transport_http_admin.CreateUserRequest: properties: avatarURL: maxLength: 2048 @@ -246,40 +271,40 @@ definitions: - password - username type: object - admin.CreateUserResponseDoc: + internal_transport_http_admin.CreateUserResponseDoc: properties: data: - $ref: '#/definitions/admin.UserDataResponse' + $ref: '#/definitions/internal_transport_http_admin.UserDataResponse' errorMsg: type: string type: object - admin.DeletePermissionGroupResponse: + internal_transport_http_admin.DeletePermissionGroupResponse: properties: deleted: type: boolean summary: - $ref: '#/definitions/admin.PermissionGroupDeleteSummaryResponse' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupDeleteSummaryResponse' type: object - admin.DeletePermissionGroupResponseDoc: + internal_transport_http_admin.DeletePermissionGroupResponseDoc: properties: data: - $ref: '#/definitions/admin.DeletePermissionGroupResponse' + $ref: '#/definitions/internal_transport_http_admin.DeletePermissionGroupResponse' errorMsg: type: string type: object - admin.DeleteUserResponse: + internal_transport_http_admin.DeleteUserResponse: properties: deleted: type: boolean type: object - admin.DeleteUserResponseDoc: + internal_transport_http_admin.DeleteUserResponseDoc: properties: data: - $ref: '#/definitions/admin.DeleteUserResponse' + $ref: '#/definitions/internal_transport_http_admin.DeleteUserResponse' errorMsg: type: string type: object - admin.ErrorDoc: + internal_transport_http_admin.ErrorDoc: properties: data: {} details: {} @@ -290,7 +315,7 @@ definitions: requestId: type: string type: object - admin.GroupModelsResponse: + internal_transport_http_admin.GroupModelsResponse: properties: modelIDs: items: @@ -298,31 +323,31 @@ definitions: type: array rules: items: - $ref: '#/definitions/admin.PermissionGroupModelRuleResponse' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupModelRuleResponse' type: array type: object - admin.GroupModelsResponseDoc: + internal_transport_http_admin.GroupModelsResponseDoc: properties: data: - $ref: '#/definitions/admin.GroupModelsResponse' + $ref: '#/definitions/internal_transport_http_admin.GroupModelsResponse' errorMsg: type: string type: object - admin.GroupUsersResponse: + internal_transport_http_admin.GroupUsersResponse: properties: userIDs: items: type: integer type: array type: object - admin.GroupUsersResponseDoc: + internal_transport_http_admin.GroupUsersResponseDoc: properties: data: - $ref: '#/definitions/admin.GroupUsersResponse' + $ref: '#/definitions/internal_transport_http_admin.GroupUsersResponse' errorMsg: type: string type: object - admin.ImportOpenWebUIUsersRequest: + internal_transport_http_admin.ImportOpenWebUIUsersRequest: properties: creditMultiplier: type: number @@ -335,7 +360,7 @@ definitions: - creditMultiplier - dsn type: object - admin.ImportOpenWebUIUsersResponse: + internal_transport_http_admin.ImportOpenWebUIUsersResponse: properties: dedupeField: type: string @@ -356,14 +381,14 @@ definitions: source: type: string type: object - admin.ImportOpenWebUIUsersResponseDoc: + internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc: properties: data: - $ref: '#/definitions/admin.ImportOpenWebUIUsersResponse' + $ref: '#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponse' errorMsg: type: string type: object - admin.ModelPermissionGroupsResponse: + internal_transport_http_admin.ModelPermissionGroupsResponse: properties: effectiveGroupIDs: items: @@ -380,14 +405,14 @@ definitions: unassigned: type: boolean type: object - admin.ModelPermissionGroupsResponseDoc: + internal_transport_http_admin.ModelPermissionGroupsResponseDoc: properties: data: - $ref: '#/definitions/admin.ModelPermissionGroupsResponse' + $ref: '#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponse' errorMsg: type: string type: object - admin.PatchUserRequest: + internal_transport_http_admin.PatchUserRequest: properties: avatarURL: maxLength: 2048 @@ -426,13 +451,13 @@ definitions: maxLength: 64 type: string type: object - admin.PaymentOrderListResponseDoc: + internal_transport_http_admin.PaymentOrderListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/admin.PaymentOrderResponse' + $ref: '#/definitions/internal_transport_http_admin.PaymentOrderResponse' type: array total: type: integer @@ -440,7 +465,7 @@ definitions: errorMsg: type: string type: object - admin.PaymentOrderResponse: + internal_transport_http_admin.PaymentOrderResponse: properties: baseAmountCents: type: integer @@ -497,19 +522,19 @@ definitions: username: type: string type: object - admin.PermissionGroupDataResponse: + internal_transport_http_admin.PermissionGroupDataResponse: properties: group: - $ref: '#/definitions/admin.PermissionGroupResponse' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupResponse' type: object - admin.PermissionGroupDataResponseDoc: + internal_transport_http_admin.PermissionGroupDataResponseDoc: properties: data: - $ref: '#/definitions/admin.PermissionGroupDataResponse' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupDataResponse' errorMsg: type: string type: object - admin.PermissionGroupDeleteSummaryResponse: + internal_transport_http_admin.PermissionGroupDeleteSummaryResponse: properties: manualModelCount: type: integer @@ -520,21 +545,21 @@ definitions: ruleCount: type: integer type: object - admin.PermissionGroupListResponse: + internal_transport_http_admin.PermissionGroupListResponse: properties: results: items: - $ref: '#/definitions/admin.PermissionGroupResponse' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupResponse' type: array type: object - admin.PermissionGroupListResponseDoc: + internal_transport_http_admin.PermissionGroupListResponseDoc: properties: data: - $ref: '#/definitions/admin.PermissionGroupListResponse' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupListResponse' errorMsg: type: string type: object - admin.PermissionGroupModelRuleRequest: + internal_transport_http_admin.PermissionGroupModelRuleRequest: properties: type: maxLength: 32 @@ -545,14 +570,14 @@ definitions: required: - type type: object - admin.PermissionGroupModelRuleResponse: + internal_transport_http_admin.PermissionGroupModelRuleResponse: properties: type: type: string value: type: string type: object - admin.PermissionGroupResponse: + internal_transport_http_admin.PermissionGroupResponse: properties: createdAt: type: string @@ -581,7 +606,7 @@ definitions: userCount: type: integer type: object - admin.ResetUserPasswordRequest: + internal_transport_http_admin.ResetUserPasswordRequest: properties: mustResetPassword: type: boolean @@ -592,31 +617,31 @@ definitions: required: - newPassword type: object - admin.ResetUserPasswordResponse: + internal_transport_http_admin.ResetUserPasswordResponse: properties: reset: type: boolean type: object - admin.ResetUserPasswordResponseDoc: + internal_transport_http_admin.ResetUserPasswordResponseDoc: properties: data: - $ref: '#/definitions/admin.ResetUserPasswordResponse' + $ref: '#/definitions/internal_transport_http_admin.ResetUserPasswordResponse' errorMsg: type: string type: object - admin.RevokeUserSessionsResponse: + internal_transport_http_admin.RevokeUserSessionsResponse: properties: revoked: type: boolean type: object - admin.RevokeUserSessionsResponseDoc: + internal_transport_http_admin.RevokeUserSessionsResponseDoc: properties: data: - $ref: '#/definitions/admin.RevokeUserSessionsResponse' + $ref: '#/definitions/internal_transport_http_admin.RevokeUserSessionsResponse' errorMsg: type: string type: object - admin.SetGroupModelsRequest: + internal_transport_http_admin.SetGroupModelsRequest: properties: modelIDs: items: @@ -624,30 +649,30 @@ definitions: type: array rules: items: - $ref: '#/definitions/admin.PermissionGroupModelRuleRequest' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupModelRuleRequest' type: array type: object - admin.SetGroupUsersRequest: + internal_transport_http_admin.SetGroupUsersRequest: properties: userIDs: items: type: integer type: array type: object - admin.SetModelPermissionGroupsRequest: + internal_transport_http_admin.SetModelPermissionGroupsRequest: properties: groupIDs: items: type: integer type: array type: object - admin.SystemEventListResponseDoc: + internal_transport_http_admin.SystemEventListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/admin.SystemEventResponse' + $ref: '#/definitions/internal_transport_http_admin.SystemEventResponse' type: array total: type: integer @@ -655,7 +680,7 @@ definitions: errorMsg: type: string type: object - admin.SystemEventResponse: + internal_transport_http_admin.SystemEventResponse: properties: createdAt: type: string @@ -682,7 +707,7 @@ definitions: updatedAt: type: string type: object - admin.UpdatePermissionGroupRequest: + internal_transport_http_admin.UpdatePermissionGroupRequest: properties: description: maxLength: 512 @@ -697,7 +722,7 @@ definitions: required: - name type: object - admin.UpdateUserStatusRequest: + internal_transport_http_admin.UpdateUserStatusRequest: properties: reason: maxLength: 255 @@ -708,20 +733,20 @@ definitions: required: - status type: object - admin.UpdateUserStatusResponseDoc: + internal_transport_http_admin.UpdateUserStatusResponseDoc: properties: data: - $ref: '#/definitions/admin.UserDataResponse' + $ref: '#/definitions/internal_transport_http_admin.UserDataResponse' errorMsg: type: string type: object - admin.UsageLogListResponseDoc: + internal_transport_http_admin.UsageLogListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/admin.UsageLogResponse' + $ref: '#/definitions/internal_transport_http_admin.UsageLogResponse' type: array total: type: integer @@ -729,7 +754,7 @@ definitions: errorMsg: type: string type: object - admin.UsageLogResponse: + internal_transport_http_admin.UsageLogResponse: properties: billedCurrency: type: string @@ -796,7 +821,7 @@ definitions: username: type: string type: object - admin.UsageStatisticsMetricsResponse: + internal_transport_http_admin.UsageStatisticsMetricsResponse: properties: avgLatencyMS: type: integer @@ -821,7 +846,7 @@ definitions: totalTokens: type: integer type: object - admin.UsageStatisticsModelRankResponse: + internal_transport_http_admin.UsageStatisticsModelRankResponse: properties: avgLatencyMS: type: integer @@ -849,10 +874,10 @@ definitions: type: integer trend: items: - $ref: '#/definitions/admin.UsageStatisticsTrendResponse' + $ref: '#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse' type: array type: object - admin.UsageStatisticsResponse: + internal_transport_http_admin.UsageStatisticsResponse: properties: range: properties: @@ -863,29 +888,31 @@ definitions: startDate: type: string type: object + section: + type: string topModels: items: - $ref: '#/definitions/admin.UsageStatisticsModelRankResponse' + $ref: '#/definitions/internal_transport_http_admin.UsageStatisticsModelRankResponse' type: array topUsers: items: - $ref: '#/definitions/admin.UsageStatisticsUserRankResponse' + $ref: '#/definitions/internal_transport_http_admin.UsageStatisticsUserRankResponse' type: array totals: - $ref: '#/definitions/admin.UsageStatisticsMetricsResponse' + $ref: '#/definitions/internal_transport_http_admin.UsageStatisticsMetricsResponse' trend: items: - $ref: '#/definitions/admin.UsageStatisticsTrendResponse' + $ref: '#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse' type: array type: object - admin.UsageStatisticsResponseDoc: + internal_transport_http_admin.UsageStatisticsResponseDoc: properties: data: - $ref: '#/definitions/admin.UsageStatisticsResponse' + $ref: '#/definitions/internal_transport_http_admin.UsageStatisticsResponse' errorMsg: type: string type: object - admin.UsageStatisticsTrendResponse: + internal_transport_http_admin.UsageStatisticsTrendResponse: properties: avgLatencyMS: type: integer @@ -912,7 +939,7 @@ definitions: totalTokens: type: integer type: object - admin.UsageStatisticsUserRankResponse: + internal_transport_http_admin.UsageStatisticsUserRankResponse: properties: avgLatencyMS: type: integer @@ -938,7 +965,7 @@ definitions: type: integer trend: items: - $ref: '#/definitions/admin.UsageStatisticsTrendResponse' + $ref: '#/definitions/internal_transport_http_admin.UsageStatisticsTrendResponse' type: array userDisplayName: type: string @@ -949,13 +976,13 @@ definitions: username: type: string type: object - admin.UserAuthEventListResponseDoc: + internal_transport_http_admin.UserAuthEventListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/admin.AuthEventResponse' + $ref: '#/definitions/internal_transport_http_admin.AuthEventResponse' type: array total: type: integer @@ -963,12 +990,12 @@ definitions: errorMsg: type: string type: object - admin.UserDataResponse: + internal_transport_http_admin.UserDataResponse: properties: user: - $ref: '#/definitions/admin.UserResponse' + $ref: '#/definitions/internal_transport_http_admin.UserResponse' type: object - admin.UserIdentityProviderSummaryResponse: + internal_transport_http_admin.UserIdentityProviderSummaryResponse: properties: id: type: integer @@ -981,13 +1008,13 @@ definitions: type: type: string type: object - admin.UserListResponseDoc: + internal_transport_http_admin.UserListResponseDoc: properties: data: properties: results: items: - $ref: '#/definitions/admin.UserResponse' + $ref: '#/definitions/internal_transport_http_admin.UserResponse' type: array total: type: integer @@ -995,7 +1022,7 @@ definitions: errorMsg: type: string type: object - admin.UserResponse: + internal_transport_http_admin.UserResponse: properties: appearancePreferences: type: string @@ -1021,7 +1048,7 @@ definitions: type: integer identityProviders: items: - $ref: '#/definitions/admin.UserIdentityProviderSummaryResponse' + $ref: '#/definitions/internal_transport_http_admin.UserIdentityProviderSummaryResponse' type: array lastActiveAt: type: string @@ -1066,31 +1093,6 @@ definitions: username: type: string type: object - github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.Envelope: - properties: - data: {} - details: {} - errorCode: - type: string - errorMsg: - type: string - requestId: - type: string - type: object - github_com_DEEIX-AI_DEEIX-Chat_backend_internal_shared_response.SuccessDoc: - properties: - data: {} - details: {} - errorCode: - example: "" - type: string - errorMsg: - example: "" - type: string - requestId: - example: "" - type: string - type: object internal_transport_http_announcement.AdminAnnouncementListResponseDoc: properties: data: @@ -5354,7 +5356,7 @@ info: contact: {} description: DEEIX Chat 后端 API 文档 title: DEEIX Chat API - version: "0.3.2" + version: 0.3.2 paths: /admin/announcements: get: @@ -5556,11 +5558,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.AuditLogListResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.AuditLogListResponseDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询审计日志 @@ -6046,15 +6048,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.UsageLogListResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.UsageLogListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询模型调用日志 @@ -6116,15 +6118,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.ConversationEventListResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.ConversationEventListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询对话事件 @@ -6143,7 +6145,7 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员导出全量对话数据 @@ -7431,22 +7433,22 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.CleanupLogsRequest' + $ref: '#/definitions/internal_transport_http_admin.CleanupLogsRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.CleanupLogsResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.CleanupLogsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员清理日志 @@ -7469,19 +7471,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.ModelPermissionGroupsResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员列出模型权限组 @@ -7502,26 +7504,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.SetModelPermissionGroupsRequest' + $ref: '#/definitions/internal_transport_http_admin.SetModelPermissionGroupsRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.ModelPermissionGroupsResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.ModelPermissionGroupsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员设置模型手动权限组 @@ -7579,15 +7581,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.PaymentOrderListResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.PaymentOrderListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询支付订单记录 @@ -7604,11 +7606,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.PermissionGroupListResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupListResponseDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员列出权限组 @@ -7624,22 +7626,22 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.CreatePermissionGroupRequest' + $ref: '#/definitions/internal_transport_http_admin.CreatePermissionGroupRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.PermissionGroupDataResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员创建权限组 @@ -7662,23 +7664,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.DeletePermissionGroupResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.DeletePermissionGroupResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员删除权限组 @@ -7699,26 +7701,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.UpdatePermissionGroupRequest' + $ref: '#/definitions/internal_transport_http_admin.UpdatePermissionGroupRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.PermissionGroupDataResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.PermissionGroupDataResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员更新权限组 @@ -7741,19 +7743,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.GroupModelsResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.GroupModelsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员列出权限组模型 @@ -7774,26 +7776,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.SetGroupModelsRequest' + $ref: '#/definitions/internal_transport_http_admin.SetGroupModelsRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.GroupModelsResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.GroupModelsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员设置权限组模型 @@ -7816,23 +7818,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.GroupUsersResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.GroupUsersResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员列出权限组用户 @@ -7853,30 +7855,30 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.SetGroupUsersRequest' + $ref: '#/definitions/internal_transport_http_admin.SetGroupUsersRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.GroupUsersResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.GroupUsersResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员设置权限组用户 @@ -8481,15 +8483,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.SystemEventListResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.SystemEventListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询系统事件 @@ -8499,7 +8501,7 @@ paths: get: consumes: - application/json - description: 管理员按日期、用户、平台模型和计费范围查看全局费用、Token、调用次数及排名 + description: 管理员按日期、统计对象、平台模型和计费范围查看全局费用、Token、调用次数及排名;用户与权限组筛选互斥 parameters: - description: 开始日期(YYYY-MM-DD),默认近30天 in: query @@ -8513,6 +8515,10 @@ paths: in: query name: user_id type: integer + - description: 权限组ID,与 user_id 互斥 + in: query + name: permission_group_id + type: integer - description: 平台模型名 in: query name: platform_model_name @@ -8521,9 +8527,17 @@ paths: in: query name: billing_scope type: string - - description: 排名指标:cost/tokens/calls + - description: 返回范围:all/models/users in: query - name: rank_by + name: section + type: string + - description: 模型排名指标:cost/tokens/calls + in: query + name: model_rank_by + type: string + - description: 用户排名指标:cost/tokens/calls + in: query + name: user_rank_by type: string produces: - application/json @@ -8531,15 +8545,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.UsageStatisticsResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.UsageStatisticsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询全局用量统计 @@ -8577,15 +8595,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.UserAuthEventListResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.UserAuthEventListResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询用户认证事件 @@ -8623,11 +8641,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.UserListResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.UserListResponseDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员查询用户 @@ -8643,26 +8661,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.CreateUserRequest' + $ref: '#/definitions/internal_transport_http_admin.CreateUserRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.CreateUserResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.CreateUserResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员创建用户 @@ -8685,23 +8703,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.DeleteUserResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.DeleteUserResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员删除用户 @@ -8722,30 +8740,30 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.PatchUserRequest' + $ref: '#/definitions/internal_transport_http_admin.PatchUserRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.UpdateUserStatusResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员更新用户可编辑字段 @@ -8767,30 +8785,30 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.ResetUserPasswordRequest' + $ref: '#/definitions/internal_transport_http_admin.ResetUserPasswordRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.ResetUserPasswordResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.ResetUserPasswordResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员重置用户密码 @@ -8813,19 +8831,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin.RevokeUserSessionsResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.RevokeUserSessionsResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员吊销用户全部会话 @@ -8847,30 +8865,30 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.UpdateUserStatusRequest' + $ref: '#/definitions/internal_transport_http_admin.UpdateUserStatusRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.UpdateUserStatusResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.UpdateUserStatusResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "404": description: Not Found schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "409": description: Conflict schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员更新用户状态 @@ -8887,26 +8905,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/admin.ImportOpenWebUIUsersRequest' + $ref: '#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/admin.ImportOpenWebUIUsersResponseDoc' + $ref: '#/definitions/internal_transport_http_admin.ImportOpenWebUIUsersResponseDoc' "400": description: Bad Request schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "403": description: Forbidden schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' "500": description: Internal Server Error schema: - $ref: '#/definitions/admin.ErrorDoc' + $ref: '#/definitions/internal_transport_http_admin.ErrorDoc' security: - BearerAuth: [] summary: 管理员导入 OpenWebUI 用户 diff --git a/backend/internal/application/admin/service_logs.go b/backend/internal/application/admin/service_logs.go index 20a35950..82f26f43 100644 --- a/backend/internal/application/admin/service_logs.go +++ b/backend/internal/application/admin/service_logs.go @@ -30,6 +30,17 @@ func (s *Service) ListUsageLogs(ctx context.Context, page int, pageSize int, fil // GetUsageStatistics 查询管理员仪表盘的全局用量聚合。 func (s *Service) GetUsageStatistics(ctx context.Context, filter billing.UsageStatisticsFilter) (domainbilling.UsageStatistics, error) { + if filter.UserID > 0 && filter.PermissionGroupID > 0 { + return domainbilling.UsageStatistics{}, billing.ErrInvalidUsageStatisticsSubject + } + if filter.PermissionGroupID > 0 { + if s.permissionGroupRepo == nil { + return domainbilling.UsageStatistics{}, ErrPermissionGroupRepoUnavailable + } + if _, err := s.permissionGroupRepo.GetPermissionGroup(ctx, filter.PermissionGroupID); err != nil { + return domainbilling.UsageStatistics{}, mapPermissionGroupRepoError(err) + } + } if s.usageStatisticsService == nil { return domainbilling.UsageStatistics{}, errors.New("usage statistics service unavailable") } diff --git a/backend/internal/application/admin/service_test.go b/backend/internal/application/admin/service_test.go index f83aad7a..5dd00037 100644 --- a/backend/internal/application/admin/service_test.go +++ b/backend/internal/application/admin/service_test.go @@ -10,6 +10,7 @@ import ( "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/billing" userapp "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/user" domainaudit "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/audit" + domainbilling "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/billing" domainchannel "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/channel" domainuser "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/user" "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/repository" @@ -79,6 +80,48 @@ func TestBuildUserViewsIncludesBillingBalanceOutsideSelfMode(t *testing.T) { } } +func TestGetUsageStatisticsRejectsConflictingSubjectFilters(t *testing.T) { + statistics := &usageStatisticsServiceFake{} + service := NewService(newAdminUserServiceFake(nil), auditServiceFake{}) + service.SetUsageStatisticsService(statistics) + + _, err := service.GetUsageStatistics(context.Background(), billing.UsageStatisticsFilter{ + UserID: 1, + PermissionGroupID: 2, + }) + if !errors.Is(err, billing.ErrInvalidUsageStatisticsSubject) { + t.Fatalf("expected conflicting subject filter error, got %v", err) + } + if statistics.calls != 0 { + t.Fatalf("expected statistics service not to be called, got %d calls", statistics.calls) + } +} + +func TestGetUsageStatisticsValidatesPermissionGroup(t *testing.T) { + statistics := &usageStatisticsServiceFake{} + service := NewService(newAdminUserServiceFake(nil), auditServiceFake{}) + service.SetUsageStatisticsService(statistics) + service.SetPermissionGroupRepo(permissionGroupRepoFake{ + groups: map[uint]domainchannel.PermissionGroup{7: {ID: 7, Name: "Pro"}}, + }) + + _, err := service.GetUsageStatistics(context.Background(), billing.UsageStatisticsFilter{PermissionGroupID: 7}) + if err != nil { + t.Fatalf("expected permission group filter to succeed, got %v", err) + } + if statistics.calls != 1 || statistics.filter.PermissionGroupID != 7 { + t.Fatalf("expected permission group filter to be forwarded, calls=%d filter=%+v", statistics.calls, statistics.filter) + } + + _, err = service.GetUsageStatistics(context.Background(), billing.UsageStatisticsFilter{PermissionGroupID: 8}) + if !errors.Is(err, ErrPermissionGroupNotFound) { + t.Fatalf("expected missing permission group error, got %v", err) + } + if statistics.calls != 1 { + t.Fatalf("expected missing permission group not to reach statistics service, got %d calls", statistics.calls) + } +} + func TestBuildUserViewsUsageModeKeepsAccountOnlyView(t *testing.T) { users := newAdminUserServiceFake(map[uint]domainuser.User{ 7: {ID: 7, Username: "alice", Role: domainuser.RoleUser}, @@ -570,6 +613,17 @@ type subscriptionResolverFake struct { accounts map[uint]billing.UserBillingAccountSnapshot } +type usageStatisticsServiceFake struct { + filter billing.UsageStatisticsFilter + calls int +} + +func (f *usageStatisticsServiceFake) GetUsageStatistics(_ context.Context, filter billing.UsageStatisticsFilter) (domainbilling.UsageStatistics, error) { + f.filter = filter + f.calls++ + return domainbilling.UsageStatistics{}, nil +} + func (s subscriptionResolverFake) ListCurrentSubscriptionSnapshots(context.Context, []uint, time.Time) (map[uint]billing.UserSubscriptionSnapshot, error) { if s.subscriptions == nil { return map[uint]billing.UserSubscriptionSnapshot{}, nil diff --git a/backend/internal/application/billing/errs.go b/backend/internal/application/billing/errs.go index b4f01a06..c3030edb 100644 --- a/backend/internal/application/billing/errs.go +++ b/backend/internal/application/billing/errs.go @@ -33,6 +33,8 @@ var ( ErrBillingPlanNotFound = errors.New("billing plan not found") // ErrInvalidPermissionGroup 非法权限组。 ErrInvalidPermissionGroup = errors.New("invalid permission group") + // ErrInvalidUsageStatisticsSubject 表示用量统计的用户与权限组筛选条件冲突。 + ErrInvalidUsageStatisticsSubject = errors.New("user and permission group filters are mutually exclusive") // ErrPermissionGroupReferenceCounterUnavailable 权限组套餐引用检查能力不可用。 ErrPermissionGroupReferenceCounterUnavailable = errors.New("permission group reference counter unavailable") // ErrSubscriptionEntitlementActive 当前仍存在有效付费订阅权益。 diff --git a/backend/internal/application/billing/service.go b/backend/internal/application/billing/service.go index 0727a892..a3585536 100644 --- a/backend/internal/application/billing/service.go +++ b/backend/internal/application/billing/service.go @@ -2271,13 +2271,19 @@ type UsageStatisticsFilter struct { StartDate time.Time EndDate time.Time UserID uint + PermissionGroupID uint PlatformModelName string BillingScope string - RankBy string + Section string + ModelRankBy string + UserRankBy string } // GetUsageStatistics 查询管理员仪表盘使用的全局用量统计。 func (s *Service) GetUsageStatistics(ctx context.Context, filter UsageStatisticsFilter) (domainbilling.UsageStatistics, error) { + if filter.UserID > 0 && filter.PermissionGroupID > 0 { + return domainbilling.UsageStatistics{}, ErrInvalidUsageStatisticsSubject + } statisticsRepo, ok := s.repo.(repository.UsageStatisticsRepository) if !ok { return domainbilling.UsageStatistics{}, errors.New("usage statistics repository unavailable") @@ -2292,29 +2298,41 @@ func (s *Service) GetUsageStatistics(ctx context.Context, filter UsageStatistics if days <= 0 || days > 366 { return domainbilling.UsageStatistics{}, errors.New("invalid usage statistics date range") } - - granularity := "day" - if days > 90 { - granularity = "month" - } + granularity := usageStatisticsGranularity(days) result, err := statisticsRepo.GetUsageStatistics(ctx, repository.UsageStatisticsFilter{ StartDate: startDate, EndDateExclusive: endDate.AddDate(0, 0, 1), UserID: filter.UserID, + PermissionGroupID: filter.PermissionGroupID, + MembershipAt: time.Now(), PlatformModelName: strings.TrimSpace(filter.PlatformModelName), BillingScope: strings.TrimSpace(filter.BillingScope), Granularity: granularity, - RankBy: strings.TrimSpace(filter.RankBy), + Section: strings.TrimSpace(filter.Section), + ModelRankBy: strings.TrimSpace(filter.ModelRankBy), + UserRankBy: strings.TrimSpace(filter.UserRankBy), RankLimit: 10, }) if err != nil { return domainbilling.UsageStatistics{}, err } result.Granularity = granularity - result.Trend = fillUsageStatisticsTrend(result.Trend, startDate, endDate, granularity) + if strings.TrimSpace(filter.Section) == "" || strings.TrimSpace(filter.Section) == "all" { + result.Trend = fillUsageStatisticsTrend(result.Trend, startDate, endDate, granularity) + } return result, nil } +func usageStatisticsGranularity(days int) string { + if days >= 180 { + return "month" + } + if days > 30 { + return "week" + } + return "day" +} + func fillUsageStatisticsTrend( items []domainbilling.UsageStatisticsTrendPoint, startDate time.Time, @@ -2327,7 +2345,10 @@ func fillUsageStatisticsTrend( } current := startDate - if granularity == "month" { + if granularity == "week" { + daysSinceMonday := (int(startDate.Weekday()) + 6) % 7 + current = startDate.AddDate(0, 0, -daysSinceMonday) + } else if granularity == "month" { current = time.Date(startDate.Year(), startDate.Month(), 1, 0, 0, 0, 0, startDate.Location()) } results := make([]domainbilling.UsageStatisticsTrendPoint, 0, len(items)) @@ -2338,7 +2359,9 @@ func fillUsageStatisticsTrend( } else { results = append(results, domainbilling.UsageStatisticsTrendPoint{PeriodStart: current}) } - if granularity == "month" { + if granularity == "week" { + current = current.AddDate(0, 0, 7) + } else if granularity == "month" { current = current.AddDate(0, 1, 0) } else { current = current.AddDate(0, 0, 1) diff --git a/backend/internal/application/billing/service_statistics_test.go b/backend/internal/application/billing/service_statistics_test.go new file mode 100644 index 00000000..94754c69 --- /dev/null +++ b/backend/internal/application/billing/service_statistics_test.go @@ -0,0 +1,55 @@ +package billing + +import ( + "testing" + "time" + + domainbilling "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/billing" +) + +func TestUsageStatisticsGranularity(t *testing.T) { + tests := []struct { + days int + want string + }{ + {days: 1, want: "day"}, + {days: 30, want: "day"}, + {days: 31, want: "week"}, + {days: 90, want: "week"}, + {days: 179, want: "week"}, + {days: 180, want: "month"}, + {days: 366, want: "month"}, + } + + for _, tt := range tests { + if got := usageStatisticsGranularity(tt.days); got != tt.want { + t.Fatalf("usageStatisticsGranularity(%d) = %q, want %q", tt.days, got, tt.want) + } + } +} + +func TestFillUsageStatisticsTrendUsesMondayWeekBoundaries(t *testing.T) { + location := time.UTC + startDate := time.Date(2026, 7, 1, 0, 0, 0, 0, location) + endDate := time.Date(2026, 7, 20, 0, 0, 0, 0, location) + items := []domainbilling.UsageStatisticsTrendPoint{ + { + PeriodStart: time.Date(2026, 7, 6, 0, 0, 0, 0, location), + Metrics: domainbilling.UsageStatisticsMetrics{CallCount: 3}, + }, + } + + result := fillUsageStatisticsTrend(items, startDate, endDate, "week") + wantDates := []string{"2026-06-29", "2026-07-06", "2026-07-13", "2026-07-20"} + if len(result) != len(wantDates) { + t.Fatalf("weekly trend length = %d, want %d", len(result), len(wantDates)) + } + for index, wantDate := range wantDates { + if got := result[index].PeriodStart.Format("2006-01-02"); got != wantDate { + t.Fatalf("weekly trend[%d] = %q, want %q", index, got, wantDate) + } + } + if result[1].Metrics.CallCount != 3 { + t.Fatalf("weekly trend did not preserve existing metrics: %+v", result[1]) + } +} diff --git a/backend/internal/infra/persistence/postgres/billing/repository.go b/backend/internal/infra/persistence/postgres/billing/repository.go index 58add142..0686f568 100644 --- a/backend/internal/infra/persistence/postgres/billing/repository.go +++ b/backend/internal/infra/persistence/postgres/billing/repository.go @@ -67,6 +67,13 @@ func (r *Repo) usageMonthKeyExpression() string { return "TO_CHAR(date_trunc('month', usage_date), 'YYYY-MM-DD')" } +func (r *Repo) usageWeekKeyExpression() string { + if r.sqliteDialect() { + return "date(usage_date, '-' || ((CAST(strftime('%w', usage_date) AS INTEGER) + 6) % 7) || ' days')" + } + return "TO_CHAR(date_trunc('week', usage_date), 'YYYY-MM-DD')" +} + // ListActivePlans 查询启用套餐。 func (r *Repo) ListActivePlans(ctx context.Context) ([]domainbilling.Plan, error) { items := make([]model.BillingPlan, 0) @@ -1520,6 +1527,41 @@ func (r *Repo) usageStatisticsQuery(ctx context.Context, filter repository.Usage if filter.UserID > 0 { query = query.Where("user_id = ?", filter.UserID) } + if filter.PermissionGroupID > 0 { + membershipAt := filter.MembershipAt + if membershipAt.IsZero() { + membershipAt = time.Now() + } + query = query.Where(` + EXISTS ( + SELECT 1 + FROM permission_groups AS permission_group + WHERE permission_group.id = ? + AND ( + permission_group.is_default = ? + OR EXISTS ( + SELECT 1 + FROM permission_group_user_access AS manual_access + WHERE manual_access.group_id = permission_group.id + AND manual_access.user_id = billing_usage_ledgers.user_id + ) + OR EXISTS ( + SELECT 1 + FROM billing_subscriptions AS subscription + JOIN billing_plans AS plan ON plan.id = subscription.plan_id + WHERE plan.permission_group_id = permission_group.id + AND subscription.user_id = billing_usage_ledgers.user_id + AND plan.deleted_at IS NULL + AND subscription.deleted_at IS NULL + AND plan.is_active = ? + AND subscription.status = ? + AND subscription.current_period_start_at <= ? + AND (subscription.current_period_end_at IS NULL OR subscription.current_period_end_at > ?) + ) + ) + ) + `, filter.PermissionGroupID, true, true, "active", membershipAt, membershipAt) + } if platformModelName := strings.TrimSpace(filter.PlatformModelName); platformModelName != "" { query = query.Where("platform_model_name = ?", platformModelName) } @@ -1551,136 +1593,148 @@ func (r *Repo) GetUsageStatistics(ctx context.Context, filter repository.UsageSt TopModels: []domainbilling.UsageStatisticsModelRank{}, TopUsers: []domainbilling.UsageStatisticsUserRank{}, } - - var totals usageStatisticsMetricRow - if err := r.usageStatisticsQuery(ctx, filter). - Select(usageStatisticsMetricsSelect). - Scan(&totals).Error; err != nil { - return result, translateError(err) - } - result.Totals = usageStatisticsMetricsFromRow(totals) - + section := strings.TrimSpace(filter.Section) + includeOverview := section == "" || section == "all" + includeModels := includeOverview || section == "models" + includeUsers := includeOverview || section == "users" periodExpression := r.usageDayKeyExpression() - if filter.Granularity == "month" { + if filter.Granularity == "week" { + periodExpression = r.usageWeekKeyExpression() + } else if filter.Granularity == "month" { periodExpression = r.usageMonthKeyExpression() } - trendRows := make([]usageStatisticsTrendRow, 0) - if err := r.usageStatisticsQuery(ctx, filter). - Select(periodExpression + " AS period_key," + usageStatisticsMetricsSelect). - Group(periodExpression). - Order("period_key ASC"). - Scan(&trendRows).Error; err != nil { - return result, translateError(err) - } - for _, row := range trendRows { - periodStart, err := time.Parse("2006-01-02", row.PeriodKey) - if err != nil { - return result, err - } - result.Trend = append(result.Trend, domainbilling.UsageStatisticsTrendPoint{ - PeriodStart: periodStart, - Metrics: usageStatisticsMetricsFromRow(row.Metrics), - }) - } - rankLimit := filter.RankLimit - if rankLimit <= 0 || rankLimit > 50 { - rankLimit = 10 - } - modelRows := make([]usageStatisticsModelRow, 0, rankLimit) - if err := r.usageStatisticsQuery(ctx, filter). - Select("platform_model_name," + usageStatisticsMetricsSelect). - Group("platform_model_name"). - Order(usageStatisticsRankOrder(filter.RankBy, "platform_model_name ASC")). - Limit(rankLimit). - Scan(&modelRows).Error; err != nil { - return result, translateError(err) - } - for _, row := range modelRows { - result.TopModels = append(result.TopModels, domainbilling.UsageStatisticsModelRank{ - PlatformModelName: row.PlatformModelName, - Metrics: usageStatisticsMetricsFromRow(row.Metrics), - Trend: []domainbilling.UsageStatisticsTrendPoint{}, - }) - } - if len(result.TopModels) > 0 { - modelNames := make([]string, 0, len(result.TopModels)) - modelIndexes := make(map[string]int, len(result.TopModels)) - for index, item := range result.TopModels { - modelNames = append(modelNames, item.PlatformModelName) - modelIndexes[item.PlatformModelName] = index + if includeOverview { + var totals usageStatisticsMetricRow + if err := r.usageStatisticsQuery(ctx, filter). + Select(usageStatisticsMetricsSelect). + Scan(&totals).Error; err != nil { + return result, translateError(err) } - modelTrendRows := make([]usageStatisticsModelTrendRow, 0) + result.Totals = usageStatisticsMetricsFromRow(totals) + + trendRows := make([]usageStatisticsTrendRow, 0) if err := r.usageStatisticsQuery(ctx, filter). - Where("platform_model_name IN ?", modelNames). - Select(periodExpression + " AS period_key, platform_model_name," + usageStatisticsMetricsSelect). - Group(periodExpression + ", platform_model_name"). - Order("period_key ASC, platform_model_name ASC"). - Scan(&modelTrendRows).Error; err != nil { + Select(periodExpression + " AS period_key," + usageStatisticsMetricsSelect). + Group(periodExpression). + Order("period_key ASC"). + Scan(&trendRows).Error; err != nil { return result, translateError(err) } - for _, row := range modelTrendRows { + for _, row := range trendRows { periodStart, err := time.Parse("2006-01-02", row.PeriodKey) if err != nil { return result, err } - index, exists := modelIndexes[row.PlatformModelName] - if !exists { - continue - } - result.TopModels[index].Trend = append(result.TopModels[index].Trend, domainbilling.UsageStatisticsTrendPoint{ + result.Trend = append(result.Trend, domainbilling.UsageStatisticsTrendPoint{ PeriodStart: periodStart, Metrics: usageStatisticsMetricsFromRow(row.Metrics), }) } } - userRows := make([]usageStatisticsUserRow, 0, rankLimit) - if err := r.usageStatisticsQuery(ctx, filter). - Select("user_id," + usageStatisticsMetricsSelect). - Group("user_id"). - Order(usageStatisticsRankOrder(filter.RankBy, "user_id ASC")). - Limit(rankLimit). - Scan(&userRows).Error; err != nil { - return result, translateError(err) - } - for _, row := range userRows { - result.TopUsers = append(result.TopUsers, domainbilling.UsageStatisticsUserRank{ - UserID: row.UserID, - Metrics: usageStatisticsMetricsFromRow(row.Metrics), - Trend: []domainbilling.UsageStatisticsTrendPoint{}, - }) + rankLimit := filter.RankLimit + if rankLimit <= 0 || rankLimit > 50 { + rankLimit = 10 } - if len(result.TopUsers) > 0 { - userIDs := make([]uint, 0, len(result.TopUsers)) - userIndexes := make(map[uint]int, len(result.TopUsers)) - for index, item := range result.TopUsers { - userIDs = append(userIDs, item.UserID) - userIndexes[item.UserID] = index - } - userTrendRows := make([]usageStatisticsUserTrendRow, 0) + if includeModels { + modelRows := make([]usageStatisticsModelRow, 0, rankLimit) if err := r.usageStatisticsQuery(ctx, filter). - Where("user_id IN ?", userIDs). - Select(periodExpression + " AS period_key, user_id," + usageStatisticsMetricsSelect). - Group(periodExpression + ", user_id"). - Order("period_key ASC, user_id ASC"). - Scan(&userTrendRows).Error; err != nil { + Select("platform_model_name," + usageStatisticsMetricsSelect). + Group("platform_model_name"). + Order(usageStatisticsRankOrder(filter.ModelRankBy, "platform_model_name ASC")). + Limit(rankLimit). + Scan(&modelRows).Error; err != nil { return result, translateError(err) } - for _, row := range userTrendRows { - periodStart, err := time.Parse("2006-01-02", row.PeriodKey) - if err != nil { - return result, err + for _, row := range modelRows { + result.TopModels = append(result.TopModels, domainbilling.UsageStatisticsModelRank{ + PlatformModelName: row.PlatformModelName, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + Trend: []domainbilling.UsageStatisticsTrendPoint{}, + }) + } + if len(result.TopModels) > 0 { + modelNames := make([]string, 0, len(result.TopModels)) + modelIndexes := make(map[string]int, len(result.TopModels)) + for index, item := range result.TopModels { + modelNames = append(modelNames, item.PlatformModelName) + modelIndexes[item.PlatformModelName] = index } - index, exists := userIndexes[row.UserID] - if !exists { - continue + modelTrendRows := make([]usageStatisticsModelTrendRow, 0) + if err := r.usageStatisticsQuery(ctx, filter). + Where("platform_model_name IN ?", modelNames). + Select(periodExpression + " AS period_key, platform_model_name," + usageStatisticsMetricsSelect). + Group(periodExpression + ", platform_model_name"). + Order("period_key ASC, platform_model_name ASC"). + Scan(&modelTrendRows).Error; err != nil { + return result, translateError(err) } - result.TopUsers[index].Trend = append(result.TopUsers[index].Trend, domainbilling.UsageStatisticsTrendPoint{ - PeriodStart: periodStart, - Metrics: usageStatisticsMetricsFromRow(row.Metrics), + for _, row := range modelTrendRows { + periodStart, err := time.Parse("2006-01-02", row.PeriodKey) + if err != nil { + return result, err + } + index, exists := modelIndexes[row.PlatformModelName] + if !exists { + continue + } + result.TopModels[index].Trend = append(result.TopModels[index].Trend, domainbilling.UsageStatisticsTrendPoint{ + PeriodStart: periodStart, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + }) + } + } + } + + if includeUsers { + userRows := make([]usageStatisticsUserRow, 0, rankLimit) + if err := r.usageStatisticsQuery(ctx, filter). + Select("user_id," + usageStatisticsMetricsSelect). + Group("user_id"). + Order(usageStatisticsRankOrder(filter.UserRankBy, "user_id ASC")). + Limit(rankLimit). + Scan(&userRows).Error; err != nil { + return result, translateError(err) + } + for _, row := range userRows { + result.TopUsers = append(result.TopUsers, domainbilling.UsageStatisticsUserRank{ + UserID: row.UserID, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + Trend: []domainbilling.UsageStatisticsTrendPoint{}, }) } + if len(result.TopUsers) > 0 { + userIDs := make([]uint, 0, len(result.TopUsers)) + userIndexes := make(map[uint]int, len(result.TopUsers)) + for index, item := range result.TopUsers { + userIDs = append(userIDs, item.UserID) + userIndexes[item.UserID] = index + } + userTrendRows := make([]usageStatisticsUserTrendRow, 0) + if err := r.usageStatisticsQuery(ctx, filter). + Where("user_id IN ?", userIDs). + Select(periodExpression + " AS period_key, user_id," + usageStatisticsMetricsSelect). + Group(periodExpression + ", user_id"). + Order("period_key ASC, user_id ASC"). + Scan(&userTrendRows).Error; err != nil { + return result, translateError(err) + } + for _, row := range userTrendRows { + periodStart, err := time.Parse("2006-01-02", row.PeriodKey) + if err != nil { + return result, err + } + index, exists := userIndexes[row.UserID] + if !exists { + continue + } + result.TopUsers[index].Trend = append(result.TopUsers[index].Trend, domainbilling.UsageStatisticsTrendPoint{ + PeriodStart: periodStart, + Metrics: usageStatisticsMetricsFromRow(row.Metrics), + }) + } + } } return result, nil diff --git a/backend/internal/infra/persistence/postgres/billing/repository_sqlite_test.go b/backend/internal/infra/persistence/postgres/billing/repository_sqlite_test.go index 9afc8f62..7585c410 100644 --- a/backend/internal/infra/persistence/postgres/billing/repository_sqlite_test.go +++ b/backend/internal/infra/persistence/postgres/billing/repository_sqlite_test.go @@ -90,6 +90,152 @@ func TestUsageQueriesUseSQLitePortableExpressions(t *testing.T) { if len(daily[0].Models) != 2 { t.Fatalf("expected two daily model summaries, got %d", len(daily[0].Models)) } + + statistics, err := repo.GetUsageStatistics(ctx, repository.UsageStatisticsFilter{ + StartDate: usageDate, + EndDateExclusive: usageDate.AddDate(0, 0, 1), + Granularity: "day", + ModelRankBy: "tokens", + UserRankBy: "cost", + RankLimit: 10, + }) + if err != nil { + t.Fatalf("GetUsageStatistics() error = %v", err) + } + if len(statistics.TopModels) != 3 || statistics.TopModels[0].PlatformModelName != "gpt-test" { + t.Fatalf("expected models ranked by tokens, got %+v", statistics.TopModels) + } + if len(statistics.TopUsers) != 2 || statistics.TopUsers[0].UserID != 2 { + t.Fatalf("expected users ranked by cost, got %+v", statistics.TopUsers) + } + + modelStatistics, err := repo.GetUsageStatistics(ctx, repository.UsageStatisticsFilter{ + StartDate: usageDate, + EndDateExclusive: usageDate.AddDate(0, 0, 1), + Granularity: "day", + Section: "models", + ModelRankBy: "tokens", + RankLimit: 10, + }) + if err != nil { + t.Fatalf("GetUsageStatistics(models) error = %v", err) + } + if len(modelStatistics.TopModels) != 3 || len(modelStatistics.TopUsers) != 0 || len(modelStatistics.Trend) != 0 { + t.Fatalf("expected model-only statistics, got %+v", modelStatistics) + } + + weeklyStatistics, err := repo.GetUsageStatistics(ctx, repository.UsageStatisticsFilter{ + StartDate: usageDate, + EndDateExclusive: usageDate.AddDate(0, 0, 1), + Granularity: "week", + Section: "all", + ModelRankBy: "cost", + UserRankBy: "cost", + RankLimit: 10, + }) + if err != nil { + t.Fatalf("GetUsageStatistics(week) error = %v", err) + } + if len(weeklyStatistics.Trend) != 1 || weeklyStatistics.Trend[0].PeriodStart.Format("2006-01-02") != "2026-06-01" { + t.Fatalf("unexpected weekly statistics: %+v", weeklyStatistics.Trend) + } +} + +func TestUsageStatisticsFiltersByCurrentPermissionGroupMembership(t *testing.T) { + db := openBillingSQLiteTestDB(t) + if err := db.AutoMigrate( + &model.PermissionGroup{}, + &model.PermissionGroupUserAccess{}, + &model.BillingPlan{}, + &model.Subscription{}, + ); err != nil { + t.Fatalf("migrate permission group tables: %v", err) + } + + groups := []model.PermissionGroup{ + {Name: "Default", IsDefault: true}, + {Name: "Pro"}, + } + if err := db.Create(&groups).Error; err != nil { + t.Fatalf("create permission groups: %v", err) + } + if err := db.Create(&model.PermissionGroupUserAccess{GroupID: groups[1].ID, UserID: 1}).Error; err != nil { + t.Fatalf("create manual group member: %v", err) + } + + now := time.Date(2026, 7, 15, 12, 0, 0, 0, time.UTC) + activePlan := model.BillingPlan{Code: "pro", Name: "Pro", IsActive: true, PermissionGroupID: &groups[1].ID} + inactivePlan := model.BillingPlan{Code: "legacy", Name: "Legacy", IsActive: false, PermissionGroupID: &groups[1].ID} + if err := db.Create(&[]model.BillingPlan{activePlan, inactivePlan}).Error; err != nil { + t.Fatalf("create billing plans: %v", err) + } + var plans []model.BillingPlan + if err := db.Order("id ASC").Find(&plans).Error; err != nil { + t.Fatalf("reload billing plans: %v", err) + } + periodEnd := now.Add(24 * time.Hour) + expiredEnd := now.Add(-time.Hour) + futureStart := now.Add(time.Hour) + subscriptions := []model.Subscription{ + {UserID: 1, PlanID: plans[0].ID, PriceID: 1, Status: "active", StartAt: now.Add(-time.Hour), CurrentPeriodStartAt: now.Add(-time.Hour), CurrentPeriodEndAt: &periodEnd}, + {UserID: 2, PlanID: plans[0].ID, PriceID: 1, Status: "active", StartAt: now.Add(-time.Hour), CurrentPeriodStartAt: now.Add(-time.Hour), CurrentPeriodEndAt: &periodEnd}, + {UserID: 3, PlanID: plans[0].ID, PriceID: 1, Status: "active", StartAt: now.Add(-48 * time.Hour), CurrentPeriodStartAt: now.Add(-48 * time.Hour), CurrentPeriodEndAt: &expiredEnd}, + {UserID: 4, PlanID: plans[0].ID, PriceID: 1, Status: "active", StartAt: futureStart, CurrentPeriodStartAt: futureStart, CurrentPeriodEndAt: &periodEnd}, + {UserID: 5, PlanID: plans[1].ID, PriceID: 1, Status: "active", StartAt: now.Add(-time.Hour), CurrentPeriodStartAt: now.Add(-time.Hour), CurrentPeriodEndAt: &periodEnd}, + {UserID: 6, PlanID: plans[0].ID, PriceID: 1, Status: "active", StartAt: now.Add(-time.Hour), CurrentPeriodStartAt: now.Add(-time.Hour), CurrentPeriodEndAt: &periodEnd}, + } + if err := db.Create(&subscriptions).Error; err != nil { + t.Fatalf("create subscriptions: %v", err) + } + if err := db.Delete(&subscriptions[5]).Error; err != nil { + t.Fatalf("soft delete active subscription: %v", err) + } + + usageDate := time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC) + ledgers := make([]model.UsageLedger, 0, 6) + for userID := uint(1); userID <= 6; userID++ { + ledgers = append(ledgers, model.UsageLedger{ + UserID: userID, + PlatformModelName: "gpt-test", + BillingAt: usageDate, + UsageDate: usageDate, + CallCount: 1, + BilledNanousd: int64(userID) * 100, + }) + } + if err := db.Create(&ledgers).Error; err != nil { + t.Fatalf("create usage ledgers: %v", err) + } + + filter := repository.UsageStatisticsFilter{ + StartDate: usageDate, + EndDateExclusive: usageDate.AddDate(0, 0, 1), + PermissionGroupID: groups[1].ID, + MembershipAt: now, + Granularity: "day", + ModelRankBy: "cost", + UserRankBy: "cost", + RankLimit: 10, + } + statistics, err := NewRepo(db).GetUsageStatistics(context.Background(), filter) + if err != nil { + t.Fatalf("GetUsageStatistics() error = %v", err) + } + if statistics.Totals.RecordCount != 2 || statistics.Totals.CallCount != 2 || statistics.Totals.BilledNanousd != 300 { + t.Fatalf("expected users 1 and 2 to be counted once, got %+v", statistics.Totals) + } + if len(statistics.TopUsers) != 2 || statistics.TopUsers[0].UserID != 2 || statistics.TopUsers[1].UserID != 1 { + t.Fatalf("unexpected permission group user ranking: %+v", statistics.TopUsers) + } + + filter.PermissionGroupID = groups[0].ID + statistics, err = NewRepo(db).GetUsageStatistics(context.Background(), filter) + if err != nil { + t.Fatalf("GetUsageStatistics(default group) error = %v", err) + } + if statistics.Totals.RecordCount != 6 || statistics.Totals.BilledNanousd != 2100 { + t.Fatalf("expected default group to include all users, got %+v", statistics.Totals) + } } func TestAddUsageAndSettleBalanceRecordsDebtWithoutReservation(t *testing.T) { diff --git a/backend/internal/repository/billing.go b/backend/internal/repository/billing.go index b6181aa0..4c16cce6 100644 --- a/backend/internal/repository/billing.go +++ b/backend/internal/repository/billing.go @@ -116,10 +116,14 @@ type UsageStatisticsFilter struct { StartDate time.Time EndDateExclusive time.Time UserID uint + PermissionGroupID uint + MembershipAt time.Time PlatformModelName string BillingScope string Granularity string - RankBy string + Section string + ModelRankBy string + UserRankBy string RankLimit int } diff --git a/backend/internal/transport/http/admin/dto.go b/backend/internal/transport/http/admin/dto.go index 9200ca63..298cc655 100644 --- a/backend/internal/transport/http/admin/dto.go +++ b/backend/internal/transport/http/admin/dto.go @@ -327,7 +327,8 @@ type UsageStatisticsUserRankResponse struct { // UsageStatisticsResponse 管理员用量统计响应。 type UsageStatisticsResponse struct { - Range struct { + Section string `json:"section"` + Range struct { StartDate string `json:"startDate"` EndDate string `json:"endDate"` Granularity string `json:"granularity"` @@ -829,9 +830,11 @@ func toUsageStatisticsResponse( item domainbilling.UsageStatistics, startDate time.Time, endDate time.Time, + section string, userLabels map[uint]appadmin.UserLabel, ) UsageStatisticsResponse { result := UsageStatisticsResponse{ + Section: section, Totals: toUsageStatisticsMetricsResponse(item.Totals), Trend: make([]UsageStatisticsTrendResponse, 0, len(item.Trend)), TopModels: make([]UsageStatisticsModelRankResponse, 0, len(item.TopModels)), diff --git a/backend/internal/transport/http/admin/handler.go b/backend/internal/transport/http/admin/handler.go index 616503d1..8d7248bc 100644 --- a/backend/internal/transport/http/admin/handler.go +++ b/backend/internal/transport/http/admin/handler.go @@ -487,7 +487,7 @@ func (h *Handler) ListUsageLogs(c *gin.Context) { // GetUsageStatistics godoc // @Summary 管理员查询全局用量统计 -// @Description 管理员按日期、用户、平台模型和计费范围查看全局费用、Token、调用次数及排名 +// @Description 管理员按日期、统计对象、平台模型和计费范围查看全局费用、Token、调用次数及排名;用户与权限组筛选互斥 // @Tags admin // @Accept json // @Produce json @@ -495,11 +495,15 @@ func (h *Handler) ListUsageLogs(c *gin.Context) { // @Param start_date query string false "开始日期(YYYY-MM-DD),默认近30天" // @Param end_date query string false "结束日期(YYYY-MM-DD,包含当日)" // @Param user_id query int false "用户ID" +// @Param permission_group_id query int false "权限组ID,与 user_id 互斥" // @Param platform_model_name query string false "平台模型名" // @Param billing_scope query string false "计费范围:all/free/billable" -// @Param rank_by query string false "排名指标:cost/tokens/calls" +// @Param section query string false "返回范围:all/models/users" +// @Param model_rank_by query string false "模型排名指标:cost/tokens/calls" +// @Param user_rank_by query string false "用户排名指标:cost/tokens/calls" // @Success 200 {object} UsageStatisticsResponseDoc // @Failure 400 {object} ErrorDoc +// @Failure 404 {object} ErrorDoc // @Failure 500 {object} ErrorDoc // @Router /admin/usage-statistics [get] // GetUsageStatistics 查询管理员全局用量统计。 @@ -508,6 +512,10 @@ func (h *Handler) GetUsageStatistics(c *gin.Context) { if !ok { return } + permissionGroupID, ok := parseOptionalUintQuery(c, "permission_group_id") + if !ok { + return + } startDateText := strings.TrimSpace(c.Query("start_date")) endDateText := strings.TrimSpace(c.Query("end_date")) @@ -516,20 +524,20 @@ func (h *Handler) GetUsageStatistics(c *gin.Context) { startDate := endDate.AddDate(0, 0, -29) if startDateText != "" || endDateText != "" { if startDateText == "" || endDateText == "" { - response.Error(c, http.StatusBadRequest, "start_date and end_date must be provided together") + response.ErrorWithCode(c, http.StatusBadRequest, "usage_statistics.invalid_date_range", "start_date and end_date must be provided together") return } parsedStartDate, startErr := time.Parse("2006-01-02", startDateText) parsedEndDate, endErr := time.Parse("2006-01-02", endDateText) if startErr != nil || endErr != nil { - response.Error(c, http.StatusBadRequest, "invalid usage statistics date range") + response.ErrorWithCode(c, http.StatusBadRequest, "usage_statistics.invalid_date_range", "invalid usage statistics date range") return } startDate = parsedStartDate endDate = parsedEndDate } if endDate.Before(startDate) || int(endDate.Sub(startDate).Hours()/24)+1 > 366 { - response.Error(c, http.StatusBadRequest, "invalid usage statistics date range") + response.ErrorWithCode(c, http.StatusBadRequest, "usage_statistics.invalid_date_range", "invalid usage statistics date range") return } @@ -538,15 +546,31 @@ func (h *Handler) GetUsageStatistics(c *gin.Context) { billingScope = "all" } if billingScope != "all" && billingScope != "free" && billingScope != "billable" { - response.Error(c, http.StatusBadRequest, "invalid billing_scope") + response.ErrorWithCode(c, http.StatusBadRequest, "usage_statistics.invalid_billing_scope", "invalid billing_scope") + return + } + section := strings.TrimSpace(c.Query("section")) + if section == "" { + section = "all" + } + if section != "all" && section != "models" && section != "users" { + response.ErrorWithCode(c, http.StatusBadRequest, "usage_statistics.invalid_section", "invalid section") return } - rankBy := strings.TrimSpace(c.Query("rank_by")) - if rankBy == "" { - rankBy = "cost" + modelRankBy := strings.TrimSpace(c.Query("model_rank_by")) + if modelRankBy == "" { + modelRankBy = "cost" } - if rankBy != "cost" && rankBy != "tokens" && rankBy != "calls" { - response.Error(c, http.StatusBadRequest, "invalid rank_by") + if modelRankBy != "cost" && modelRankBy != "tokens" && modelRankBy != "calls" { + response.ErrorWithCode(c, http.StatusBadRequest, "usage_statistics.invalid_rank_by", "invalid model_rank_by") + return + } + userRankBy := strings.TrimSpace(c.Query("user_rank_by")) + if userRankBy == "" { + userRankBy = "cost" + } + if userRankBy != "cost" && userRankBy != "tokens" && userRankBy != "calls" { + response.ErrorWithCode(c, http.StatusBadRequest, "usage_statistics.invalid_rank_by", "invalid user_rank_by") return } @@ -554,12 +578,22 @@ func (h *Handler) GetUsageStatistics(c *gin.Context) { StartDate: startDate, EndDate: endDate, UserID: userID, + PermissionGroupID: permissionGroupID, PlatformModelName: c.Query("platform_model_name"), BillingScope: billingScope, - RankBy: rankBy, + Section: section, + ModelRankBy: modelRankBy, + UserRankBy: userRankBy, }) if err != nil { - response.Error(c, http.StatusInternalServerError, "get usage statistics failed") + switch { + case errors.Is(err, appbilling.ErrInvalidUsageStatisticsSubject): + response.ErrorWithCode(c, http.StatusBadRequest, "usage_statistics.subject_conflict", err.Error()) + case errors.Is(err, appadmin.ErrPermissionGroupNotFound): + response.ErrorFrom(c, http.StatusNotFound, err) + default: + response.Error(c, http.StatusInternalServerError, "get usage statistics failed") + } return } userIDs := make([]uint, 0, len(statistics.TopUsers)) @@ -567,7 +601,7 @@ func (h *Handler) GetUsageStatistics(c *gin.Context) { userIDs = append(userIDs, rankedUser.UserID) } userLabels := h.service.ResolveUserLabels(c.Request.Context(), userIDs) - response.Success(c, toUsageStatisticsResponse(statistics, startDate, endDate, userLabels)) + response.Success(c, toUsageStatisticsResponse(statistics, startDate, endDate, section, userLabels)) } // ListPaymentOrders godoc diff --git a/backend/internal/transport/http/admin/handler_security_test.go b/backend/internal/transport/http/admin/handler_security_test.go index 12f6544e..dfdc917c 100644 --- a/backend/internal/transport/http/admin/handler_security_test.go +++ b/backend/internal/transport/http/admin/handler_security_test.go @@ -10,7 +10,9 @@ import ( appadmin "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/admin" auditapp "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/audit" + appbilling "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/billing" domainaudit "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/audit" + domainbilling "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/billing" domainuser "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/user" "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/repository" "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/transport/http/middleware" @@ -47,6 +49,79 @@ func TestPatchUserReturnsForbiddenWhenAdminManagesSuperAdmin(t *testing.T) { } } +func TestGetUsageStatisticsRejectsConflictingSubjectFilters(t *testing.T) { + gin.SetMode(gin.TestMode) + + service := appadmin.NewService(&handlerUserServiceFake{}, handlerAuditServiceFake{}) + service.SetUsageStatisticsService(handlerUsageStatisticsServiceFake{}) + handler := NewHandler(service) + router := gin.New() + router.GET("/admin/usage-statistics", handler.GetUsageStatistics) + + request := httptest.NewRequest(http.MethodGet, "/admin/usage-statistics?user_id=1&permission_group_id=2", nil) + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, request) + + if recorder.Code != http.StatusBadRequest { + t.Fatalf("expected bad request, got status=%d body=%s", recorder.Code, recorder.Body.String()) + } + if !strings.Contains(recorder.Body.String(), "usage_statistics.subject_conflict") { + t.Fatalf("expected stable subject conflict error code, got body=%s", recorder.Body.String()) + } +} + +func TestGetUsageStatisticsResolvesRankingMetrics(t *testing.T) { + tests := []struct { + name string + query string + wantSection string + wantModelRankBy string + wantUserRankBy string + }{ + { + name: "default ranking metrics", + wantSection: "all", + wantModelRankBy: "cost", + wantUserRankBy: "cost", + }, + { + name: "independent ranking metrics", + query: "?section=models&model_rank_by=tokens&user_rank_by=calls", + wantSection: "models", + wantModelRankBy: "tokens", + wantUserRankBy: "calls", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gin.SetMode(gin.TestMode) + capturedFilter := appbilling.UsageStatisticsFilter{} + service := appadmin.NewService(&handlerUserServiceFake{}, handlerAuditServiceFake{}) + service.SetUsageStatisticsService(handlerUsageStatisticsCaptureFake{filter: &capturedFilter}) + handler := NewHandler(service) + router := gin.New() + router.GET("/admin/usage-statistics", handler.GetUsageStatistics) + + request := httptest.NewRequest(http.MethodGet, "/admin/usage-statistics"+tt.query, nil) + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, request) + + if recorder.Code != http.StatusOK { + t.Fatalf("expected success, got status=%d body=%s", recorder.Code, recorder.Body.String()) + } + if capturedFilter.Section != tt.wantSection || + capturedFilter.ModelRankBy != tt.wantModelRankBy || + capturedFilter.UserRankBy != tt.wantUserRankBy { + t.Fatalf("unexpected ranking metrics: %+v", capturedFilter) + } + if !strings.Contains(recorder.Body.String(), `"section":"`+tt.wantSection+`"`) { + t.Fatalf("expected response section %q, got body=%s", tt.wantSection, recorder.Body.String()) + } + }) + } +} + type handlerUserServiceFake struct { users map[uint]domainuser.User } @@ -136,6 +211,21 @@ func (s *handlerUserServiceFake) ListAuthEvents(context.Context, uint, string, s type handlerAuditServiceFake struct{} +type handlerUsageStatisticsServiceFake struct{} + +func (handlerUsageStatisticsServiceFake) GetUsageStatistics(context.Context, appbilling.UsageStatisticsFilter) (domainbilling.UsageStatistics, error) { + return domainbilling.UsageStatistics{}, nil +} + +type handlerUsageStatisticsCaptureFake struct { + filter *appbilling.UsageStatisticsFilter +} + +func (f handlerUsageStatisticsCaptureFake) GetUsageStatistics(_ context.Context, filter appbilling.UsageStatisticsFilter) (domainbilling.UsageStatistics, error) { + *f.filter = filter + return domainbilling.UsageStatistics{}, nil +} + func (handlerAuditServiceFake) Write(context.Context, string, uint, string, string, string, string, string, interface{}) { } diff --git a/frontend/components/ui/chart.tsx b/frontend/components/ui/chart.tsx index 011836a0..013672b2 100644 --- a/frontend/components/ui/chart.tsx +++ b/frontend/components/ui/chart.tsx @@ -4,6 +4,7 @@ import * as React from "react" import * as RechartsPrimitive from "recharts" import type { TooltipValueType } from "recharts" +import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" // Format: { THEME_NAME: CSS_SELECTOR } @@ -27,6 +28,13 @@ type ChartContextProps = { config: ChartConfig } +type ChartInteractiveLegendItem = { + id: string + label: string + title?: string + color: string +} + const ChartContext = React.createContext(null) function useChart() { @@ -327,6 +335,42 @@ function ChartLegendContent({ ) } +function ChartInteractiveLegend({ + items, + hiddenSeries, + onToggle, +}: { + items: ChartInteractiveLegendItem[] + hiddenSeries: ReadonlySet + onToggle: (id: string) => void +}) { + return ( +
+ {items.map((item) => { + const visible = !hiddenSeries.has(item.id) + return ( + + ) + })} +
+ ) +} + // Helper to extract item config from a payload. function getPayloadConfigFromPayload( config: ChartConfig, @@ -370,5 +414,7 @@ export { ChartTooltipContent, ChartLegend, ChartLegendContent, + ChartInteractiveLegend, ChartStyle, } +export type { ChartInteractiveLegendItem } diff --git a/frontend/features/admin/api/statistics.ts b/frontend/features/admin/api/statistics.ts index 4550a02a..def358a9 100644 --- a/frontend/features/admin/api/statistics.ts +++ b/frontend/features/admin/api/statistics.ts @@ -2,6 +2,7 @@ import { authedRequest } from "@/shared/api/authed-client"; export type AdminUsageStatisticsRankBy = "cost" | "tokens" | "calls"; export type AdminUsageStatisticsBillingScope = "all" | "free" | "billable"; +export type AdminUsageStatisticsSection = "all" | "models" | "users"; export type AdminUsageStatisticsMetricsDTO = { recordCount: number; @@ -35,6 +36,7 @@ export type AdminUsageStatisticsUserRankDTO = AdminUsageStatisticsMetricsDTO & { }; export type AdminUsageStatisticsData = { + section: AdminUsageStatisticsSection; range: { startDate: string; endDate: string; @@ -46,14 +48,20 @@ export type AdminUsageStatisticsData = { topUsers: AdminUsageStatisticsUserRankDTO[]; }; +type AdminUsageStatisticsSubjectOptions = + | { userID?: never; permissionGroupID?: never } + | { userID: number; permissionGroupID?: never } + | { userID?: never; permissionGroupID: number }; + export type GetAdminUsageStatisticsOptions = { startDate: string; endDate: string; - userID?: number; platformModelName?: string; billingScope?: AdminUsageStatisticsBillingScope; - rankBy?: AdminUsageStatisticsRankBy; -}; + section?: AdminUsageStatisticsSection; + modelRankBy?: AdminUsageStatisticsRankBy; + userRankBy?: AdminUsageStatisticsRankBy; +} & AdminUsageStatisticsSubjectOptions; export async function getAdminUsageStatistics( accessToken: string, @@ -63,11 +71,16 @@ export async function getAdminUsageStatistics( start_date: options.startDate, end_date: options.endDate, billing_scope: options.billingScope ?? "all", - rank_by: options.rankBy ?? "cost", + section: options.section ?? "all", + model_rank_by: options.modelRankBy ?? "cost", + user_rank_by: options.userRankBy ?? "cost", }); if (options.userID && options.userID > 0) { params.set("user_id", String(options.userID)); } + if (options.permissionGroupID && options.permissionGroupID > 0) { + params.set("permission_group_id", String(options.permissionGroupID)); + } if (options.platformModelName?.trim()) { params.set("platform_model_name", options.platformModelName.trim()); } diff --git a/frontend/features/admin/components/admin-date-range-filter.tsx b/frontend/features/admin/components/admin-date-range-filter.tsx index d600a94b..ad1f53e0 100644 --- a/frontend/features/admin/components/admin-date-range-filter.tsx +++ b/frontend/features/admin/components/admin-date-range-filter.tsx @@ -1,10 +1,10 @@ "use client"; -import { format } from "date-fns"; +import { differenceInCalendarDays, format } from "date-fns"; import { enUS, zhCN } from "date-fns/locale"; import { CalendarIcon } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; -import type { DateRange } from "react-day-picker"; +import type { DateRange, Matcher } from "react-day-picker"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; @@ -20,6 +20,7 @@ type AdminDateRangeFilterProps = { onFromChange: (value: string) => void; onToChange: (value: string) => void; disabled?: boolean; + maxRangeDays?: number; placeholder?: string; triggerClassName?: string; }; @@ -43,6 +44,7 @@ export function AdminDateRangeFilter({ onFromChange, onToChange, disabled = false, + maxRangeDays, placeholder, triggerClassName, }: AdminDateRangeFilterProps) { @@ -55,6 +57,10 @@ export function AdminDateRangeFilter({ to: parseDateValue(toValue), } : undefined; + const rangeStart = selectedRange?.from; + const disabledDates: Matcher | undefined = maxRangeDays && rangeStart && !selectedRange?.to + ? (date) => Math.abs(differenceInCalendarDays(date, rangeStart)) >= maxRangeDays + : undefined; const triggerLabel = selectedRange?.from ? selectedRange.to ? `${format(selectedRange.from, "yyyy-MM-dd")} - ${format(selectedRange.to, "yyyy-MM-dd")}` @@ -85,6 +91,9 @@ export function AdminDateRangeFilter({ mode="range" defaultMonth={selectedRange?.from} selected={selectedRange} + disabled={disabledDates} + max={maxRangeDays ? maxRangeDays - 1 : undefined} + resetOnSelect={Boolean(maxRangeDays)} locale={locale === "zh-CN" ? zhCN : enUS} onSelect={(range) => { onFromChange(range?.from ? format(range.from, "yyyy-MM-dd") : ""); diff --git a/frontend/features/admin/components/sections/statistics/admin-statistics-billing-filter.tsx b/frontend/features/admin/components/sections/statistics/admin-statistics-billing-filter.tsx new file mode 100644 index 00000000..f1411d1c --- /dev/null +++ b/frontend/features/admin/components/sections/statistics/admin-statistics-billing-filter.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Check, CircleDollarSign } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import type { AdminUsageStatisticsBillingScope } from "@/features/admin/api"; +import { cn } from "@/lib/utils"; + +type BillingFilterOption = { + value: AdminUsageStatisticsBillingScope; + label: string; +}; + +export function AdminStatisticsBillingFilter({ + value, + options, + label, + triggerClassName, + disabled = false, + onChange, +}: { + value: AdminUsageStatisticsBillingScope; + options: BillingFilterOption[]; + label: string; + triggerClassName?: string; + disabled?: boolean; + onChange: (value: AdminUsageStatisticsBillingScope) => void; +}) { + const selectedLabel = options.find((option) => option.value === value)?.label ?? ""; + + return ( + + + + + + {options.map((option) => ( + onChange(option.value)}> + {option.label} + {value === option.value ? : null} + + ))} + + + ); +} diff --git a/frontend/features/admin/components/sections/statistics/admin-statistics-charts.tsx b/frontend/features/admin/components/sections/statistics/admin-statistics-charts.tsx index 62697dcf..f21ae98f 100644 --- a/frontend/features/admin/components/sections/statistics/admin-statistics-charts.tsx +++ b/frontend/features/admin/components/sections/statistics/admin-statistics-charts.tsx @@ -2,10 +2,15 @@ import * as React from "react"; import { useLocale, useTranslations } from "next-intl"; -import { Bar, BarChart, CartesianGrid, Rectangle, XAxis, YAxis } from "recharts"; -import type { BarShapeProps, RectangleProps } from "recharts"; +import { Area, Bar, BarChart, CartesianGrid, ComposedChart, Line, XAxis, YAxis } from "recharts"; -import { ChartContainer, ChartTooltip, type ChartConfig } from "@/components/ui/chart"; +import { + ChartContainer, + ChartInteractiveLegend, + ChartTooltip, + type ChartConfig, + type ChartInteractiveLegendItem, +} from "@/components/ui/chart"; import { Skeleton } from "@/components/ui/skeleton"; import type { AdminUsageStatisticsMetricsDTO, @@ -16,17 +21,9 @@ import type { } from "@/features/admin/api"; import { formatBillingDisplayAmountFromUSD, - formatBillingDisplayCompactAmountFromUSD, type BillingDisplayOptions, } from "@/shared/lib/billing-display"; -const trendChartConfig = { - metricValue: { - label: "Value", - color: "var(--chart-1)", - }, -} satisfies ChartConfig; - const STACK_COLORS = [ "#2563eb", "#06b6d4", @@ -40,15 +37,12 @@ const STACK_COLORS = [ "#ef4444", ]; +const CHART_ANIMATION_DURATION_MS = 240; + function displayCost(value: number, billingDisplay: BillingDisplayOptions): string { - if (!Number.isFinite(value) || value <= 0) { - return formatBillingDisplayAmountFromUSD(0, billingDisplay, { maximumFractionDigits: 4 }); - } - const compact = formatBillingDisplayCompactAmountFromUSD(value, billingDisplay, 0.0001); - if (compact.startsWith("< ")) return compact; return formatBillingDisplayAmountFromUSD(value, billingDisplay, { - minimumFractionDigits: 0, - maximumFractionDigits: 4, + minimumFractionDigits: 2, + maximumFractionDigits: 2, }); } @@ -90,7 +84,10 @@ function chartAxisValue( ): string { if (rankBy !== "cost") return compactNumber(value, locale); const symbol = billingDisplay.currency === "CNY" && Number(billingDisplay.usdToCnyRate) > 0 ? "¥" : "$"; - return `${symbol}${compactNumber(value, locale)}`; + return `${symbol}${value.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; } function formatLatency(value: number, locale: string): string { @@ -106,9 +103,29 @@ function parsePeriodDate(value: string): Date | null { return Number.isNaN(date.getTime()) ? null : date; } -function periodLabels(value: string, granularity: string, locale: string): { short: string; full: string } { +function periodLabels( + value: string, + granularity: string, + locale: string, + rangeStartDate?: string, + rangeEndDate?: string, +): { short: string; full: string } { const date = parsePeriodDate(value); if (!date) return { short: "-", full: "-" }; + if (granularity === "week") { + const selectedStart = rangeStartDate ? parsePeriodDate(rangeStartDate) : null; + const selectedEnd = rangeEndDate ? parsePeriodDate(rangeEndDate) : null; + const weekStart = selectedStart && selectedStart > date ? selectedStart : date; + const weekEnd = new Date(date); + weekEnd.setDate(weekEnd.getDate() + 6); + const visibleWeekEnd = selectedEnd && selectedEnd < weekEnd ? selectedEnd : weekEnd; + const shortFormatter = new Intl.DateTimeFormat(locale, { month: "2-digit", day: "2-digit" }); + const fullFormatter = new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit" }); + return { + short: shortFormatter.format(weekStart), + full: `${fullFormatter.format(weekStart)} - ${fullFormatter.format(visibleWeekEnd)}`, + }; + } if (granularity === "month") { return { short: new Intl.DateTimeFormat(locale, { month: "short" }).format(date), @@ -141,8 +158,8 @@ function StatisticsTooltipContent({

{item.fullLabel || label}

- +
@@ -173,63 +190,156 @@ function ChartLoadingSkeleton({ horizontal = false }: { horizontal?: boolean }) ); } -export function StatisticsTrendChart({ +function useHiddenChartSeries() { + const [hiddenSeries, setHiddenSeries] = React.useState>(() => new Set()); + const toggleSeries = React.useCallback((id: string) => { + setHiddenSeries((current) => { + const next = new Set(current); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); + return { hiddenSeries, toggleSeries }; +} + +export const StatisticsTrendChart = React.memo(function StatisticsTrendChart({ items, granularity, + rangeStartDate, + rangeEndDate, rankBy, billingDisplay, loading, }: { items: AdminUsageStatisticsTrendDTO[]; granularity: string; + rangeStartDate?: string; + rangeEndDate?: string; rankBy: AdminUsageStatisticsRankBy; billingDisplay: BillingDisplayOptions; loading: boolean; }) { const t = useTranslations("adminStatistics"); const locale = useLocale(); + const { hiddenSeries, toggleSeries } = useHiddenChartSeries(); + const chartConfig = React.useMemo(() => ({ + metricValue: { + label: t(`rankBy.${rankBy}`), + color: "var(--chart-1)", + }, + avgLatencyMS: { + label: t("metrics.latency"), + color: "var(--chart-2)", + }, + }), [rankBy, t]); const data = React.useMemo( () => items.map((item) => { - const labels = periodLabels(item.periodStart, granularity, locale); + const labels = periodLabels(item.periodStart, granularity, locale, rangeStartDate, rangeEndDate); return { label: labels.short, fullLabel: labels.full, metricValue: chartMetricValue(item, rankBy, billingDisplay), + avgLatencyMS: item.avgLatencyMS, metrics: item, }; }), - [billingDisplay, granularity, items, locale, rankBy], + [billingDisplay, granularity, items, locale, rangeEndDate, rangeStartDate, rankBy], ); const hasData = items.some((item) => item.recordCount > 0 || item.totalTokens > 0 || item.callCount > 0 || item.billedUSD > 0); + const legendItems = React.useMemo(() => [ + { id: "metricValue", label: t(`rankBy.${rankBy}`), color: "var(--chart-1)" }, + { id: "avgLatencyMS", label: t("metrics.latency"), color: "var(--chart-2)" }, + ], [rankBy, t]); if (loading) return ; if (!loading && !hasData) { - return
{t("empty")}
; + return
{t("empty")}
; } return ( - - +
+ + event.preventDefault()} + > + + + + + + - + chartAxisValue(value, rankBy, billingDisplay, locale)} /> + formatLatency(value, locale)} + /> } /> - - - + + + + + +
); -} +}); type StatisticsStackedSeries = { + id: string; key: string; label: string; fullLabel: string; @@ -238,11 +348,13 @@ type StatisticsStackedSeries = { }; type StatisticsStackedSegment = { + id: string; key: string; label: string; fullLabel: string; color: string; rawValue: number; + chartValue: number; }; type StatisticsStackedChartPoint = Record & { @@ -271,26 +383,24 @@ function StatisticsStackedTooltipContent({ payload, rankBy, billingDisplay, + hiddenSeries, }: { active?: boolean; payload?: Array<{ payload?: StatisticsStackedChartPoint }>; rankBy: AdminUsageStatisticsRankBy; billingDisplay: BillingDisplayOptions; + hiddenSeries: ReadonlySet; }) { const t = useTranslations("adminStatistics"); const locale = useLocale(); const item = payload?.[0]?.payload; if (!active || !item) return null; - const segments = item.segments.filter((segment) => segment.rawValue > 0); + const segments = item.segments.filter((segment) => segment.rawValue > 0 && !hiddenSeries.has(segment.id)); if (segments.length === 0) return null; return (

{item.fullLabel}

- -
+
{segments.map((segment) => (
@@ -303,39 +413,26 @@ function StatisticsStackedTooltipContent({
))}
+
+ total + segment.rawValue, 0), + rankBy, + billingDisplay, + locale, + )} + /> +
); } -function isTopStackSegment( - point: StatisticsStackedChartPoint, - series: StatisticsStackedSeries[], - seriesIndex: number, -): boolean { - if (Number(point[series[seriesIndex]?.key] ?? 0) <= 0) return false; - for (let index = seriesIndex + 1; index < series.length; index += 1) { - if (Number(point[series[index]?.key] ?? 0) > 0) return false; - } - return true; -} - -function StatisticsStackedBarShape({ - series, - seriesIndex, - props, -}: { - series: StatisticsStackedSeries[]; - seriesIndex: number; - props: BarShapeProps; -}) { - const point: StatisticsStackedChartPoint | undefined = props.payload; - const radius: RectangleProps["radius"] = point && isTopStackSegment(point, series, seriesIndex) ? [4, 4, 0, 0] : 0; - return ; -} - function StatisticsStackedTrendChart({ periods, granularity, + rangeStartDate, + rangeEndDate, series, rankBy, billingDisplay, @@ -343,6 +440,8 @@ function StatisticsStackedTrendChart({ }: { periods: AdminUsageStatisticsTrendDTO[]; granularity: string; + rangeStartDate?: string; + rangeEndDate?: string; series: StatisticsStackedSeries[]; rankBy: AdminUsageStatisticsRankBy; billingDisplay: BillingDisplayOptions; @@ -350,6 +449,7 @@ function StatisticsStackedTrendChart({ }) { const t = useTranslations("adminStatistics"); const locale = useLocale(); + const { hiddenSeries, toggleSeries } = useHiddenChartSeries(); const config = React.useMemo( () => Object.fromEntries(series.map((item) => [item.key, { label: item.label, color: item.color }])), [series], @@ -359,11 +459,12 @@ function StatisticsStackedTrendChart({ const metricsBySeries = series.map((item) => new Map(item.trend.map((point) => [statisticsPeriodKey(point.periodStart), point])), ); - return periods.map((period) => { - const labels = periodLabels(period.periodStart, granularity, locale); + const points = periods.map((period) => { + const labels = periodLabels(period.periodStart, granularity, locale, rangeStartDate, rangeEndDate); const segments = series.map((item, index) => { const metrics = metricsBySeries[index]?.get(statisticsPeriodKey(period.periodStart)); return { + id: item.id, key: item.key, label: item.label, fullLabel: item.fullLabel, @@ -381,20 +482,41 @@ function StatisticsStackedTrendChart({ for (const segment of segments) point[segment.key] = segment.chartValue; return point; }); + return points; }, - [billingDisplay, granularity, locale, periods, rankBy, series], + [billingDisplay, granularity, locale, periods, rangeEndDate, rangeStartDate, rankBy, series], ); const hasData = data.some((point) => point.totalRawValue > 0); + const legendItems = React.useMemo( + () => series.map((item) => ({ id: item.id, label: item.label, title: item.fullLabel, color: item.color })), + [series], + ); + const topVisibleSeriesID = React.useMemo( + () => [...series].reverse().find((item) => !hiddenSeries.has(item.id))?.id, + [hiddenSeries, series], + ); if (loading) return ; if (!loading && !hasData) { - return
{t("empty")}
; + return
{t("empty")}
; } return (
- + event.preventDefault()} + > - + } + content={ + + } /> - {series.map((item, index) => ( + {series.map((item) => ( ( - - )} + isAnimationActive + animationDuration={CHART_ANIMATION_DURATION_MS} + animationEasing="ease-out" + hide={hiddenSeries.has(item.id)} + radius={item.id === topVisibleSeriesID ? [4, 4, 0, 0] : 0} /> ))} -
- {series.map((item) => ( - - - {item.label} - - ))} -
+
); } -export function StatisticsModelRankingChart({ +export const StatisticsModelRankingChart = React.memo(function StatisticsModelRankingChart({ items, periods, granularity, + rangeStartDate, + rangeEndDate, rankBy, billingDisplay, loading, @@ -443,6 +568,8 @@ export function StatisticsModelRankingChart({ items: AdminUsageStatisticsModelRankDTO[]; periods: AdminUsageStatisticsTrendDTO[]; granularity: string; + rangeStartDate?: string; + rangeEndDate?: string; rankBy: AdminUsageStatisticsRankBy; billingDisplay: BillingDisplayOptions; loading: boolean; @@ -451,6 +578,7 @@ export function StatisticsModelRankingChart({ const series = React.useMemo( () => items.map((item, index) => ({ + id: item.platformModelName.trim() || `unknown-model-${index}`, key: `model_${index}`, label: item.platformModelName.trim() || t("unknownModel"), fullLabel: item.platformModelName.trim() || t("unknownModel"), @@ -463,18 +591,22 @@ export function StatisticsModelRankingChart({ ); -} +}); -export function StatisticsUserRankingChart({ +export const StatisticsUserRankingChart = React.memo(function StatisticsUserRankingChart({ items, periods, granularity, + rangeStartDate, + rangeEndDate, rankBy, billingDisplay, loading, @@ -482,6 +614,8 @@ export function StatisticsUserRankingChart({ items: AdminUsageStatisticsUserRankDTO[]; periods: AdminUsageStatisticsTrendDTO[]; granularity: string; + rangeStartDate?: string; + rangeEndDate?: string; rankBy: AdminUsageStatisticsRankBy; billingDisplay: BillingDisplayOptions; loading: boolean; @@ -492,6 +626,7 @@ export function StatisticsUserRankingChart({ items.map((item, index) => { const label = item.userLabel.trim() || item.userDisplayName.trim() || item.username.trim() || t("unknownUser", { id: item.userID }); return { + id: String(item.userID), key: `user_${index}`, label, fullLabel: `${label} (#${item.userID})`, @@ -505,13 +640,15 @@ export function StatisticsUserRankingChart({ ); -} +}); export function formatStatisticsCost(value: number, billingDisplay: BillingDisplayOptions): string { return displayCost(value, billingDisplay); diff --git a/frontend/features/admin/components/sections/statistics/admin-statistics-model-filter.tsx b/frontend/features/admin/components/sections/statistics/admin-statistics-model-filter.tsx new file mode 100644 index 00000000..89ccc908 --- /dev/null +++ b/frontend/features/admin/components/sections/statistics/admin-statistics-model-filter.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { Bot, Sparkles } from "lucide-react"; +import { useTranslations } from "next-intl"; + +import { Button } from "@/components/ui/button"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "@/components/ui/combobox"; +import { cn } from "@/lib/utils"; +import type { ModelSelectOption } from "@/shared/components/model-select"; +import { ModelOptionIcon } from "@/shared/components/model-option-icon"; + +export function AdminStatisticsModelFilter({ + value, + fallbackValue, + options, + label, + triggerClassName, + disabled = false, + onChange, +}: { + value: string; + fallbackValue: string; + options: ModelSelectOption[]; + label: string; + triggerClassName?: string; + disabled?: boolean; + onChange: (value: string) => void; +}) { + const t = useTranslations("common.modelSelect"); + const selectedOption = options.find((option) => option.value === value) ?? options.find((option) => option.value === fallbackValue); + + return ( + option.label} + isItemEqualToValue={(option, selected) => option.value === selected.value} + onValueChange={(option) => onChange(option?.value ?? fallbackValue)} + > + + + {label} + + {selectedOption?.label ?? t("empty")} + + + } + /> + + + {t("empty")} + + {(option: ModelSelectOption) => ( + + {option.value === fallbackValue ? ( + + + + ) : ( + + )} + + {option.label} + + + )} + + + + ); +} diff --git a/frontend/features/admin/components/sections/statistics/admin-statistics-subject-filter.tsx b/frontend/features/admin/components/sections/statistics/admin-statistics-subject-filter.tsx new file mode 100644 index 00000000..2a545815 --- /dev/null +++ b/frontend/features/admin/components/sections/statistics/admin-statistics-subject-filter.tsx @@ -0,0 +1,284 @@ +"use client"; + +import * as React from "react"; +import { LoaderCircle, ShieldCheck, UsersRound } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "@/components/ui/combobox"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { listAdminUsers, type PermissionGroup } from "@/features/admin/api"; +import type { AdminStatisticsSubject } from "@/features/admin/hooks/use-admin-statistics"; +import { resolveAdminErrorMessage } from "@/features/admin/utils/admin-error"; +import { cn } from "@/lib/utils"; +import type { UserDTO } from "@/shared/api/auth.types"; +import { resolveAccessToken } from "@/shared/auth/resolve-access-token"; +import { resolveAvatarImageSrc } from "@/shared/lib/avatar"; + +type SubjectMode = "user" | "permission-group"; + +type SubjectFilterOption = + | { key: "all"; type: "all" } + | { key: string; type: "user"; user: UserDTO } + | { key: string; type: "permission-group"; permissionGroup: PermissionGroup }; + +const ALL_SUBJECT_OPTION: SubjectFilterOption = { key: "all", type: "all" }; + +function userPrimaryLabel(user: UserDTO): string { + return user.displayName.trim() || user.username.trim() || `#${user.id}`; +} + +function userSecondaryLabel(user: UserDTO): string { + const values = [user.username.trim(), user.email.trim(), `#${user.id}`].filter(Boolean); + return values.join(" · "); +} + +function subjectLabel(subject: AdminStatisticsSubject, allUsersLabel: string): string { + if (subject.type === "user") return userPrimaryLabel(subject.user); + if (subject.type === "permission-group") return subject.permissionGroup.name; + return allUsersLabel; +} + +export function AdminStatisticsSubjectFilter({ + value, + permissionGroups, + onChange, + disabled = false, + label, + triggerClassName, +}: { + value: AdminStatisticsSubject; + permissionGroups: PermissionGroup[]; + onChange: (subject: AdminStatisticsSubject) => void; + disabled?: boolean; + label: string; + triggerClassName?: string; +}) { + const t = useTranslations("adminStatistics.filters.subjectSelect"); + const [open, setOpen] = React.useState(false); + const [mode, setMode] = React.useState("user"); + const [query, setQuery] = React.useState(""); + const [debouncedQuery, setDebouncedQuery] = React.useState(""); + const [users, setUsers] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const requestSequenceRef = React.useRef(0); + + React.useEffect(() => { + const timer = window.setTimeout(() => setDebouncedQuery(query.trim()), 250); + return () => window.clearTimeout(timer); + }, [query]); + + React.useEffect(() => { + if (!open || mode !== "user") return; + const requestSequence = requestSequenceRef.current + 1; + requestSequenceRef.current = requestSequence; + setLoading(true); + void (async () => { + try { + const token = await resolveAccessToken(); + if (!token) return; + const result = await listAdminUsers(token, { + page: 1, + pageSize: 20, + query: debouncedQuery, + }); + if (requestSequence === requestSequenceRef.current) { + setUsers(result.results); + } + } catch (error) { + if (requestSequence === requestSequenceRef.current) { + toast.error(t("loadFailed"), { description: resolveAdminErrorMessage(error) }); + } + } finally { + if (requestSequence === requestSequenceRef.current) setLoading(false); + } + })(); + }, [debouncedQuery, mode, open, t]); + + const options = React.useMemo(() => { + if (mode === "permission-group") { + const normalizedQuery = query.trim().toLocaleLowerCase(); + const groupOptions = permissionGroups + .filter((group) => { + if (!normalizedQuery) return true; + return `${group.name} ${group.description}`.toLocaleLowerCase().includes(normalizedQuery); + }) + .map((permissionGroup) => ({ + key: `permission-group:${permissionGroup.id}`, + type: "permission-group" as const, + permissionGroup, + })); + return normalizedQuery ? groupOptions : [ALL_SUBJECT_OPTION, ...groupOptions]; + } + + const userOptions: SubjectFilterOption[] = users.map((user) => ({ + key: `user:${user.id}`, + type: "user", + user, + })); + if (query.trim()) return userOptions; + if (value.type === "user" && !userOptions.some((option) => option.type === "user" && option.user.id === value.user.id)) { + userOptions.unshift({ key: `user:${value.user.id}`, type: "user", user: value.user }); + } + return [ALL_SUBJECT_OPTION, ...userOptions]; + }, [mode, permissionGroups, query, users, value]); + + const selectedOption: SubjectFilterOption = value.type === "user" + ? { key: `user:${value.user.id}`, type: "user", user: value.user } + : value.type === "permission-group" + ? { + key: `permission-group:${value.permissionGroup.id}`, + type: "permission-group", + permissionGroup: value.permissionGroup, + } + : ALL_SUBJECT_OPTION; + + return ( + { + if (option.type === "user") return userSecondaryLabel(option.user); + if (option.type === "permission-group") return option.permissionGroup.name; + return t("allUsers"); + }} + isItemEqualToValue={(option, selected) => option.key === selected.key} + onInputValueChange={setQuery} + onOpenChange={(nextOpen) => { + setOpen(nextOpen); + if (nextOpen) { + setMode(value.type === "permission-group" ? "permission-group" : "user"); + } else { + setQuery(""); + } + }} + onValueChange={(option) => { + if (!option || option.type === "all") { + onChange({ type: "all" }); + } else if (option.type === "user") { + onChange({ type: "user", user: option.user }); + } else { + onChange({ type: "permission-group", permissionGroup: option.permissionGroup }); + } + setQuery(""); + setOpen(false); + }} + > + + + {label} + + {subjectLabel(value, t("allUsers"))} + + + } + /> + + { + setMode(nextMode as SubjectMode); + setQuery(""); + }} + > + + {t("users")} + {t("permissionGroups")} + + + + {mode === "user" && loading ? ( + + ) : null} + + {mode === "user" ? t("emptyUsers") : t("emptyPermissionGroups")} + + {(option: SubjectFilterOption) => { + if (option.type === "user") { + return ( + + + + + {userPrimaryLabel(option.user).charAt(0).toUpperCase()} + + + + {userPrimaryLabel(option.user)} + {option.user.username.trim() ? ( + (@{option.user.username.trim()}) + ) : null} + + + #{option.user.id} + + + ); + } + if (option.type === "permission-group") { + return ( + + + + + + + {option.permissionGroup.name} + + {t("memberCount", { count: option.permissionGroup.userCount })} + + + ); + } + return ( + + + + + + + {t("allUsers")} + + ); + }} + + + + ); +} diff --git a/frontend/features/admin/components/sections/statistics/admin-statistics-user-filter.tsx b/frontend/features/admin/components/sections/statistics/admin-statistics-user-filter.tsx deleted file mode 100644 index a4253961..00000000 --- a/frontend/features/admin/components/sections/statistics/admin-statistics-user-filter.tsx +++ /dev/null @@ -1,149 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Check, ChevronsUpDown, LoaderCircle, UserRound, UsersRound } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { toast } from "sonner"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { listAdminUsers } from "@/features/admin/api"; -import { resolveAdminErrorMessage } from "@/features/admin/utils/admin-error"; -import { cn } from "@/lib/utils"; -import type { UserDTO } from "@/shared/api/auth.types"; -import { resolveAccessToken } from "@/shared/auth/resolve-access-token"; - -function userPrimaryLabel(user: UserDTO): string { - return user.displayName.trim() || user.username.trim() || `#${user.id}`; -} - -function userSecondaryLabel(user: UserDTO): string { - const values = [user.username.trim(), user.email.trim(), `#${user.id}`].filter(Boolean); - return values.join(" · "); -} - -export function AdminStatisticsUserFilter({ - value, - onChange, - disabled = false, -}: { - value: UserDTO | null; - onChange: (user: UserDTO | null) => void; - disabled?: boolean; -}) { - const t = useTranslations("adminStatistics.filters.userSelect"); - const [open, setOpen] = React.useState(false); - const [query, setQuery] = React.useState(""); - const [debouncedQuery, setDebouncedQuery] = React.useState(""); - const [users, setUsers] = React.useState([]); - const [loading, setLoading] = React.useState(false); - const requestSequenceRef = React.useRef(0); - - React.useEffect(() => { - const timer = window.setTimeout(() => setDebouncedQuery(query.trim()), 250); - return () => window.clearTimeout(timer); - }, [query]); - - React.useEffect(() => { - if (!open) return; - const requestSequence = requestSequenceRef.current + 1; - requestSequenceRef.current = requestSequence; - setLoading(true); - void (async () => { - try { - const token = await resolveAccessToken(); - if (!token) return; - const result = await listAdminUsers(token, { - page: 1, - pageSize: 20, - query: debouncedQuery, - }); - if (requestSequence === requestSequenceRef.current) { - setUsers(result.results); - } - } catch (error) { - if (requestSequence === requestSequenceRef.current) { - toast.error(t("loadFailed"), { description: resolveAdminErrorMessage(error) }); - } - } finally { - if (requestSequence === requestSequenceRef.current) setLoading(false); - } - })(); - }, [debouncedQuery, open, t]); - - return ( - - - - - -
- setQuery(event.target.value)} - placeholder={t("searchPlaceholder")} - className="h-8 pr-8 text-xs" - autoComplete="off" - autoCapitalize="none" - spellCheck={false} - data-1p-ignore="true" - data-form-type="other" - autoFocus - /> - {loading ? : null} -
-
- - {users.map((user) => ( - - ))} - {!loading && users.length === 0 ? ( -
{t("empty")}
- ) : null} -
-
-
- ); -} diff --git a/frontend/features/admin/components/sections/statistics/admin-statistics.tsx b/frontend/features/admin/components/sections/statistics/admin-statistics.tsx index 27107163..f427edac 100644 --- a/frontend/features/admin/components/sections/statistics/admin-statistics.tsx +++ b/frontend/features/admin/components/sections/statistics/admin-statistics.tsx @@ -1,30 +1,24 @@ "use client"; import * as React from "react"; -import { Activity, BadgeDollarSign, Braces, RefreshCw, Timer } from "lucide-react"; +import { Activity, BadgeDollarSign, Braces, CalendarDays, Check, Funnel, RefreshCw, Timer } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AdminDateRangeFilter } from "@/features/admin/components/admin-date-range-filter"; import type { - AdminUsageStatisticsBillingScope, AdminUsageStatisticsMetricsDTO, AdminUsageStatisticsRankBy, } from "@/features/admin/api"; -import { useAdminStatistics, type AdminStatisticsRangePreset } from "@/features/admin/hooks/use-admin-statistics"; +import { useAdminStatistics } from "@/features/admin/hooks/use-admin-statistics"; import { cn } from "@/lib/utils"; -import { ModelSelect } from "@/shared/components/model-select"; -import type { UserDTO } from "@/shared/api/auth.types"; -import { AdminStatisticsUserFilter } from "./admin-statistics-user-filter"; +import { AdminStatisticsBillingFilter } from "./admin-statistics-billing-filter"; +import { AdminStatisticsModelFilter } from "./admin-statistics-model-filter"; +import { AdminStatisticsSubjectFilter } from "./admin-statistics-subject-filter"; import { formatStatisticsCost, formatStatisticsCount, @@ -36,15 +30,6 @@ import { const ALL_MODELS_VALUE = "__all_models__"; -function FilterField({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) { - return ( -
- {label} - {children} -
- ); -} - function MetricCard({ label, value, @@ -57,53 +42,20 @@ function MetricCard({ loading: boolean; }) { return ( -
-
- {label} +
+
{icon} + {label}
{loading ? ( ) : ( -

{value}

+

{value}

)}
); } -function MetricSwitch({ - value, - onChange, - disabled, -}: { - value: AdminUsageStatisticsRankBy; - onChange: (value: AdminUsageStatisticsRankBy) => void; - disabled: boolean; -}) { - const t = useTranslations("adminStatistics"); - const options: AdminUsageStatisticsRankBy[] = ["cost", "tokens", "calls"]; - return ( -
- {options.map((option) => ( - - ))} -
- ); -} - function emptyMetrics(): AdminUsageStatisticsMetricsDTO { return { recordCount: 0, @@ -122,190 +74,313 @@ function emptyMetrics(): AdminUsageStatisticsMetricsDTO { export function AdminStatisticsPage() { const t = useTranslations("adminStatistics"); + const tTable = useTranslations("common.table"); const locale = useLocale(); const statistics = useAdminStatistics(); - const [selectedUser, setSelectedUser] = React.useState(null); + const [dateFilterOpen, setDateFilterOpen] = React.useState(false); + const [trendMetric, setTrendMetric] = React.useState("tokens"); const totals = statistics.statistics?.totals ?? emptyMetrics(); const trendItems = statistics.statistics?.trend ?? []; const granularity = statistics.statistics?.range.granularity ?? "day"; + const chartRangeStartDate = statistics.statistics?.range.startDate ?? statistics.startDate; + const chartRangeEndDate = statistics.statistics?.range.endDate ?? statistics.endDate; const initialLoading = statistics.loading && !statistics.statistics; const modelOptions = React.useMemo( () => [{ label: t("filters.allModels"), value: ALL_MODELS_VALUE }, ...statistics.modelOptions], [statistics.modelOptions, t], ); const rangeErrorText = statistics.rangeError ? t(`filters.rangeErrors.${statistics.rangeError}`) : ""; + const rangeLabel = statistics.rangePreset === "custom" + ? statistics.startDate && statistics.endDate + ? `${statistics.startDate} - ${statistics.endDate}` + : t("filters.custom") + : t(`filters.last${statistics.rangePreset}Days`); + const activeFilterCount = Number(statistics.subject.type !== "all") + Number(Boolean(statistics.platformModelName)) + Number(statistics.billingScope !== "all"); return ( -
-
-
+
+
+

{t("title")}

-

{t("description")}

-
- -
- -
-
- - - - - - - - { - setSelectedUser(user); - statistics.setUserID(user?.id); - }} - /> - - - statistics.setPlatformModelName(value === ALL_MODELS_VALUE ? "" : value)} - /> - - - - + + + + {t("refresh")} + +
- {rangeErrorText ?

{rangeErrorText}

: null} - + {rangeErrorText ?

{rangeErrorText}

: null} -
- } - loading={initialLoading} - /> - } - loading={initialLoading} - /> - } - loading={initialLoading} - /> - } - loading={initialLoading} - /> -
+
+ } + loading={initialLoading} + /> + } + loading={initialLoading} + /> + } + loading={initialLoading} + /> + } + loading={initialLoading} + /> +
+
- + -
-
-
-

{t("trend.title")}

-

- {statistics.statistics?.range.granularity === "month" ? t("trend.monthly") : t("trend.daily")} -

-
- +
+
+

{t("trend.title")}

+ setTrendMetric(value as AdminUsageStatisticsRankBy)} + > + + {(["tokens", "cost", "calls"] as const).map((metric) => ( + + {t(`rankBy.${metric}`)} + + ))} + +
- + -
-
-
-

{t("rankings.models")}

-

{t("rankings.top10", { metric: t(`rankBy.${statistics.rankBy}`) })}

-
+
+
+

{t("rankings.models")}

+ statistics.setModelRankingMetric(value as AdminUsageStatisticsRankBy)} + > + + {(["tokens", "cost", "calls"] as const).map((metric) => ( + + {t(`rankBy.${metric}`)} + + ))} + + +
+
-
-
-

{t("rankings.users")}

-

{t("rankings.top10", { metric: t(`rankBy.${statistics.rankBy}`) })}

-
+
+ + + +
+
+

{t("rankings.users")}

+ statistics.setUserRankingMetric(value as AdminUsageStatisticsRankBy)} + > + + {(["tokens", "cost", "calls"] as const).map((metric) => ( + + {t(`rankBy.${metric}`)} + + ))} + + +
+
diff --git a/frontend/features/admin/hooks/use-admin-statistics.ts b/frontend/features/admin/hooks/use-admin-statistics.ts index 8c5c3387..b42d019d 100644 --- a/frontend/features/admin/hooks/use-admin-statistics.ts +++ b/frontend/features/admin/hooks/use-admin-statistics.ts @@ -6,9 +6,14 @@ import { getAdminBillingConfig, getAdminUsageStatistics, listAdminLLMModels, + listPermissionGroups, type AdminUsageStatisticsBillingScope, type AdminUsageStatisticsData, + type AdminUsageStatisticsModelRankDTO, type AdminUsageStatisticsRankBy, + type AdminUsageStatisticsSection, + type AdminUsageStatisticsUserRankDTO, + type PermissionGroup, } from "@/features/admin/api"; import { listAllAdminPages } from "@/features/admin/api/shared"; import { resolveAdminErrorMessage } from "@/features/admin/utils/admin-error"; @@ -19,9 +24,14 @@ import { type BillingDisplayOptions, } from "@/shared/lib/billing-display"; import { resolveModelOptionIconUrl } from "@/shared/lib/model-option-display"; +import type { UserDTO } from "@/shared/api/auth.types"; export type AdminStatisticsRangePreset = "7" | "30" | "90" | "custom"; export type AdminStatisticsRangeError = "incomplete" | "invalid" | "tooLong" | null; +export type AdminStatisticsSubject = + | { type: "all" } + | { type: "user"; user: UserDTO } + | { type: "permission-group"; permissionGroup: PermissionGroup }; function formatLocalDate(date: Date): string { const year = date.getFullYear(); @@ -66,24 +76,49 @@ export function useAdminStatistics() { const [startDate, setStartDateState] = React.useState(initialRangeRef.current.startDate); const [endDate, setEndDateState] = React.useState(initialRangeRef.current.endDate); const [rangePreset, setRangePresetState] = React.useState("30"); - const [userID, setUserID] = React.useState(); + const [subject, setSubject] = React.useState({ type: "all" }); const [platformModelName, setPlatformModelName] = React.useState(""); const [billingScope, setBillingScope] = React.useState("all"); - const [rankBy, setRankBy] = React.useState("cost"); + const [modelRankingMetric, setModelRankingMetric] = React.useState("cost"); + const [userRankingMetric, setUserRankingMetric] = React.useState("cost"); + const [appliedModelRankingMetric, setAppliedModelRankingMetric] = React.useState("cost"); + const [appliedUserRankingMetric, setAppliedUserRankingMetric] = React.useState("cost"); const [statistics, setStatistics] = React.useState(null); const [loading, setLoading] = React.useState(true); const [reloadKey, setReloadKey] = React.useState(0); const [modelOptions, setModelOptions] = React.useState([]); + const [permissionGroups, setPermissionGroups] = React.useState([]); const [referenceLoading, setReferenceLoading] = React.useState(true); const [billingDisplay, setBillingDisplay] = React.useState({ currency: "USD", usdToCnyRate: null, }); const requestSequenceRef = React.useRef(0); + const loadedFilterKeyRef = React.useRef(""); + const appliedModelRankingMetricRef = React.useRef("cost"); + const appliedUserRankingMetricRef = React.useRef("cost"); + const modelRankingCacheRef = React.useRef(new Map()); + const userRankingCacheRef = React.useRef(new Map()); const rangeError = React.useMemo( () => validateDateRange(startDate, endDate), [endDate, startDate], ); + const statisticsFilterKey = React.useMemo( + () => JSON.stringify([ + startDate, + endDate, + platformModelName, + billingScope, + reloadKey, + subject.type, + subject.type === "user" + ? subject.user.id + : subject.type === "permission-group" + ? subject.permissionGroup.id + : 0, + ]), + [billingScope, endDate, platformModelName, reloadKey, startDate, subject], + ); React.useEffect(() => { let cancelled = false; @@ -92,9 +127,10 @@ export function useAdminStatistics() { try { const token = await resolveAccessToken(); if (!token) return; - const [configResult, modelsResult] = await Promise.allSettled([ + const [configResult, modelsResult, permissionGroupsResult] = await Promise.allSettled([ getAdminBillingConfig(token), listAllAdminPages((options) => listAdminLLMModels(token, { ...options, onlyActive: false })), + listPermissionGroups(token), ]); if (cancelled) return; if (configResult.status === "fulfilled") { @@ -121,7 +157,10 @@ export function useAdminStatistics() { ), ); } - const rejectedResult = [configResult, modelsResult].find((result) => result.status === "rejected"); + if (permissionGroupsResult.status === "fulfilled") { + setPermissionGroups(permissionGroupsResult.value); + } + const rejectedResult = [configResult, modelsResult, permissionGroupsResult].find((result) => result.status === "rejected"); if (rejectedResult?.status === "rejected") { toast.error(t("toasts.referenceLoadFailed"), { description: resolveAdminErrorMessage(rejectedResult.reason) }); } @@ -144,6 +183,45 @@ export function useAdminStatistics() { setLoading(false); return; } + const filterChanged = loadedFilterKeyRef.current !== statisticsFilterKey; + const modelRankingChanged = appliedModelRankingMetricRef.current !== modelRankingMetric; + const userRankingChanged = appliedUserRankingMetricRef.current !== userRankingMetric; + if (!filterChanged && !modelRankingChanged && !userRankingChanged) { + setLoading(false); + return; + } + let section: AdminUsageStatisticsSection = "all"; + if (!filterChanged && modelRankingChanged && !userRankingChanged) { + section = "models"; + } else if (!filterChanged && userRankingChanged && !modelRankingChanged) { + section = "users"; + } + + if (filterChanged) { + modelRankingCacheRef.current.clear(); + userRankingCacheRef.current.clear(); + } else if (section === "models") { + const cachedModels = modelRankingCacheRef.current.get(modelRankingMetric); + if (cachedModels) { + requestSequenceRef.current += 1; + setStatistics((current) => current ? { ...current, topModels: cachedModels } : current); + appliedModelRankingMetricRef.current = modelRankingMetric; + setAppliedModelRankingMetric(modelRankingMetric); + setLoading(false); + return; + } + } else if (section === "users") { + const cachedUsers = userRankingCacheRef.current.get(userRankingMetric); + if (cachedUsers) { + requestSequenceRef.current += 1; + setStatistics((current) => current ? { ...current, topUsers: cachedUsers } : current); + appliedUserRankingMetricRef.current = userRankingMetric; + setAppliedUserRankingMetric(userRankingMetric); + setLoading(false); + return; + } + } + const requestSequence = requestSequenceRef.current + 1; requestSequenceRef.current = requestSequence; setLoading(true); @@ -157,16 +235,43 @@ export function useAdminStatistics() { const data = await getAdminUsageStatistics(token, { startDate, endDate, - userID, platformModelName, billingScope, - rankBy, + section, + modelRankBy: modelRankingMetric, + userRankBy: userRankingMetric, + ...(subject.type === "user" + ? { userID: subject.user.id } + : subject.type === "permission-group" + ? { permissionGroupID: subject.permissionGroup.id } + : {}), }); if (requestSequence === requestSequenceRef.current) { - setStatistics(data); + setStatistics((current) => section === "models" && current + ? { ...current, topModels: data.topModels } + : section === "users" && current + ? { ...current, topUsers: data.topUsers } + : data); + if (section !== "users") { + modelRankingCacheRef.current.set(modelRankingMetric, data.topModels); + appliedModelRankingMetricRef.current = modelRankingMetric; + setAppliedModelRankingMetric(modelRankingMetric); + } + if (section !== "models") { + userRankingCacheRef.current.set(userRankingMetric, data.topUsers); + appliedUserRankingMetricRef.current = userRankingMetric; + setAppliedUserRankingMetric(userRankingMetric); + } + loadedFilterKeyRef.current = statisticsFilterKey; } } catch (error) { if (requestSequence === requestSequenceRef.current) { + const rollbackModelMetric = modelRankingMetric !== appliedModelRankingMetricRef.current; + const rollbackUserMetric = userRankingMetric !== appliedUserRankingMetricRef.current; + if (rollbackModelMetric || rollbackUserMetric) { + if (rollbackModelMetric) setModelRankingMetric(appliedModelRankingMetricRef.current); + if (rollbackUserMetric) setUserRankingMetric(appliedUserRankingMetricRef.current); + } toast.error(t("toasts.loadFailed"), { description: resolveAdminErrorMessage(error) }); } } finally { @@ -175,7 +280,7 @@ export function useAdminStatistics() { } } })(); - }, [billingScope, endDate, platformModelName, rangeError, rankBy, reloadKey, startDate, t, userID]); + }, [billingScope, endDate, modelRankingMetric, platformModelName, rangeError, startDate, statisticsFilterKey, subject, t, userRankingMetric]); const setRangePreset = React.useCallback((preset: AdminStatisticsRangePreset) => { setRangePresetState(preset); @@ -200,21 +305,26 @@ export function useAdminStatistics() { referenceLoading, billingDisplay, modelOptions, + permissionGroups, startDate, endDate, rangePreset, rangeError, - userID, + subject, platformModelName, billingScope, - rankBy, + modelRankingMetric, + userRankingMetric, + appliedModelRankingMetric, + appliedUserRankingMetric, setStartDate, setEndDate, setRangePreset, - setUserID, + setSubject, setPlatformModelName, setBillingScope, - setRankBy, + setModelRankingMetric, + setUserRankingMetric, refresh, }; } diff --git a/frontend/features/settings/components/sections/subscription/subscription-trend.tsx b/frontend/features/settings/components/sections/subscription/subscription-trend.tsx index 8e9f2fac..53503f17 100644 --- a/frontend/features/settings/components/sections/subscription/subscription-trend.tsx +++ b/frontend/features/settings/components/sections/subscription/subscription-trend.tsx @@ -1,13 +1,14 @@ "use client"; import * as React from "react"; -import { Bar, BarChart, CartesianGrid, Rectangle, XAxis, YAxis } from "recharts"; -import type { BarShapeProps, RectangleProps } from "recharts"; +import { Activity, BadgeDollarSign, Braces, Timer } from "lucide-react"; +import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { useTranslations } from "next-intl"; -import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; -import type { ChartConfig } from "@/components/ui/chart"; +import { ChartContainer, ChartInteractiveLegend, ChartTooltip } from "@/components/ui/chart"; +import type { ChartConfig, ChartInteractiveLegendItem } from "@/components/ui/chart"; import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useAppLocale } from "@/i18n/app-i18n-provider"; import type { BillingUsageDailyDTO, BillingUsageMonthlyDTO } from "@/shared/api/billing.types"; import { @@ -24,6 +25,11 @@ import { } from "@/features/settings/model/subscription-format"; import type { BillingDisplayOptions } from "@/shared/lib/billing-display"; +type DailyUsageChartModel = BillingUsageDailyDTO["models"][number] & { + color?: string; + modelLabel?: string; +}; + type DailyUsageChartPoint = { dayLabel: string; fullDayLabel: string; @@ -32,8 +38,8 @@ type DailyUsageChartPoint = { callCount: number; recordCount: number; avgLatencyMS: number; - models: Array; - [key: string]: string | number | Array; + models: DailyUsageChartModel[]; + [key: string]: string | number | DailyUsageChartModel[]; }; type MonthlyUsageChartPoint = { @@ -69,13 +75,44 @@ const usageTokenChartConfig = { }, } satisfies ChartConfig; -const STACK_COLORS = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"]; +const STACK_COLORS = [ + "#2563eb", + "#06b6d4", + "#f97316", + "#7c3aed", + "#22c55e", + "#eab308", + "#ec4899", + "#4f46e5", + "#14b8a6", + "#ef4444", +]; +const CHART_ANIMATION_DURATION_MS = 240; +const MAX_DAILY_MODEL_SERIES = 10; +const OTHER_MODEL_KEY = "__other_models__"; +const OTHER_MODEL_COLOR = "#64748b"; + +function useHiddenUsageSeries() { + const [hiddenSeries, setHiddenSeries] = React.useState>(() => new Set()); + const toggleSeries = React.useCallback((id: string) => { + setHiddenSeries((current) => { + const next = new Set(current); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); + return { hiddenSeries, toggleSeries }; +} -function MetricTile({ label, value }: { label: string; value: string }) { +function MetricTile({ label, value, icon }: { label: string; value: string; icon: React.ReactNode }) { return ( -
-

{label}

-

{value}

+
+
+ {icon} + {label} +
+

{value}

); } @@ -84,10 +121,26 @@ function UsageTrendMetricTiles({ stats, billingDisplay }: { stats: UsageTrendSta const t = useTranslations("settings.subscriptionPage.usageTrend.metrics"); return (
- - - - + } + /> + } + /> + } + /> + } + />
); } @@ -136,46 +189,121 @@ function calculateMonthlyTrendStats(items: BillingUsageMonthlyDTO[]): UsageTrend }; } +function aggregateDailyModels(models: BillingUsageDailyDTO["models"], modelLabel: string): DailyUsageChartModel { + const totals = models.reduce( + (result, model) => ({ + recordCount: result.recordCount + model.recordCount, + inputTokens: result.inputTokens + model.inputTokens, + cacheReadTokens: result.cacheReadTokens + model.cacheReadTokens, + cacheWriteTokens: result.cacheWriteTokens + model.cacheWriteTokens, + outputTokens: result.outputTokens + model.outputTokens, + reasoningTokens: result.reasoningTokens + model.reasoningTokens, + totalTokens: result.totalTokens + model.totalTokens, + callCount: result.callCount + model.callCount, + durationSeconds: result.durationSeconds + model.durationSeconds, + latency: result.latency + model.avgLatencyMS * model.recordCount, + billedNanousd: result.billedNanousd + model.billedNanousd, + billedUSD: result.billedUSD + model.billedUSD, + }), + { + recordCount: 0, + inputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + outputTokens: 0, + reasoningTokens: 0, + totalTokens: 0, + callCount: 0, + durationSeconds: 0, + latency: 0, + billedNanousd: 0, + billedUSD: 0, + }, + ); + return { + platformModelName: OTHER_MODEL_KEY, + recordCount: totals.recordCount, + inputTokens: totals.inputTokens, + cacheReadTokens: totals.cacheReadTokens, + cacheWriteTokens: totals.cacheWriteTokens, + outputTokens: totals.outputTokens, + reasoningTokens: totals.reasoningTokens, + totalTokens: totals.totalTokens, + callCount: totals.callCount, + durationSeconds: totals.durationSeconds, + avgLatencyMS: totals.recordCount > 0 ? totals.latency / totals.recordCount : 0, + billedNanousd: totals.billedNanousd, + billedUSD: totals.billedUSD, + modelLabel, + }; +} + function DailyUsageChartTooltip({ active, payload, billingDisplay, + hiddenSeries, }: { active?: boolean; payload?: Array<{ payload?: DailyUsageChartPoint; }>; billingDisplay: BillingDisplayOptions; + hiddenSeries: ReadonlySet; }) { const t = useTranslations("settings.subscriptionPage.usageTrend.tooltip"); const item = payload?.[0]?.payload; if (!active || !item) { return null; } + const visibleModels = item.models.filter((model) => !hiddenSeries.has(model.platformModelName || "-")); + const visibleMetrics = item.models.length > 0 + ? visibleModels.reduce( + (totals, model) => ({ + tokens: totals.tokens + model.totalTokens, + billedUsd: totals.billedUsd + model.billedUSD, + calls: totals.calls + model.callCount, + latency: totals.latency + model.avgLatencyMS * model.recordCount, + records: totals.records + model.recordCount, + }), + { tokens: 0, billedUsd: 0, calls: 0, latency: 0, records: 0 }, + ) + : { + tokens: item.totalTokens, + billedUsd: item.billedUsd, + calls: item.callCount, + latency: item.avgLatencyMS * item.recordCount, + records: item.recordCount, + }; + const visibleLatency = visibleMetrics.records > 0 ? visibleMetrics.latency / visibleMetrics.records : 0; return (

{item.fullDayLabel}

+
+ Tokens + {formatTokenCount(visibleMetrics.tokens)} +
{t("cost")} - {formatUsageSummaryCost(item.billedUsd, billingDisplay)} + {formatUsageSummaryCost(visibleMetrics.billedUsd, billingDisplay)}
{t("calls")} - {item.callCount.toLocaleString("en-US")} + {visibleMetrics.calls.toLocaleString("en-US")}
- Tokens - {formatTokenCount(item.totalTokens)} + {t("latency")} + {formatUsageTrendLatency(visibleLatency)}
- {item.models.length > 0 ? ( + {visibleModels.length > 0 ? (
- {item.models.slice(0, 6).map((model) => ( + {visibleModels.slice(0, 10).map((model) => (
- {modelDisplayLabel(model)} + {model.modelLabel ?? modelDisplayLabel(model)} {formatTokenCount(model.totalTokens)}
@@ -187,31 +315,6 @@ function DailyUsageChartTooltip({ ); } -function isTopStackSegment(point: DailyUsageChartPoint, modelSeries: ModelSeries[], modelIndex: number): boolean { - const current = Number(point[modelSeries[modelIndex]?.key] ?? 0); - if (current <= 0) return false; - for (let index = modelIndex + 1; index < modelSeries.length; index += 1) { - if (Number(point[modelSeries[index].key] ?? 0) > 0) { - return false; - } - } - return true; -} - -function DailyUsageStackBarShape({ - modelIndex, - modelSeries, - props, -}: { - modelIndex: number; - modelSeries: ModelSeries[]; - props: BarShapeProps; -}) { - const point: DailyUsageChartPoint | undefined = props.payload; - const radius: RectangleProps["radius"] = point && isTopStackSegment(point, modelSeries, modelIndex) ? [4, 4, 0, 0] : 0; - return ; -} - function DailyUsageChart({ items, loading, @@ -223,6 +326,13 @@ function DailyUsageChart({ }) { const t = useTranslations("settings.subscriptionPage.usageTrend"); const { locale } = useAppLocale(); + const { hiddenSeries, toggleSeries } = useHiddenUsageSeries(); + const otherModelLabel = t("otherModels"); + const [todayEndMS] = React.useState(() => { + const today = new Date(); + today.setHours(23, 59, 59, 999); + return today.getTime(); + }); const modelSeries = React.useMemo(() => { const totals = new Map(); for (const item of items) { @@ -236,22 +346,40 @@ function DailyUsageChart({ }); } } - return Array.from(totals.entries()) - .sort((left, right) => right[1].totalTokens - left[1].totalTokens || left[0].localeCompare(right[0])) + const orderedModels = Array.from(totals.entries()) + .sort((left, right) => right[1].totalTokens - left[1].totalTokens || left[0].localeCompare(right[0])); + const topModels = orderedModels.slice(0, MAX_DAILY_MODEL_SERIES) .map(([platformModelName, summary], index) => ({ key: `model_${index}`, platformModelName, modelLabel: summary.label, - color: STACK_COLORS[index % STACK_COLORS.length], + color: STACK_COLORS[index], })); - }, [items]); + if (orderedModels.length > MAX_DAILY_MODEL_SERIES) { + topModels.push({ + key: "model_other", + platformModelName: OTHER_MODEL_KEY, + modelLabel: otherModelLabel, + color: OTHER_MODEL_COLOR, + }); + } + return topModels; + }, [items, otherModelLabel]); + const topModelNames = React.useMemo( + () => new Set(modelSeries.filter((item) => item.platformModelName !== OTHER_MODEL_KEY).map((item) => item.platformModelName)), + [modelSeries], + ); const modelKeyByName = React.useMemo(() => new Map(modelSeries.map((item) => [item.platformModelName, item.key])), [modelSeries]); const modelColorByName = React.useMemo(() => new Map(modelSeries.map((item) => [item.platformModelName, item.color])), [modelSeries]); const chartData = React.useMemo( () => [...items] + .filter((item) => new Date(item.usageDate).getTime() <= todayEndMS) .sort((left, right) => new Date(left.usageDate).getTime() - new Date(right.usageDate).getTime()) .map((item) => { + const models: DailyUsageChartModel[] = (item.models ?? []).filter((model) => topModelNames.has(model.platformModelName || "-")); + const otherModels = (item.models ?? []).filter((model) => !topModelNames.has(model.platformModelName || "-")); + if (otherModels.length > 0) models.push(aggregateDailyModels(otherModels, otherModelLabel)); const point: DailyUsageChartPoint = { dayLabel: formatDay(item.usageDate), fullDayLabel: formatShortDate(item.usageDate, locale), @@ -260,12 +388,13 @@ function DailyUsageChart({ callCount: item.callCount, recordCount: item.recordCount, avgLatencyMS: item.avgLatencyMS, - models: (item.models ?? []).map((model) => ({ + models: models.map((model) => ({ ...model, color: modelColorByName.get(model.platformModelName || "-"), + modelLabel: model.modelLabel, })), }; - for (const model of item.models ?? []) { + for (const model of models) { const key = modelKeyByName.get(model.platformModelName || "-"); if (key) { point[key] = model.totalTokens; @@ -273,7 +402,7 @@ function DailyUsageChart({ } return point; }), - [items, locale, modelColorByName, modelKeyByName], + [items, locale, modelColorByName, modelKeyByName, otherModelLabel, todayEndMS, topModelNames], ); const chartConfig = React.useMemo(() => { if (modelSeries.length === 0) return usageTokenChartConfig; @@ -281,6 +410,20 @@ function DailyUsageChart({ }, [modelSeries]); const rangeLabel = chartData.length > 0 ? `${chartData[0].fullDayLabel} - ${chartData[chartData.length - 1].fullDayLabel}` : ""; const hasUsageData = chartData.some((item) => item.billedUsd > 0 || item.totalTokens > 0 || item.callCount > 0 || item.recordCount > 0); + const legendItems = React.useMemo( + () => modelSeries.length > 0 + ? modelSeries.map((item) => ({ + id: item.platformModelName, + label: item.modelLabel, + color: item.color, + })) + : [{ id: "totalTokens", label: "Tokens", color: "var(--chart-1)" }], + [modelSeries], + ); + const topVisibleModelName = React.useMemo( + () => [...modelSeries].reverse().find((item) => !hiddenSeries.has(item.platformModelName))?.platformModelName, + [hiddenSeries, modelSeries], + ); return (
@@ -291,36 +434,60 @@ function DailyUsageChart({ {loading ? : null} {!loading && !hasUsageData ?
{t("empty")}
: null} {!loading && hasUsageData ? ( - - - - - formatUsageAxisTokens(value)} - /> - } /> - {modelSeries.length > 0 ? ( - modelSeries.map((model, modelIndex) => ( +
+ + + + + formatUsageAxisTokens(value)} + /> + } + /> + {modelSeries.length > 0 ? ( + modelSeries.map((model) => ( + + )) + ) : ( ( - - )} + hide={hiddenSeries.has("totalTokens")} + isAnimationActive + animationDuration={CHART_ANIMATION_DURATION_MS} + animationEasing="ease-out" /> - )) - ) : ( - - )} - - + )} + + + +
) : null}
); @@ -361,6 +528,10 @@ function MonthlyUsageChartTooltip({

{item.fullMonthLabel}

+
+ Tokens + {formatTokenCount(item.totalTokens)} +
{t("cost")} {formatUsageSummaryCost(item.billedUsd, billingDisplay)} @@ -370,8 +541,8 @@ function MonthlyUsageChartTooltip({ {item.callCount.toLocaleString("en-US")}
- Tokens - {formatTokenCount(item.totalTokens)} + {t("latency")} + {formatUsageTrendLatency(item.avgLatencyMS)}
@@ -389,6 +560,7 @@ function MonthlyUsageChart({ }) { const t = useTranslations("settings.subscriptionPage.usageTrend"); const { locale } = useAppLocale(); + const { hiddenSeries, toggleSeries } = useHiddenUsageSeries(); const chartData = React.useMemo( () => [...items] @@ -406,6 +578,10 @@ function MonthlyUsageChart({ ); const rangeLabel = chartData.length > 0 ? `${chartData[0].fullMonthLabel} - ${chartData[chartData.length - 1].fullMonthLabel}` : ""; const hasUsageData = chartData.some((item) => item.billedUsd > 0 || item.totalTokens > 0 || item.callCount > 0 || item.recordCount > 0); + const legendItems = React.useMemo( + () => [{ id: "totalTokens", label: "Tokens", color: "var(--chart-1)" }], + [], + ); return (
@@ -416,15 +592,34 @@ function MonthlyUsageChart({ {loading ? : null} {!loading && !hasUsageData ?
{t("empty")}
: null} {!loading && hasUsageData ? ( - - - - - formatUsageAxisTokens(value)} /> - } /> - - - +
+ + + + + formatUsageAxisTokens(value)} /> + } /> + + + + +
) : null}
); @@ -455,22 +650,12 @@ export function SubscriptionTrend({

{view === "daily" ? t("usageTrend.dailyTitle") : t("usageTrend.monthlyTitle")}

-
- - -
+ onViewChange(value as UsageTrendView)}> + + {t("usageTrend.daily")} + {t("usageTrend.monthly")} + +
{view === "daily" ? ( diff --git a/frontend/i18n/messages/en-US/admin-statistics.json b/frontend/i18n/messages/en-US/admin-statistics.json index 93833923..9690985e 100644 --- a/frontend/i18n/messages/en-US/admin-statistics.json +++ b/frontend/i18n/messages/en-US/admin-statistics.json @@ -1,33 +1,36 @@ { "title": "Usage statistics", - "description": "Review global model usage, billed cost, tokens, calls, and latency.", "refresh": "Refresh", "empty": "No usage data for the selected filters.", "unknownModel": "Unknown model", "unknownUser": "User #{id}", "filters": { - "rangePreset": "Time range", - "dateRange": "Dates", "last7Days": "Last 7 days", "last30Days": "Last 30 days", "last90Days": "Last 90 days", "custom": "Custom", - "user": "User", + "subject": "Subject", "model": "Model", "allModels": "All models", "billingScope": "Billing type", "billingAll": "All calls", "billingFree": "Free models", "billingBillable": "Billable models", + "clear": "Clear filters", "rangeErrors": { "incomplete": "Select both a start and end date.", "invalid": "The end date must be on or after the start date.", "tooLong": "The selected range cannot exceed 366 days." }, - "userSelect": { + "subjectSelect": { "allUsers": "All users", - "searchPlaceholder": "Search username, name, or email", - "empty": "No matching users.", + "users": "Users", + "permissionGroups": "Permission groups", + "searchUsers": "Search username, name, or email", + "searchPermissionGroups": "Search permission groups", + "emptyUsers": "No matching users.", + "emptyPermissionGroups": "No matching permission groups.", + "memberCount": "{count} users", "loadFailed": "Could not load users" } }, @@ -43,14 +46,11 @@ "calls": "Calls" }, "trend": { - "title": "Usage trend", - "daily": "Aggregated by day", - "monthly": "Aggregated by month" + "title": "Usage trend" }, "rankings": { "models": "Top models", "users": "Top users", - "top10": "Top 10 by {metric}", "total": "Total" }, "toasts": { diff --git a/frontend/i18n/messages/en-US/settings.json b/frontend/i18n/messages/en-US/settings.json index c5fb063f..b44e6690 100644 --- a/frontend/i18n/messages/en-US/settings.json +++ b/frontend/i18n/messages/en-US/settings.json @@ -504,6 +504,7 @@ "monthly": "Monthly", "dailyUsage": "Daily usage", "monthlyUsage": "Monthly usage", + "otherModels": "Other", "empty": "No usage data.", "metrics": { "totalCost": "Total cost", @@ -513,7 +514,8 @@ }, "tooltip": { "cost": "Cost", - "calls": "Calls" + "calls": "Calls", + "latency": "Average latency" } }, "usageLog": { diff --git a/frontend/i18n/messages/zh-CN/admin-statistics.json b/frontend/i18n/messages/zh-CN/admin-statistics.json index 1f60a177..e9827880 100644 --- a/frontend/i18n/messages/zh-CN/admin-statistics.json +++ b/frontend/i18n/messages/zh-CN/admin-statistics.json @@ -1,33 +1,36 @@ { "title": "用量统计", - "description": "查看全局模型调用的费用、Token、调用次数与平均耗时。", "refresh": "刷新", "empty": "当前筛选条件下暂无用量数据。", "unknownModel": "未知模型", "unknownUser": "用户 #{id}", "filters": { - "rangePreset": "时间范围", - "dateRange": "起止日期", "last7Days": "近 7 天", "last30Days": "近 30 天", "last90Days": "近 90 天", "custom": "自定义", - "user": "用户", + "subject": "统计对象", "model": "模型", "allModels": "全部模型", "billingScope": "计费类型", "billingAll": "全部调用", "billingFree": "免费模型", "billingBillable": "计费模型", + "clear": "清除筛选", "rangeErrors": { "incomplete": "请选择完整的开始和结束日期。", "invalid": "结束日期不能早于开始日期。", "tooLong": "时间范围不能超过 366 天。" }, - "userSelect": { + "subjectSelect": { "allUsers": "全部用户", - "searchPlaceholder": "搜索用户名、昵称或邮箱", - "empty": "没有匹配的用户。", + "users": "用户", + "permissionGroups": "权限组", + "searchUsers": "搜索用户名、昵称或邮箱", + "searchPermissionGroups": "搜索权限组", + "emptyUsers": "没有匹配的用户。", + "emptyPermissionGroups": "没有匹配的权限组。", + "memberCount": "{count} 人", "loadFailed": "用户列表加载失败" } }, @@ -38,19 +41,16 @@ "latency": "平均耗时" }, "rankBy": { - "cost": "费用", + "cost": "计费", "tokens": "Tokens", "calls": "调用" }, "trend": { - "title": "用量趋势", - "daily": "按日聚合", - "monthly": "按月聚合" + "title": "用量趋势" }, "rankings": { "models": "模型排行", "users": "用户排行", - "top10": "按{metric}排序的 Top 10", "total": "总计" }, "toasts": { diff --git a/frontend/i18n/messages/zh-CN/settings.json b/frontend/i18n/messages/zh-CN/settings.json index c56cf43f..2624cd9e 100644 --- a/frontend/i18n/messages/zh-CN/settings.json +++ b/frontend/i18n/messages/zh-CN/settings.json @@ -504,6 +504,7 @@ "monthly": "按月", "dailyUsage": "每日用量", "monthlyUsage": "月度用量", + "otherModels": "其他", "empty": "暂无用量数据。", "metrics": { "totalCost": "总费用", @@ -513,7 +514,8 @@ }, "tooltip": { "cost": "费用", - "calls": "调用" + "calls": "调用", + "latency": "平均耗时" } }, "usageLog": {