diff --git a/backend/internal/handler/admin/ops_alerts_handler.go b/backend/internal/handler/admin/ops_alerts_handler.go index e7ad693b..8dce68c8 100644 --- a/backend/internal/handler/admin/ops_alerts_handler.go +++ b/backend/internal/handler/admin/ops_alerts_handler.go @@ -544,8 +544,14 @@ func (h *OpsHandler) ListAlertEvents(c *gin.Context) { } } - // Cursor pagination - if rawTS := strings.TrimSpace(c.Query("before_fired_at")); rawTS != "" { + // Cursor pagination: both params must be provided together. + rawTS := strings.TrimSpace(c.Query("before_fired_at")) + rawID := strings.TrimSpace(c.Query("before_id")) + if (rawTS == "") != (rawID == "") { + response.BadRequest(c, "before_fired_at and before_id must be provided together") + return + } + if rawTS != "" { ts, err := time.Parse(time.RFC3339Nano, rawTS) if err != nil { if t2, err2 := time.Parse(time.RFC3339, rawTS); err2 == nil { @@ -557,7 +563,7 @@ func (h *OpsHandler) ListAlertEvents(c *gin.Context) { } filter.BeforeFiredAt = &ts } - if rawID := strings.TrimSpace(c.Query("before_id")); rawID != "" { + if rawID != "" { id, err := strconv.ParseInt(rawID, 10, 64) if err != nil || id <= 0 { response.BadRequest(c, "Invalid before_id") diff --git a/backend/internal/repository/ops_repo.go b/backend/internal/repository/ops_repo.go index d9d71867..08dc0c22 100644 --- a/backend/internal/repository/ops_repo.go +++ b/backend/internal/repository/ops_repo.go @@ -925,9 +925,13 @@ func buildOpsErrorLogsWhere(filter *service.OpsErrorLogFilter) (string, []any) { // ops_error_logs stores client-visible error requests (status>=400), // but we also persist "recovered" upstream errors (status<400) for upstream health visibility. // By default, keep list endpoints scoped to unresolved records if the caller didn't specify. - if filter != nil && filter.Resolved == nil { + resolvedFilter := (*bool)(nil) + if filter != nil { + resolvedFilter = filter.Resolved + } + if resolvedFilter == nil { f := false - filter.Resolved = &f + resolvedFilter = &f } // Keep list endpoints scoped to client errors unless explicitly filtering upstream phase. if phaseFilter != "upstream" { @@ -967,8 +971,8 @@ func buildOpsErrorLogsWhere(filter *service.OpsErrorLogFilter) (string, []any) { args = append(args, source) clauses = append(clauses, "LOWER(COALESCE(error_source,'')) = $"+itoa(len(args))) } - if filter.Resolved != nil { - args = append(args, *filter.Resolved) + if resolvedFilter != nil { + args = append(args, *resolvedFilter) clauses = append(clauses, "COALESCE(resolved,false) = $"+itoa(len(args))) } if len(filter.StatusCodes) > 0 { diff --git a/backend/internal/repository/scheduler_snapshot_outbox_integration_test.go b/backend/internal/repository/scheduler_snapshot_outbox_integration_test.go index dede6014..e442a125 100644 --- a/backend/internal/repository/scheduler_snapshot_outbox_integration_test.go +++ b/backend/internal/repository/scheduler_snapshot_outbox_integration_test.go @@ -27,7 +27,7 @@ func TestSchedulerSnapshotOutboxReplay(t *testing.T) { RunMode: config.RunModeStandard, Gateway: config.GatewayConfig{ Scheduling: config.GatewaySchedulingConfig{ - OutboxPollIntervalSeconds: 1, + OutboxPollIntervalSeconds: 1, FullRebuildIntervalSeconds: 0, DbFallbackEnabled: true, }, diff --git a/backend/internal/repository/usage_log_repo_integration_test.go b/backend/internal/repository/usage_log_repo_integration_test.go index 3f90e49e..5e40bb52 100644 --- a/backend/internal/repository/usage_log_repo_integration_test.go +++ b/backend/internal/repository/usage_log_repo_integration_test.go @@ -416,8 +416,8 @@ func (s *UsageLogRepoSuite) TestDashboardAggregationConsistency() { // 使用固定的时间偏移确保 hour1 和 hour2 在同一天且都在过去 // 选择当天 02:00 和 03:00 作为测试时间点(基于 now 的日期) dayStart := truncateToDayUTC(now) - hour1 := dayStart.Add(2 * time.Hour) // 当天 02:00 - hour2 := dayStart.Add(3 * time.Hour) // 当天 03:00 + hour1 := dayStart.Add(2 * time.Hour) // 当天 02:00 + hour2 := dayStart.Add(3 * time.Hour) // 当天 03:00 // 如果当前时间早于 hour2,则使用昨天的时间 if now.Before(hour2.Add(time.Hour)) { dayStart = dayStart.Add(-24 * time.Hour) diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index d96732bd..34ac5611 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -262,11 +262,11 @@ func TestAPIContracts(t *testing.T) { name: "GET /api/v1/admin/settings", setup: func(t *testing.T, deps *contractDeps) { t.Helper() - deps.settingRepo.SetAll(map[string]string{ - service.SettingKeyRegistrationEnabled: "true", - service.SettingKeyEmailVerifyEnabled: "false", + deps.settingRepo.SetAll(map[string]string{ + service.SettingKeyRegistrationEnabled: "true", + service.SettingKeyEmailVerifyEnabled: "false", - service.SettingKeySMTPHost: "smtp.example.com", + service.SettingKeySMTPHost: "smtp.example.com", service.SettingKeySMTPPort: "587", service.SettingKeySMTPUsername: "user", service.SettingKeySMTPPassword: "secret", @@ -285,15 +285,15 @@ func TestAPIContracts(t *testing.T) { service.SettingKeyContactInfo: "support", service.SettingKeyDocURL: "https://docs.example.com", - service.SettingKeyDefaultConcurrency: "5", - service.SettingKeyDefaultBalance: "1.25", + service.SettingKeyDefaultConcurrency: "5", + service.SettingKeyDefaultBalance: "1.25", - service.SettingKeyOpsMonitoringEnabled: "false", - service.SettingKeyOpsRealtimeMonitoringEnabled: "true", - service.SettingKeyOpsQueryModeDefault: "auto", - service.SettingKeyOpsMetricsIntervalSeconds: "60", - }) - }, + service.SettingKeyOpsMonitoringEnabled: "false", + service.SettingKeyOpsRealtimeMonitoringEnabled: "true", + service.SettingKeyOpsQueryModeDefault: "auto", + service.SettingKeyOpsMetricsIntervalSeconds: "60", + }) + }, method: http.MethodGet, path: "/api/v1/admin/settings", wantStatus: http.StatusOK, diff --git a/backend/internal/service/admin_service_bulk_update_test.go b/backend/internal/service/admin_service_bulk_update_test.go index ef621213..662b95fb 100644 --- a/backend/internal/service/admin_service_bulk_update_test.go +++ b/backend/internal/service/admin_service_bulk_update_test.go @@ -12,9 +12,9 @@ import ( type accountRepoStubForBulkUpdate struct { accountRepoStub - bulkUpdateErr error - bulkUpdateIDs []int64 - bindGroupErrByID map[int64]error + bulkUpdateErr error + bulkUpdateIDs []int64 + bindGroupErrByID map[int64]error } func (s *accountRepoStubForBulkUpdate) BulkUpdate(_ context.Context, ids []int64, _ AccountBulkUpdate) (int64, error) { diff --git a/backend/internal/service/ops_alert_evaluator_service.go b/backend/internal/service/ops_alert_evaluator_service.go index 3efa11d2..a0c93772 100644 --- a/backend/internal/service/ops_alert_evaluator_service.go +++ b/backend/internal/service/ops_alert_evaluator_service.go @@ -206,7 +206,7 @@ func (s *OpsAlertEvaluatorService) evaluateOnce(interval time.Duration) { continue } - scopePlatform, scopeGroupID := parseOpsAlertRuleScope(rule.Filters) + scopePlatform, scopeGroupID, scopeRegion := parseOpsAlertRuleScope(rule.Filters) windowMinutes := rule.WindowMinutes if windowMinutes <= 0 { @@ -239,7 +239,7 @@ func (s *OpsAlertEvaluatorService) evaluateOnce(interval time.Duration) { // Scoped silencing: if a matching silence exists, skip creating a firing event. if s.opsService != nil { platform := strings.TrimSpace(scopePlatform) - region := (*string)(nil) + region := scopeRegion if platform != "" { if ok, err := s.opsService.IsAlertSilenced(ctx, rule.ID, platform, scopeGroupID, region, now); err == nil && ok { continue @@ -370,9 +370,9 @@ func requiredSustainedBreaches(sustainedMinutes int, interval time.Duration) int return required } -func parseOpsAlertRuleScope(filters map[string]any) (platform string, groupID *int64) { +func parseOpsAlertRuleScope(filters map[string]any) (platform string, groupID *int64, region *string) { if filters == nil { - return "", nil + return "", nil, nil } if v, ok := filters["platform"]; ok { if s, ok := v.(string); ok { @@ -403,7 +403,15 @@ func parseOpsAlertRuleScope(filters map[string]any) (platform string, groupID *i } } } - return platform, groupID + if v, ok := filters["region"]; ok { + if s, ok := v.(string); ok { + vv := strings.TrimSpace(s) + if vv != "" { + region = &vv + } + } + } + return platform, groupID, region } func (s *OpsAlertEvaluatorService) computeRuleMetric( diff --git a/backend/internal/service/ops_alerts.go b/backend/internal/service/ops_alerts.go index c2bb4e7b..b4c09824 100644 --- a/backend/internal/service/ops_alerts.go +++ b/backend/internal/service/ops_alerts.go @@ -208,7 +208,11 @@ func (s *OpsService) UpdateAlertEventStatus(ctx context.Context, eventID int64, if eventID <= 0 { return infraerrors.BadRequest("INVALID_EVENT_ID", "invalid event id") } - if strings.TrimSpace(status) == "" { + status = strings.TrimSpace(status) + if status == "" { + return infraerrors.BadRequest("INVALID_STATUS", "invalid status") + } + if status != OpsAlertStatusResolved && status != OpsAlertStatusManualResolved { return infraerrors.BadRequest("INVALID_STATUS", "invalid status") } return s.opsRepo.UpdateAlertEventStatus(ctx, eventID, status, resolvedAt) diff --git a/backend/internal/service/ops_retry.go b/backend/internal/service/ops_retry.go index 2cbb8ced..f52e2b77 100644 --- a/backend/internal/service/ops_retry.go +++ b/backend/internal/service/ops_retry.go @@ -220,11 +220,8 @@ func (s *OpsService) RetryError(ctx context.Context, requestedByUserID int64, er msg := result.ErrorMessage updateErrMsg = &msg } + // Keep legacy result_request_id empty; use upstream_request_id instead. var resultRequestID *string - if strings.TrimSpace(result.UpstreamRequestID) != "" { - v := result.UpstreamRequestID - resultRequestID = &v - } finalStatus := result.Status if strings.TrimSpace(finalStatus) == "" { diff --git a/backend/internal/service/ops_service.go b/backend/internal/service/ops_service.go index d9984659..3a40c0f6 100644 --- a/backend/internal/service/ops_service.go +++ b/backend/internal/service/ops_service.go @@ -261,7 +261,7 @@ func (s *OpsService) ListRetryAttemptsByErrorID(ctx context.Context, errorID int return nil, err } if s.opsRepo == nil { - return nil, infraerrors.NotFound("OPS_ERROR_NOT_FOUND", "ops error log not found") + return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available") } if errorID <= 0 { return nil, infraerrors.BadRequest("OPS_ERROR_INVALID_ID", "invalid error id") diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index b9fad5be..3c6d8f84 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -150,12 +150,13 @@ export default { invalidEmail: 'Please enter a valid email address', optional: 'optional', selectOption: 'Select an option', - searchPlaceholder: 'Search...', - noOptionsFound: 'No options found', - noGroupsAvailable: 'No groups available', - unknownError: 'Unknown error occurred', - saving: 'Saving...', - selectedCount: '({count} selected)', refresh: 'Refresh', + searchPlaceholder: 'Search...', + noOptionsFound: 'No options found', + noGroupsAvailable: 'No groups available', + unknownError: 'Unknown error occurred', + saving: 'Saving...', + selectedCount: '({count} selected)', + refresh: 'Refresh', settings: 'Settings', notAvailable: 'N/A', now: 'Now',