feat(dashboard): add traffic flow sankey chart (#5465)

* 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>
This commit is contained in:
Quaternijkon
2026-06-20 18:57:47 +08:00
committed by GitHub
co-authored by CaIon
parent f9e508bdae
commit a68041f7d6
29 changed files with 2659 additions and 77 deletions
+76 -31
View File
@@ -16,11 +16,28 @@ type QuotaData struct {
Username string `json:"username" gorm:"index:idx_qdt_model_user_name,priority:2;size:64;default:''"`
ModelName string `json:"model_name" gorm:"index:idx_qdt_model_user_name,priority:1;size:64;default:''"`
CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_qdt_created_at,priority:2"`
UseGroup string `json:"use_group" gorm:"index;size:64;default:''"`
TokenID int `json:"token_id" gorm:"index;default:0"`
ChannelID int `json:"channel_id" gorm:"index;default:0"`
NodeName string `json:"node_name" gorm:"index;size:64;default:''"`
TokenUsed int `json:"token_used" gorm:"default:0"`
Count int `json:"count" gorm:"default:0"`
Quota int `json:"quota" gorm:"default:0"`
}
type QuotaDataLogParams struct {
UserID int
Username string
ModelName string
Quota int
CreatedAt int64
TokenUsed int
UseGroup string
TokenID int
ChannelID int
NodeName string
}
func UpdateQuotaData() {
for {
if common.DataExportEnabled {
@@ -34,34 +51,50 @@ func UpdateQuotaData() {
var CacheQuotaData = make(map[string]*QuotaData)
var CacheQuotaDataLock = sync.Mutex{}
func logQuotaDataCache(userId int, username string, modelName string, quota int, createdAt int64, tokenUsed int) {
key := fmt.Sprintf("%d-%s-%s-%d", userId, username, modelName, createdAt)
quotaData, ok := CacheQuotaData[key]
func logQuotaDataCache(quotaData *QuotaData) {
key := fmt.Sprintf("%d\x00%s\x00%s\x00%d\x00%s\x00%d\x00%d\x00%s",
quotaData.UserID,
quotaData.Username,
quotaData.ModelName,
quotaData.CreatedAt,
quotaData.UseGroup,
quotaData.TokenID,
quotaData.ChannelID,
quotaData.NodeName,
)
count := quotaData.Count
quota := quotaData.Quota
tokenUsed := quotaData.TokenUsed
cachedQuotaData, ok := CacheQuotaData[key]
if ok {
quotaData.Count += 1
quotaData.Quota += quota
quotaData.TokenUsed += tokenUsed
} else {
quotaData = &QuotaData{
UserID: userId,
Username: username,
ModelName: modelName,
CreatedAt: createdAt,
Count: 1,
Quota: quota,
TokenUsed: tokenUsed,
}
cachedQuotaData.Count += count
cachedQuotaData.Quota += quota
cachedQuotaData.TokenUsed += tokenUsed
quotaData = cachedQuotaData
}
CacheQuotaData[key] = quotaData
}
func LogQuotaData(userId int, username string, modelName string, quota int, createdAt int64, tokenUsed int) {
func LogQuotaData(params QuotaDataLogParams) {
// 只精确到小时
createdAt = createdAt - (createdAt % 3600)
createdAt := params.CreatedAt - (params.CreatedAt % 3600)
quotaData := &QuotaData{
UserID: params.UserID,
Username: params.Username,
ModelName: params.ModelName,
CreatedAt: createdAt,
UseGroup: params.UseGroup,
TokenID: params.TokenID,
ChannelID: params.ChannelID,
NodeName: params.NodeName,
Count: 1,
Quota: params.Quota,
TokenUsed: params.TokenUsed,
}
CacheQuotaDataLock.Lock()
defer CacheQuotaDataLock.Unlock()
logQuotaDataCache(userId, username, modelName, quota, createdAt, tokenUsed)
logQuotaDataCache(quotaData)
}
func SaveQuotaDataCache() {
@@ -74,13 +107,15 @@ func SaveQuotaDataCache() {
// 3. 如果没有数据,就插入数据
for _, quotaData := range CacheQuotaData {
quotaDataDB := &QuotaData{}
DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?",
quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.CreatedAt).First(quotaDataDB)
DB.Table("quota_data").
Where("user_id = ? and username = ? and model_name = ? and created_at = ? and use_group = ? and token_id = ? and channel_id = ? and node_name = ?",
quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.CreatedAt, quotaData.UseGroup, quotaData.TokenID, quotaData.ChannelID, quotaData.NodeName).
First(quotaDataDB)
if quotaDataDB.Id > 0 {
//quotaDataDB.Count += quotaData.Count
//quotaDataDB.Quota += quotaData.Quota
//DB.Table("quota_data").Save(quotaDataDB)
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt, quotaData.TokenUsed)
increaseQuotaData(quotaData)
} else {
DB.Table("quota_data").Create(quotaData)
}
@@ -89,13 +124,15 @@ func SaveQuotaDataCache() {
common.SysLog(fmt.Sprintf("保存数据看板数据成功,共保存%d条数据", size))
}
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64, tokenUsed int) {
err := DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?",
userId, username, modelName, createdAt).Updates(map[string]interface{}{
"count": gorm.Expr("count + ?", count),
"quota": gorm.Expr("quota + ?", quota),
"token_used": gorm.Expr("token_used + ?", tokenUsed),
}).Error
func increaseQuotaData(quotaData *QuotaData) {
err := DB.Table("quota_data").
Where("user_id = ? and username = ? and model_name = ? and created_at = ? and use_group = ? and token_id = ? and channel_id = ? and node_name = ?",
quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.CreatedAt, quotaData.UseGroup, quotaData.TokenID, quotaData.ChannelID, quotaData.NodeName).
Updates(map[string]interface{}{
"count": gorm.Expr("count + ?", quotaData.Count),
"quota": gorm.Expr("quota + ?", quotaData.Quota),
"token_used": gorm.Expr("token_used + ?", quotaData.TokenUsed),
}).Error
if err != nil {
common.SysLog(fmt.Sprintf("increaseQuotaData error: %s", err))
}
@@ -104,14 +141,22 @@ func increaseQuotaData(userId int, username string, modelName string, count int,
func GetQuotaDataByUsername(username string, startTime int64, endTime int64) (quotaData []*QuotaData, err error) {
var quotaDatas []*QuotaData
// 从quota_data表中查询数据
err = DB.Table("quota_data").Where("username = ? and created_at >= ? and created_at <= ?", username, startTime, endTime).Find(&quotaDatas).Error
err = DB.Table("quota_data").
Select("user_id, username, model_name, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used").
Where("username = ? and created_at >= ? and created_at <= ?", username, startTime, endTime).
Group("user_id, username, model_name, created_at").
Find(&quotaDatas).Error
return quotaDatas, err
}
func GetQuotaDataByUserId(userId int, startTime int64, endTime int64) (quotaData []*QuotaData, err error) {
var quotaDatas []*QuotaData
// 从quota_data表中查询数据
err = DB.Table("quota_data").Where("user_id = ? and created_at >= ? and created_at <= ?", userId, startTime, endTime).Find(&quotaDatas).Error
err = DB.Table("quota_data").
Select("user_id, username, model_name, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used").
Where("user_id = ? and created_at >= ? and created_at <= ?", userId, startTime, endTime).
Group("user_id, username, model_name, created_at").
Find(&quotaDatas).Error
return quotaDatas, err
}