Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
33f58d58
Commit
33f58d58
authored
Jan 14, 2026
by
IanShaw027
Browse files
fix(ops): 修复告警状态验证和错误处理逻辑
- 增强告警事件状态验证,添加合法状态值检查 - 移除重试逻辑中的遗留字段赋值 - 修正仓库不可用时的错误类型 - 格式化测试文件代码
parent
1e169685
Changes
11
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/ops_alerts_handler.go
View file @
33f58d58
...
...
@@ -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"
)
...
...
backend/internal/repository/ops_repo.go
View file @
33f58d58
...
...
@@ -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
.
R
esolved
=
&
f
r
esolved
Filter
=
&
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
.
R
esolved
!=
nil
{
args
=
append
(
args
,
*
filter
.
R
esolved
)
if
r
esolved
Filter
!=
nil
{
args
=
append
(
args
,
*
r
esolved
Filter
)
clauses
=
append
(
clauses
,
"COALESCE(resolved,false) = $"
+
itoa
(
len
(
args
)))
}
if
len
(
filter
.
StatusCodes
)
>
0
{
...
...
backend/internal/repository/scheduler_snapshot_outbox_integration_test.go
View file @
33f58d58
...
...
@@ -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
,
},
...
...
backend/internal/repository/usage_log_repo_integration_test.go
View file @
33f58d58
...
...
@@ -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
)
...
...
backend/internal/server/api_contract_test.go
View file @
33f58d58
...
...
@@ -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
,
...
...
backend/internal/service/admin_service_bulk_update_test.go
View file @
33f58d58
...
...
@@ -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
)
{
...
...
backend/internal/service/ops_alert_evaluator_service.go
View file @
33f58d58
...
...
@@ -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
(
...
...
backend/internal/service/ops_alerts.go
View file @
33f58d58
...
...
@@ -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
)
...
...
backend/internal/service/ops_retry.go
View file @
33f58d58
...
...
@@ -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
)
==
""
{
...
...
backend/internal/service/ops_service.go
View file @
33f58d58
...
...
@@ -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
"
,
"
o
ps
error log not found
"
)
return
nil
,
infraerrors
.
ServiceUnavailable
(
"OPS_REPO_UNAVAILABLE
"
,
"
O
ps
repository not available
"
)
}
if
errorID
<=
0
{
return
nil
,
infraerrors
.
BadRequest
(
"OPS_ERROR_INVALID_ID"
,
"invalid error id"
)
...
...
frontend/src/i18n/locales/en.ts
View file @
33f58d58
...
...
@@ -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
'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment