* feat(dashboard): add traffic flow sankey chart Add dashboard flow APIs and a Sankey-based flow view with user, optional API key, model, and channel layers.\n\nReuse the dashboard VChart palette, add precise link/node tooltips and interactions, and cover filtering, layer ordering, color stability, and error states with tests. * feat: build flow chart from quota data --------- Co-authored-by: CaIon <i@caion.me>
179 lines
5.4 KiB
Go
179 lines
5.4 KiB
Go
package model
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type FlowQuotaData struct {
|
|
UserID int `json:"user_id,omitempty" gorm:"column:user_id"`
|
|
Username string `json:"username,omitempty" gorm:"column:username"`
|
|
NodeName string `json:"node_name,omitempty" gorm:"column:node_name"`
|
|
TokenID int `json:"token_id,omitempty" gorm:"column:token_id"`
|
|
TokenName string `json:"token_name,omitempty" gorm:"-"`
|
|
UseGroup string `json:"use_group" gorm:"column:use_group"`
|
|
ChannelID int `json:"channel_id,omitempty" gorm:"column:channel_id"`
|
|
ChannelName string `json:"channel_name,omitempty" gorm:"-"`
|
|
ModelName string `json:"model_name" gorm:"column:model_name"`
|
|
TokenUsed int `json:"token_used" gorm:"column:token_used"`
|
|
Count int `json:"count" gorm:"column:count"`
|
|
Quota int `json:"quota" gorm:"column:quota"`
|
|
}
|
|
|
|
func GetFlowQuotaData(startTime int64, endTime int64, username string, userID int, role int) ([]*FlowQuotaData, error) {
|
|
switch {
|
|
case role >= common.RoleRootUser:
|
|
return getRootFlowQuotaData(startTime, endTime, username)
|
|
case role >= common.RoleAdminUser:
|
|
return getAdminFlowQuotaData(startTime, endTime, username)
|
|
default:
|
|
return getSelfFlowQuotaData(startTime, endTime, userID)
|
|
}
|
|
}
|
|
|
|
func flowQuotaBaseQuery(startTime int64, endTime int64) *gorm.DB {
|
|
query := DB.Table("quota_data").
|
|
Where("use_group <> ''").
|
|
Where("created_at >= ? and created_at <= ?", startTime, endTime)
|
|
return query
|
|
}
|
|
|
|
func getSelfFlowQuotaData(startTime int64, endTime int64, userID int) ([]*FlowQuotaData, error) {
|
|
rows := make([]*FlowQuotaData, 0)
|
|
err := flowQuotaBaseQuery(startTime, endTime).
|
|
Select("token_id, use_group, model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used").
|
|
Where("user_id = ?", userID).
|
|
Group("token_id, use_group, model_name").
|
|
Order("quota DESC").
|
|
Find(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, fillFlowTokenNames(rows)
|
|
}
|
|
|
|
func getAdminFlowQuotaData(startTime int64, endTime int64, username string) ([]*FlowQuotaData, error) {
|
|
rows := make([]*FlowQuotaData, 0)
|
|
query := flowQuotaBaseQuery(startTime, endTime).
|
|
Select("user_id, username, use_group, model_name, channel_id, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used")
|
|
if username != "" {
|
|
query = query.Where("username = ?", username)
|
|
}
|
|
err := query.
|
|
Group("user_id, username, use_group, model_name, channel_id").
|
|
Order("quota DESC").
|
|
Find(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, fillFlowChannelNames(rows)
|
|
}
|
|
|
|
func getRootFlowQuotaData(startTime int64, endTime int64, username string) ([]*FlowQuotaData, error) {
|
|
rows := make([]*FlowQuotaData, 0)
|
|
query := flowQuotaBaseQuery(startTime, endTime).
|
|
Select("user_id, username, node_name, token_id, use_group, model_name, channel_id, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used")
|
|
if username != "" {
|
|
query = query.Where("username = ?", username)
|
|
}
|
|
err := query.
|
|
Group("user_id, username, node_name, token_id, use_group, model_name, channel_id").
|
|
Order("quota DESC").
|
|
Find(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := fillFlowTokenNames(rows); err != nil {
|
|
return rows, err
|
|
}
|
|
return rows, fillFlowChannelNames(rows)
|
|
}
|
|
|
|
func fillFlowTokenNames(rows []*FlowQuotaData) error {
|
|
tokenIDSet := make(map[int]struct{})
|
|
tokenIDs := make([]int, 0)
|
|
for _, row := range rows {
|
|
if row.TokenID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := tokenIDSet[row.TokenID]; ok {
|
|
continue
|
|
}
|
|
tokenIDSet[row.TokenID] = struct{}{}
|
|
tokenIDs = append(tokenIDs, row.TokenID)
|
|
}
|
|
if len(tokenIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var tokens []struct {
|
|
Id int `gorm:"column:id"`
|
|
Name string `gorm:"column:name"`
|
|
}
|
|
if err := DB.Model(&Token{}).Select("id, name").Where("id IN ?", tokenIDs).Find(&tokens).Error; err != nil {
|
|
return err
|
|
}
|
|
tokenNameByID := make(map[int]string, len(tokens))
|
|
for _, token := range tokens {
|
|
tokenNameByID[token.Id] = token.Name
|
|
}
|
|
// Deleted tokens are intentionally not resolved here: leave TokenName empty
|
|
// so the frontend can render a localized "deleted (id)" label instead.
|
|
for _, row := range rows {
|
|
if name := tokenNameByID[row.TokenID]; name != "" {
|
|
row.TokenName = name
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fillFlowChannelNames(rows []*FlowQuotaData) error {
|
|
channelIDSet := make(map[int]struct{})
|
|
channelIDs := make([]int, 0)
|
|
for _, row := range rows {
|
|
if row.ChannelID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := channelIDSet[row.ChannelID]; ok {
|
|
continue
|
|
}
|
|
channelIDSet[row.ChannelID] = struct{}{}
|
|
channelIDs = append(channelIDs, row.ChannelID)
|
|
}
|
|
if len(channelIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
channelNameByID := make(map[int]string, len(channelIDs))
|
|
if common.MemoryCacheEnabled {
|
|
for _, channelID := range channelIDs {
|
|
if channel, err := CacheGetChannel(channelID); err == nil {
|
|
channelNameByID[channelID] = channel.Name
|
|
}
|
|
}
|
|
} else {
|
|
var channels []struct {
|
|
Id int `gorm:"column:id"`
|
|
Name string `gorm:"column:name"`
|
|
}
|
|
if err := DB.Table("channels").Select("id, name").Where("id IN ?", channelIDs).Find(&channels).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, channel := range channels {
|
|
channelNameByID[channel.Id] = channel.Name
|
|
}
|
|
}
|
|
for _, row := range rows {
|
|
if name := channelNameByID[row.ChannelID]; name != "" {
|
|
row.ChannelName = name
|
|
continue
|
|
}
|
|
if row.ChannelID > 0 {
|
|
row.ChannelName = fmt.Sprintf("channel-%d", row.ChannelID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|