mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:08:03 +08:00
Merge pull request #5243 from wucm667/feat/issue-5240-dashboard-username
fix(dashboard): show usernames in spending ranking
This commit is contained in:
@@ -149,6 +149,7 @@ type UserUsageTrendPoint struct {
|
||||
type UserSpendingRankingItem struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
ActualCost float64 `json:"actual_cost"` // 实际扣除
|
||||
Requests int64 `json:"requests"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
|
||||
@@ -713,10 +713,10 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) {
|
||||
start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
end := start.Add(24 * time.Hour)
|
||||
|
||||
rows := sqlmock.NewRows([]string{"user_id", "email", "actual_cost", "requests", "tokens", "total_actual_cost", "total_requests", "total_tokens"}).
|
||||
AddRow(int64(2), "beta@example.com", 12.5, int64(9), int64(900), 40.0, int64(30), int64(2600)).
|
||||
AddRow(int64(1), "alpha@example.com", 12.5, int64(8), int64(800), 40.0, int64(30), int64(2600)).
|
||||
AddRow(int64(3), "gamma@example.com", 4.25, int64(5), int64(300), 40.0, int64(30), int64(2600))
|
||||
rows := sqlmock.NewRows([]string{"user_id", "email", "username", "actual_cost", "requests", "tokens", "total_actual_cost", "total_requests", "total_tokens"}).
|
||||
AddRow(int64(2), "beta@example.com", "beta", 12.5, int64(9), int64(900), 40.0, int64(30), int64(2600)).
|
||||
AddRow(int64(1), "alpha@example.com", "alpha", 12.5, int64(8), int64(800), 40.0, int64(30), int64(2600)).
|
||||
AddRow(int64(3), "gamma@example.com", "", 4.25, int64(5), int64(300), 40.0, int64(30), int64(2600))
|
||||
|
||||
mock.ExpectQuery("WITH user_spend AS \\(").
|
||||
WithArgs(start, end, 12).
|
||||
@@ -726,8 +726,8 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &usagestats.UserSpendingRankingResponse{
|
||||
Ranking: []usagestats.UserSpendingRankingItem{
|
||||
{UserID: 2, Email: "beta@example.com", ActualCost: 12.5, Requests: 9, Tokens: 900},
|
||||
{UserID: 1, Email: "alpha@example.com", ActualCost: 12.5, Requests: 8, Tokens: 800},
|
||||
{UserID: 2, Email: "beta@example.com", Username: "beta", ActualCost: 12.5, Requests: 9, Tokens: 900},
|
||||
{UserID: 1, Email: "alpha@example.com", Username: "alpha", ActualCost: 12.5, Requests: 8, Tokens: 800},
|
||||
{UserID: 3, Email: "gamma@example.com", ActualCost: 4.25, Requests: 5, Tokens: 300},
|
||||
},
|
||||
TotalActualCost: 40.0,
|
||||
|
||||
@@ -150,18 +150,20 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
|
||||
SELECT
|
||||
u.user_id,
|
||||
COALESCE(us.email, '') as email,
|
||||
COALESCE(us.username, '') as username,
|
||||
COALESCE(SUM(u.actual_cost), 0) as actual_cost,
|
||||
COUNT(*) as requests,
|
||||
COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens
|
||||
FROM usage_logs u
|
||||
LEFT JOIN users us ON u.user_id = us.id
|
||||
WHERE u.created_at >= $1 AND u.created_at < $2
|
||||
GROUP BY u.user_id, us.email
|
||||
GROUP BY u.user_id, us.email, us.username
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
user_id,
|
||||
email,
|
||||
username,
|
||||
actual_cost,
|
||||
requests,
|
||||
tokens,
|
||||
@@ -175,6 +177,7 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
|
||||
SELECT
|
||||
user_id,
|
||||
email,
|
||||
username,
|
||||
actual_cost,
|
||||
requests,
|
||||
tokens,
|
||||
@@ -202,7 +205,7 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
|
||||
totalTokens := int64(0)
|
||||
for rows.Next() {
|
||||
var row UserSpendingRankingItem
|
||||
if err = rows.Scan(&row.UserID, &row.Email, &row.ActualCost, &row.Requests, &row.Tokens, &totalActualCost, &totalRequests, &totalTokens); err != nil {
|
||||
if err = rows.Scan(&row.UserID, &row.Email, &row.Username, &row.ActualCost, &row.Requests, &row.Tokens, &totalActualCost, &totalRequests, &totalTokens); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ranking = append(ranking, row)
|
||||
|
||||
@@ -422,6 +422,7 @@ const otherRankingItem = computed<RankingDisplayItem | null>(() => {
|
||||
return {
|
||||
user_id: 0,
|
||||
email: '',
|
||||
username: '',
|
||||
actual_cost: otherActualCost,
|
||||
requests: otherRequests,
|
||||
tokens: otherTokens,
|
||||
@@ -495,7 +496,8 @@ const formatNumber = (value: number): string => {
|
||||
}
|
||||
|
||||
const getRankingUserLabel = (item: UserSpendingRankingItem): string => {
|
||||
if (item.email) return item.email
|
||||
if (item.username?.trim()) return item.username.trim()
|
||||
if (item.email?.trim()) return item.email.trim()
|
||||
return t('admin.redeem.userPrefix', { id: item.user_id })
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,12 @@ vi.mock('vue-i18n', async () => {
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => messages[key] ?? key,
|
||||
t: (key: string, params?: Record<string, unknown>) => {
|
||||
const message = messages[key] ?? key
|
||||
return params
|
||||
? message.replace(/\{(\w+)\}/g, (_, name: string) => String(params[name]))
|
||||
: message
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
@@ -145,14 +150,15 @@ describe('ModelDistributionChart', () => {
|
||||
expect(wrapper.findAll('tbody tr')[0].findAll('td')).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('renders Others in the spending ranking table and uses a dedicated chart color', async () => {
|
||||
it('uses the dashboard user label policy and renders Others with a dedicated chart color', async () => {
|
||||
const wrapper = mount(ModelDistributionChart, {
|
||||
props: {
|
||||
modelStats: [],
|
||||
enableRankingView: true,
|
||||
rankingItems: [
|
||||
{ user_id: 1, email: 'alpha@example.com', actual_cost: 12, requests: 10, tokens: 1000 },
|
||||
{ user_id: 2, email: 'beta@example.com', actual_cost: 8, requests: 6, tokens: 600 },
|
||||
{ user_id: 1, email: 'alpha@example.com', username: 'alpha', actual_cost: 12, requests: 10, tokens: 1000 },
|
||||
{ user_id: 2, email: 'beta@example.com', username: ' ', actual_cost: 8, requests: 6, tokens: 600 },
|
||||
{ user_id: 3, email: ' ', username: '', actual_cost: 0, requests: 0, tokens: 0 },
|
||||
],
|
||||
rankingTotalActualCost: 30,
|
||||
rankingTotalRequests: 20,
|
||||
@@ -171,20 +177,25 @@ describe('ModelDistributionChart', () => {
|
||||
|
||||
const chartData = JSON.parse(wrapper.find('.chart-data').text())
|
||||
expect(chartData.labels).toEqual([
|
||||
'#1 alpha@example.com',
|
||||
'#1 alpha',
|
||||
'#2 beta@example.com',
|
||||
'#3 User #3',
|
||||
'Others',
|
||||
])
|
||||
expect(chartData.datasets[0].data).toEqual([12, 8, 10])
|
||||
expect(chartData.datasets[0].data).toEqual([12, 8, 0, 10])
|
||||
expect(chartData.datasets[0].backgroundColor[0]).toBe('#3b82f6')
|
||||
expect(chartData.datasets[0].backgroundColor[2]).toBe('#94a3b8')
|
||||
expect(chartData.datasets[0].backgroundColor[2]).not.toBe(chartData.datasets[0].backgroundColor[0])
|
||||
expect(chartData.datasets[0].backgroundColor[3]).toBe('#94a3b8')
|
||||
expect(chartData.datasets[0].backgroundColor[3]).not.toBe(chartData.datasets[0].backgroundColor[0])
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).toHaveLength(3)
|
||||
expect(rows[2].text()).toContain('Others')
|
||||
expect(rows[2].text()).toContain('4')
|
||||
expect(rows[2].text()).toContain('400')
|
||||
expect(rows[2].text()).toContain('$10.00')
|
||||
expect(rows).toHaveLength(4)
|
||||
expect(rows[0].text()).toContain('alpha')
|
||||
expect(rows[0].text()).not.toContain('alpha@example.com')
|
||||
expect(rows[1].text()).toContain('beta@example.com')
|
||||
expect(rows[2].text()).toContain('User #3')
|
||||
expect(rows[3].text()).toContain('Others')
|
||||
expect(rows[3].text()).toContain('4')
|
||||
expect(rows[3].text()).toContain('400')
|
||||
expect(rows[3].text()).toContain('$10.00')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1855,6 +1855,7 @@ export interface UserUsageTrendPoint {
|
||||
export interface UserSpendingRankingItem {
|
||||
user_id: number
|
||||
email: string
|
||||
username: string
|
||||
actual_cost: number
|
||||
requests: number
|
||||
tokens: number
|
||||
|
||||
Reference in New Issue
Block a user