Skip to content

Commit 0868c82

Browse files
authored
fix: Keep key last_used_at accurate and group-scoped (#383)
1 parent ea0b98f commit 0868c82

File tree

3 files changed

+86
-46
lines changed

3 files changed

+86
-46
lines changed

internal/models/types.go

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -79,46 +79,47 @@ type ParentAggregateGroupInfo struct {
7979

8080
// Group 对应 groups 表
8181
type Group struct {
82-
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
83-
EffectiveConfig types.SystemSettings `gorm:"-" json:"effective_config,omitempty"`
84-
Name string `gorm:"type:varchar(255);not null;unique" json:"name"`
85-
Endpoint string `gorm:"-" json:"endpoint"`
86-
DisplayName string `gorm:"type:varchar(255)" json:"display_name"`
87-
ProxyKeys string `gorm:"type:text" json:"proxy_keys"`
88-
Description string `gorm:"type:varchar(512)" json:"description"`
89-
GroupType string `gorm:"type:varchar(50);default:'standard'" json:"group_type"` // 'standard' or 'aggregate'
90-
Upstreams datatypes.JSON `gorm:"type:json;not null" json:"upstreams"`
91-
ValidationEndpoint string `gorm:"type:varchar(255)" json:"validation_endpoint"`
92-
ChannelType string `gorm:"type:varchar(50);not null" json:"channel_type"`
93-
Sort int `gorm:"default:0" json:"sort"`
94-
TestModel string `gorm:"type:varchar(255);not null" json:"test_model"`
95-
ParamOverrides datatypes.JSONMap `gorm:"type:json" json:"param_overrides"`
96-
Config datatypes.JSONMap `gorm:"type:json" json:"config"`
97-
HeaderRules datatypes.JSON `gorm:"type:json" json:"header_rules"`
98-
ModelRedirectRules datatypes.JSONMap `gorm:"type:json" json:"model_redirect_rules"`
99-
ModelRedirectStrict bool `gorm:"default:false" json:"model_redirect_strict"`
100-
APIKeys []APIKey `gorm:"foreignKey:GroupID" json:"api_keys"`
101-
SubGroups []GroupSubGroup `gorm:"-" json:"sub_groups,omitempty"`
102-
LastValidatedAt *time.Time `json:"last_validated_at"`
103-
CreatedAt time.Time `json:"created_at"`
104-
UpdatedAt time.Time `json:"updated_at"`
82+
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
83+
EffectiveConfig types.SystemSettings `gorm:"-" json:"effective_config,omitempty"`
84+
Name string `gorm:"type:varchar(255);not null;unique" json:"name"`
85+
Endpoint string `gorm:"-" json:"endpoint"`
86+
DisplayName string `gorm:"type:varchar(255)" json:"display_name"`
87+
ProxyKeys string `gorm:"type:text" json:"proxy_keys"`
88+
Description string `gorm:"type:varchar(512)" json:"description"`
89+
GroupType string `gorm:"type:varchar(50);default:'standard'" json:"group_type"` // 'standard' or 'aggregate'
90+
Upstreams datatypes.JSON `gorm:"type:json;not null" json:"upstreams"`
91+
ValidationEndpoint string `gorm:"type:varchar(255)" json:"validation_endpoint"`
92+
ChannelType string `gorm:"type:varchar(50);not null" json:"channel_type"`
93+
Sort int `gorm:"default:0" json:"sort"`
94+
TestModel string `gorm:"type:varchar(255);not null" json:"test_model"`
95+
ParamOverrides datatypes.JSONMap `gorm:"type:json" json:"param_overrides"`
96+
Config datatypes.JSONMap `gorm:"type:json" json:"config"`
97+
HeaderRules datatypes.JSON `gorm:"type:json" json:"header_rules"`
98+
ModelRedirectRules datatypes.JSONMap `gorm:"type:json" json:"model_redirect_rules"`
99+
ModelRedirectStrict bool `gorm:"default:false" json:"model_redirect_strict"`
100+
APIKeys []APIKey `gorm:"foreignKey:GroupID" json:"api_keys"`
101+
SubGroups []GroupSubGroup `gorm:"-" json:"sub_groups,omitempty"`
102+
LastValidatedAt *time.Time `json:"last_validated_at"`
103+
CreatedAt time.Time `json:"created_at"`
104+
UpdatedAt time.Time `json:"updated_at"`
105105

106106
// For cache
107-
ProxyKeysMap map[string]struct{} `gorm:"-" json:"-"`
108-
HeaderRuleList []HeaderRule `gorm:"-" json:"-"`
109-
ModelRedirectMap map[string]string `gorm:"-" json:"-"`
107+
ProxyKeysMap map[string]struct{} `gorm:"-" json:"-"`
108+
HeaderRuleList []HeaderRule `gorm:"-" json:"-"`
109+
ModelRedirectMap map[string]string `gorm:"-" json:"-"`
110110
}
111111

112112
// APIKey 对应 api_keys 表
113113
type APIKey struct {
114-
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
114+
ID uint `gorm:"primaryKey;autoIncrement;index:idx_api_keys_group_last_used_id,priority:3" json:"id"`
115115
KeyValue string `gorm:"type:text;not null" json:"key_value"`
116116
KeyHash string `gorm:"type:varchar(128);index" json:"key_hash"`
117-
GroupID uint `gorm:"not null;index" json:"group_id"`
117+
GroupID uint `gorm:"not null;index;index:idx_api_keys_group_last_used_id,priority:1" json:"group_id"`
118118
Status string `gorm:"type:varchar(50);not null;default:'active';index" json:"status"`
119119
Notes string `gorm:"type:varchar(255);default:''" json:"notes"`
120120
RequestCount int64 `gorm:"not null;default:0" json:"request_count"`
121121
FailureCount int64 `gorm:"not null;default:0" json:"failure_count"`
122+
LastUsedAt *time.Time `gorm:"index:idx_api_keys_group_last_used_id,priority:2" json:"last_used_at"`
122123
CreatedAt time.Time `json:"created_at"`
123124
UpdatedAt time.Time `json:"updated_at"`
124125
}

internal/services/key_service.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,12 @@ func (s *KeyService) ListKeysInGroupQuery(groupID uint, statusFilter string, sea
301301
query = query.Where("key_hash = ?", searchHash)
302302
}
303303

304-
query = query.Order("id desc")
304+
orderBy := "last_used_at desc, id desc"
305+
if s.DB.Dialector.Name() == "postgres" {
306+
orderBy = "last_used_at desc nulls last, id desc"
307+
}
308+
309+
query = query.Order(orderBy)
305310

306311
return query
307312
}

internal/services/request_log_service.go

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -207,28 +207,62 @@ func (s *RequestLogService) writeLogsToDB(logs []*models.RequestLog) error {
207207
return fmt.Errorf("failed to batch insert request logs: %w", err)
208208
}
209209

210-
keyStats := make(map[string]int64)
210+
type keyUsageStat struct {
211+
Count int64
212+
LastUsedAt time.Time
213+
}
214+
215+
groupedKeyStats := make(map[uint]map[string]keyUsageStat)
211216
for _, log := range logs {
212-
if log.IsSuccess && log.KeyHash != "" {
213-
keyStats[log.KeyHash]++
217+
if !log.IsSuccess || log.KeyHash == "" {
218+
continue
214219
}
215-
}
216220

217-
if len(keyStats) > 0 {
218-
var caseStmt strings.Builder
219-
var keyHashes []string
220-
caseStmt.WriteString("CASE key_hash ")
221-
for keyHash, count := range keyStats {
222-
caseStmt.WriteString(fmt.Sprintf("WHEN '%s' THEN request_count + %d ", keyHash, count))
223-
keyHashes = append(keyHashes, keyHash)
221+
if _, exists := groupedKeyStats[log.GroupID]; !exists {
222+
groupedKeyStats[log.GroupID] = make(map[string]keyUsageStat)
224223
}
225-
caseStmt.WriteString("END")
226224

227-
if err := tx.Model(&models.APIKey{}).Where("key_hash IN ?", keyHashes).
228-
Updates(map[string]any{
229-
"request_count": gorm.Expr(caseStmt.String()),
230-
}).Error; err != nil {
231-
return fmt.Errorf("failed to batch update api_key stats: %w", err)
225+
stats := groupedKeyStats[log.GroupID][log.KeyHash]
226+
stats.Count++
227+
if stats.LastUsedAt.IsZero() || log.Timestamp.After(stats.LastUsedAt) {
228+
stats.LastUsedAt = log.Timestamp
229+
}
230+
groupedKeyStats[log.GroupID][log.KeyHash] = stats
231+
}
232+
233+
if len(groupedKeyStats) > 0 {
234+
for groupID, keyStats := range groupedKeyStats {
235+
var requestCountCase strings.Builder
236+
requestCountCase.WriteString("CASE key_hash ")
237+
238+
var lastUsedAtCase strings.Builder
239+
lastUsedAtCase.WriteString("CASE key_hash ")
240+
241+
requestCountArgs := make([]any, 0, len(keyStats)*2)
242+
lastUsedAtArgs := make([]any, 0, len(keyStats)*2)
243+
keyHashes := make([]string, 0, len(keyStats))
244+
245+
for keyHash, stats := range keyStats {
246+
requestCountCase.WriteString("WHEN ? THEN request_count + ? ")
247+
requestCountArgs = append(requestCountArgs, keyHash, stats.Count)
248+
249+
lastUsedAtCase.WriteString("WHEN ? THEN ? ")
250+
lastUsedAtArgs = append(lastUsedAtArgs, keyHash, stats.LastUsedAt)
251+
252+
keyHashes = append(keyHashes, keyHash)
253+
}
254+
255+
requestCountCase.WriteString("ELSE request_count END")
256+
lastUsedAtCase.WriteString("ELSE last_used_at END")
257+
258+
if err := tx.Model(&models.APIKey{}).
259+
Where("group_id = ? AND key_hash IN ?", groupID, keyHashes).
260+
Updates(map[string]any{
261+
"request_count": gorm.Expr(requestCountCase.String(), requestCountArgs...),
262+
"last_used_at": gorm.Expr(lastUsedAtCase.String(), lastUsedAtArgs...),
263+
}).Error; err != nil {
264+
return fmt.Errorf("failed to batch update api_key stats: %w", err)
265+
}
232266
}
233267
}
234268

0 commit comments

Comments
 (0)