openai_quota_reset_workflow.go 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. package service
  2. import (
  3. "context"
  4. "log/slog"
  5. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  6. )
  7. const (
  8. OpenAIQuotaResetWarningCacheRefreshFailed = "reset_credit_cache_refresh_failed"
  9. OpenAIQuotaResetWarningAccountRecoveryFailed = "account_state_recovery_failed"
  10. OpenAIQuotaResetWarningAccountRefreshFailed = "account_state_refresh_failed"
  11. )
  12. type openAIQuotaResetWorkflowQuota interface {
  13. QueryUsage(ctx context.Context, accountID int64) (*OpenAIQuotaUsage, error)
  14. CachePostResetSnapshot(ctx context.Context, accountID int64, usage *OpenAIQuotaUsage) error
  15. }
  16. type openAIQuotaResetWorkflowRecoverer interface {
  17. RecoverAccountState(ctx context.Context, accountID int64, options AccountRecoveryOptions) (*SuccessfulTestRecoveryResult, error)
  18. }
  19. // OpenAIQuotaResetPostProcessResult 汇总手动和自动用卡后的共享恢复结果。
  20. // WarningCode 允许上游消费成功但本地恢复部分失败时仍然准确呈现状态。
  21. type OpenAIQuotaResetPostProcessResult struct {
  22. Quota *OpenAIQuotaUsage
  23. Account *Account
  24. CacheRefreshed bool
  25. AccountStateRecovered bool
  26. WarningCode string
  27. }
  28. // RunOpenAIQuotaResetPostProcess 按“解除限流、刷新额度缓存、刷新账号行”的固定顺序
  29. // 执行消费后恢复,避免手动和自动入口在失败语义上逐渐分叉。
  30. func RunOpenAIQuotaResetPostProcess(
  31. ctx context.Context,
  32. accountID int64,
  33. quota openAIQuotaResetWorkflowQuota,
  34. recoverer openAIQuotaResetWorkflowRecoverer,
  35. loadAccount func(context.Context, int64) (*Account, error),
  36. ) OpenAIQuotaResetPostProcessResult {
  37. result := OpenAIQuotaResetPostProcessResult{}
  38. if recoverer == nil {
  39. result.WarningCode = OpenAIQuotaResetWarningAccountRecoveryFailed
  40. return result
  41. }
  42. if _, err := recoverer.RecoverAccountState(ctx, accountID, AccountRecoveryOptions{InvalidateToken: true}); err != nil {
  43. slog.Warn("openai_quota_reset_account_recovery_failed", "account_id", accountID, "error_code", infraerrors.Reason(err))
  44. result.WarningCode = OpenAIQuotaResetWarningAccountRecoveryFailed
  45. return result
  46. }
  47. result.AccountStateRecovered = true
  48. if quota != nil {
  49. usage, usageErr := quota.QueryUsage(ctx, accountID)
  50. switch {
  51. case usageErr != nil || usage == nil:
  52. slog.Warn("openai_quota_reset_cache_refresh_failed", "account_id", accountID, "error_code", infraerrors.Reason(usageErr))
  53. result.WarningCode = OpenAIQuotaResetWarningCacheRefreshFailed
  54. default:
  55. if err := quota.CachePostResetSnapshot(ctx, accountID, usage); err != nil {
  56. slog.Warn("openai_quota_reset_cache_refresh_failed", "account_id", accountID, "error_code", infraerrors.Reason(err))
  57. result.WarningCode = OpenAIQuotaResetWarningCacheRefreshFailed
  58. } else {
  59. result.Quota = usage
  60. result.CacheRefreshed = true
  61. }
  62. }
  63. }
  64. if loadAccount == nil {
  65. return result
  66. }
  67. account, err := loadAccount(ctx, accountID)
  68. if err != nil {
  69. slog.Warn("openai_quota_reset_account_refresh_failed", "account_id", accountID, "error_code", infraerrors.Reason(err))
  70. if result.WarningCode == "" {
  71. result.WarningCode = OpenAIQuotaResetWarningAccountRefreshFailed
  72. }
  73. return result
  74. }
  75. result.Account = account
  76. return result
  77. }