fix: 修复批量更新凭证明细与缓存TTL抖动
- BatchUpdateCredentials 返回 success/failed/results 及 success_ids/failed_ids - billing jitteredTTL 改为只减不增,确保TTL不超上界 - crypto/rand 失败时随机ID降级避免 panic - OpenAI SelectAccount 失败日志去重并补充字段 - 修复两处类型断言以通过 errcheck
This commit is contained in:
@@ -810,20 +810,38 @@ func (h *AccountHandler) BatchUpdateCredentials(c *gin.Context) {
|
|||||||
updates = append(updates, accountUpdate{ID: accountID, Credentials: account.Credentials})
|
updates = append(updates, accountUpdate{ID: accountID, Credentials: account.Credentials})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阶段二:依次更新,任何失败立即返回(避免部分成功部分失败)
|
// 阶段二:依次更新,返回每个账号的成功/失败明细,便于调用方重试
|
||||||
|
success := 0
|
||||||
|
failed := 0
|
||||||
|
successIDs := make([]int64, 0, len(updates))
|
||||||
|
failedIDs := make([]int64, 0, len(updates))
|
||||||
|
results := make([]gin.H, 0, len(updates))
|
||||||
for _, u := range updates {
|
for _, u := range updates {
|
||||||
updateInput := &service.UpdateAccountInput{
|
updateInput := &service.UpdateAccountInput{Credentials: u.Credentials}
|
||||||
Credentials: u.Credentials,
|
|
||||||
}
|
|
||||||
if _, err := h.adminService.UpdateAccount(ctx, u.ID, updateInput); err != nil {
|
if _, err := h.adminService.UpdateAccount(ctx, u.ID, updateInput); err != nil {
|
||||||
response.Error(c, 500, fmt.Sprintf("Failed to update account %d: %v", u.ID, err))
|
failed++
|
||||||
return
|
failedIDs = append(failedIDs, u.ID)
|
||||||
|
results = append(results, gin.H{
|
||||||
|
"account_id": u.ID,
|
||||||
|
"success": false,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
success++
|
||||||
|
successIDs = append(successIDs, u.ID)
|
||||||
|
results = append(results, gin.H{
|
||||||
|
"account_id": u.ID,
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, gin.H{
|
response.Success(c, gin.H{
|
||||||
"success": len(updates),
|
"success": success,
|
||||||
"failed": 0,
|
"failed": failed,
|
||||||
|
"success_ids": successIDs,
|
||||||
|
"failed_ids": failedIDs,
|
||||||
|
"results": results,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -219,9 +219,8 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
|||||||
log.Printf("[OpenAI Handler] Selecting account: groupID=%v model=%s", apiKey.GroupID, reqModel)
|
log.Printf("[OpenAI Handler] Selecting account: groupID=%v model=%s", apiKey.GroupID, reqModel)
|
||||||
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionHash, reqModel, failedAccountIDs)
|
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionHash, reqModel, failedAccountIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[OpenAI Handler] SelectAccount failed: %v", err)
|
log.Printf("[OpenAI Handler] SelectAccount failed: groupID=%v model=%s tried=%d err=%v", apiKey.GroupID, reqModel, len(failedAccountIDs), err)
|
||||||
if len(failedAccountIDs) == 0 {
|
if len(failedAccountIDs) == 0 {
|
||||||
log.Printf("[OpenAI Gateway] SelectAccount failed: %v", err)
|
|
||||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "Service temporarily unavailable", streamStarted)
|
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "Service temporarily unavailable", streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TransformGeminiToClaude 将 Gemini 响应转换为 Claude 格式(非流式)
|
// TransformGeminiToClaude 将 Gemini 响应转换为 Claude 格式(非流式)
|
||||||
@@ -345,13 +346,25 @@ func buildGroundingText(grounding *GeminiGroundingMetadata) string {
|
|||||||
// generateRandomID 生成密码学安全的随机 ID
|
// generateRandomID 生成密码学安全的随机 ID
|
||||||
func generateRandomID() string {
|
func generateRandomID() string {
|
||||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
result := make([]byte, 12)
|
id := make([]byte, 12)
|
||||||
randBytes := make([]byte, 12)
|
randBytes := make([]byte, 12)
|
||||||
if _, err := rand.Read(randBytes); err != nil {
|
if _, err := rand.Read(randBytes); err != nil {
|
||||||
panic("crypto/rand unavailable: " + err.Error())
|
// 避免在请求路径里 panic:极端情况下熵源不可用时降级为伪随机。
|
||||||
|
// 这里主要用于生成响应/工具调用的临时 ID,安全要求不高但需尽量避免碰撞。
|
||||||
|
seed := uint64(time.Now().UnixNano())
|
||||||
|
if err != nil {
|
||||||
|
seed ^= uint64(len(err.Error())) << 32
|
||||||
|
}
|
||||||
|
for i := range id {
|
||||||
|
seed ^= seed << 13
|
||||||
|
seed ^= seed >> 7
|
||||||
|
seed ^= seed << 17
|
||||||
|
id[i] = chars[int(seed)%len(chars)]
|
||||||
|
}
|
||||||
|
return string(id)
|
||||||
}
|
}
|
||||||
for i, b := range randBytes {
|
for i, b := range randBytes {
|
||||||
result[i] = chars[int(b)%len(chars)]
|
id[i] = chars[int(b)%len(chars)]
|
||||||
}
|
}
|
||||||
return string(result)
|
return string(id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,12 @@ const (
|
|||||||
|
|
||||||
// jitteredTTL 返回带随机抖动的 TTL,防止缓存雪崩
|
// jitteredTTL 返回带随机抖动的 TTL,防止缓存雪崩
|
||||||
func jitteredTTL() time.Duration {
|
func jitteredTTL() time.Duration {
|
||||||
jitter := time.Duration(rand.Int63n(int64(2*billingCacheJitter))) - billingCacheJitter
|
// 只做“减法抖动”,确保实际 TTL 不会超过 billingCacheTTL(避免上界预期被打破)。
|
||||||
return billingCacheTTL + jitter
|
if billingCacheJitter <= 0 {
|
||||||
|
return billingCacheTTL
|
||||||
|
}
|
||||||
|
jitter := time.Duration(rand.Int63n(int64(billingCacheJitter)))
|
||||||
|
return billingCacheTTL - jitter
|
||||||
}
|
}
|
||||||
|
|
||||||
// billingBalanceKey generates the Redis key for user balance cache.
|
// billingBalanceKey generates the Redis key for user balance cache.
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ var sseScannerBuf64KPool = sync.Pool{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSSEScannerBuf64K() *sseScannerBuf64K {
|
func getSSEScannerBuf64K() *sseScannerBuf64K {
|
||||||
return sseScannerBuf64KPool.Get().(*sseScannerBuf64K)
|
v := sseScannerBuf64KPool.Get()
|
||||||
|
buf, ok := v.(*sseScannerBuf64K)
|
||||||
|
if !ok || buf == nil {
|
||||||
|
return new(sseScannerBuf64K)
|
||||||
|
}
|
||||||
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func putSSEScannerBuf64K(buf *sseScannerBuf64K) {
|
func putSSEScannerBuf64K(buf *sseScannerBuf64K) {
|
||||||
|
|||||||
@@ -483,7 +483,11 @@ func (s *SubscriptionService) GetActiveSubscription(ctx context.Context, userID,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// singleflight 返回的也是缓存指针,需要浅拷贝
|
// singleflight 返回的也是缓存指针,需要浅拷贝
|
||||||
cp := *value.(*UserSubscription)
|
sub, ok := value.(*UserSubscription)
|
||||||
|
if !ok || sub == nil {
|
||||||
|
return nil, ErrSubscriptionNotFound
|
||||||
|
}
|
||||||
|
cp := *sub
|
||||||
return &cp, nil
|
return &cp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user