feat: add account usage statistics view#2227
Open
specialpointcentral wants to merge 2 commits into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a new admin-facing “Account Statistics” page to the frontend and introduces backend support for listing account users (recent and date-range) to power account detail usage breakdowns.
Changes:
- Added
AccountStatsViewwith filters (date range/platform/group), sortable/paginated account rows, and an auto-refresh control with persisted settings. - Added an account detail modal showing usage summary plus “recent users” and “range users” tables (including current concurrency for recent users).
- Added backend
/admin/accounts/:id/recent-usersendpoint and repository/service support for fetching recent and date-range account users.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/views/admin/AccountStatsView.vue | New admin UI for account usage stats with auto-refresh and account detail modal. |
| frontend/src/router/index.ts | Registers the new /admin/account-stats route. |
| frontend/src/i18n/locales/en.ts | Adds nav + page translations for Account Stats (EN). |
| frontend/src/i18n/locales/zh.ts | Adds nav + page translations for Account Stats (ZH). |
| frontend/src/components/layout/AppSidebar.vue | Adds “Account Stats” entry to the admin sidebar. |
| frontend/src/api/admin/accounts.ts | Adds getRecentUsers API + RecentAccountUser type. |
| backend/internal/service/account_usage_service.go | Adds service/repo interfaces + RecentAccountUser DTO for account user stats. |
| backend/internal/server/routes/admin.go | Registers the new admin account route /recent-users. |
| backend/internal/server/api_contract_test.go | Updates repo stub to satisfy the expanded repository interface. |
| backend/internal/repository/usage_log_repo.go | Implements SQL queries for recent users and time-range users per account. |
| backend/internal/handler/admin/account_handler.go | Adds GetRecentUsers handler with optional date-range behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+448
to
+473
| // Load data | ||
| async function loadData(options?: { silent?: boolean }) { | ||
| if (!options?.silent) loading.value = true | ||
| try { | ||
| // Build filters | ||
| const filters: Record<string, string> = {} | ||
| if (platformFilter.value) filters.platform = String(platformFilter.value) | ||
| if (groupFilter.value) filters.group = String(groupFilter.value) | ||
|
|
||
| const result = await adminAPI.accounts.list(page.value, pageSize.value, filters) | ||
| accounts.value = result.items || [] | ||
| totalAccounts.value = result.total || 0 | ||
|
|
||
| // Load concurrency data | ||
| await loadConcurrency() | ||
|
|
||
| // Load stats for each account | ||
| await loadAccountStats() | ||
|
|
||
| applyStatsToAccounts() | ||
| } catch (err) { | ||
| console.error('Failed to load account stats:', err) | ||
| } finally { | ||
| if (!options?.silent) loading.value = false | ||
| } | ||
| } |
Comment on lines
+508
to
+522
| async function loadAccountStats() { | ||
| const stats: Record<number, any> = {} | ||
| const promises = accounts.value.map(async (account) => { | ||
| try { | ||
| const result = await adminAPI.usage.getStats({ | ||
| account_id: account.id, | ||
| start_date: startDate.value, | ||
| end_date: endDate.value | ||
| }) | ||
| stats[account.id] = result | ||
| } catch { | ||
| // Ignore individual failures | ||
| } | ||
| }) | ||
| await Promise.all(promises) |
Comment on lines
+263
to
+294
| // State | ||
| const loading = ref(false) | ||
| const accounts = ref<any[]>([]) | ||
| const totalAccounts = ref(0) | ||
| const page = ref(1) | ||
| const pageSize = ref(getPersistedPageSize()) | ||
| const platformFilter = ref<string | number | boolean | null>('') | ||
| const groupFilter = ref<string | number | boolean | null>('') | ||
| const groups = ref<AdminGroup[]>([]) | ||
| const accountStats = ref<Record<number, any>>({}) | ||
|
|
||
| // Time range (default: last 24 hours via DateRangePicker) | ||
| const formatDateToString = (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}` | ||
| } | ||
| const now = new Date() | ||
| const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000) | ||
| const startDate = ref(formatDateToString(yesterday)) | ||
| const endDate = ref(formatDateToString(now)) | ||
|
|
||
| // Detail modal | ||
| const showDetail = ref(false) | ||
| const selectedAccount = ref<any>(null) | ||
| const detailStats = ref<any>(null) | ||
| const rangeUsers = ref<RecentAccountUser[]>([]) | ||
| const recentUsers = ref<RecentAccountUser[]>([]) | ||
| const detailUsersLoading = ref(false) | ||
| const userConcurrencyByID = ref<Record<number, number>>({}) | ||
|
|
Comment on lines
+645
to
+657
| /** | ||
| * Get recent users of an account (last 5 minutes) | ||
| * @param id - Account ID | ||
| * @returns List of recent users | ||
| */ | ||
| export async function getRecentUsers(id: number, params?: { | ||
| start_date?: string | ||
| end_date?: string | ||
| timezone?: string | ||
| }): Promise<{ users: RecentAccountUser[] }> { | ||
| const { data } = await apiClient.get<{ users: RecentAccountUser[] }>(`/admin/accounts/${id}/recent-users`, { params }) | ||
| return data | ||
| } |
Comment on lines
+1670
to
+1711
| // GetRecentUsers handles getting recent users of an account | ||
| // GET /api/v1/admin/accounts/:id/recent-users | ||
| func (h *AccountHandler) GetRecentUsers(c *gin.Context) { | ||
| accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) | ||
| if err != nil { | ||
| response.BadRequest(c, "Invalid account ID") | ||
| return | ||
| } | ||
|
|
||
| var users []service.RecentAccountUser | ||
| startDateStr := strings.TrimSpace(c.Query("start_date")) | ||
| endDateStr := strings.TrimSpace(c.Query("end_date")) | ||
| if startDateStr != "" || endDateStr != "" { | ||
| if startDateStr == "" || endDateStr == "" { | ||
| response.BadRequest(c, "start_date and end_date are required together") | ||
| return | ||
| } | ||
| userTZ := c.Query("timezone") | ||
| startTime, parseErr := timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ) | ||
| if parseErr != nil { | ||
| response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD") | ||
| return | ||
| } | ||
| endTime, parseErr := timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ) | ||
| if parseErr != nil { | ||
| response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD") | ||
| return | ||
| } | ||
| users, err = h.accountUsageService.GetAccountUsersByTimeRange(c.Request.Context(), accountID, startTime, endTime.AddDate(0, 0, 1)) | ||
| } else { | ||
| users, err = h.accountUsageService.GetRecentAccountUsers(c.Request.Context(), accountID, 5) | ||
| } | ||
| if err != nil { | ||
| response.ErrorFrom(c, err) | ||
| return | ||
| } | ||
| if users == nil { | ||
| users = []service.RecentAccountUser{} | ||
| } | ||
|
|
||
| response.Success(c, gin.H{"users": users}) | ||
| } |
90c8ca3 to
6dcd32d
Compare
f4f6e9a to
45bd0bd
Compare
45bd0bd to
7a9ba78
Compare
|
功能看起来不错 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Notes
Tests