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
64236361
"backend/internal/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "0743652d92265e94a9e9b88d0d9e9ea8813acb04"
Commit
64236361
authored
Feb 12, 2026
by
yangjianbo
Browse files
Merge branch 'test' into dev
parents
2d6066f9
b6aaee01
Changes
92
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/ops_log_runtime_test.go
0 → 100644
View file @
64236361
package
service
import
(
"context"
"encoding/json"
"errors"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
)
type
runtimeSettingRepoStub
struct
{
values
map
[
string
]
string
deleted
map
[
string
]
bool
setCalls
int
getValueFn
func
(
key
string
)
(
string
,
error
)
setFn
func
(
key
,
value
string
)
error
deleteFn
func
(
key
string
)
error
}
func
newRuntimeSettingRepoStub
()
*
runtimeSettingRepoStub
{
return
&
runtimeSettingRepoStub
{
values
:
map
[
string
]
string
{},
deleted
:
map
[
string
]
bool
{},
}
}
func
(
s
*
runtimeSettingRepoStub
)
Get
(
ctx
context
.
Context
,
key
string
)
(
*
Setting
,
error
)
{
value
,
err
:=
s
.
GetValue
(
ctx
,
key
)
if
err
!=
nil
{
return
nil
,
err
}
return
&
Setting
{
Key
:
key
,
Value
:
value
},
nil
}
func
(
s
*
runtimeSettingRepoStub
)
GetValue
(
_
context
.
Context
,
key
string
)
(
string
,
error
)
{
if
s
.
getValueFn
!=
nil
{
return
s
.
getValueFn
(
key
)
}
value
,
ok
:=
s
.
values
[
key
]
if
!
ok
{
return
""
,
ErrSettingNotFound
}
return
value
,
nil
}
func
(
s
*
runtimeSettingRepoStub
)
Set
(
_
context
.
Context
,
key
,
value
string
)
error
{
if
s
.
setFn
!=
nil
{
if
err
:=
s
.
setFn
(
key
,
value
);
err
!=
nil
{
return
err
}
}
s
.
values
[
key
]
=
value
s
.
setCalls
++
return
nil
}
func
(
s
*
runtimeSettingRepoStub
)
GetMultiple
(
_
context
.
Context
,
keys
[]
string
)
(
map
[
string
]
string
,
error
)
{
out
:=
make
(
map
[
string
]
string
,
len
(
keys
))
for
_
,
key
:=
range
keys
{
if
value
,
ok
:=
s
.
values
[
key
];
ok
{
out
[
key
]
=
value
}
}
return
out
,
nil
}
func
(
s
*
runtimeSettingRepoStub
)
SetMultiple
(
_
context
.
Context
,
settings
map
[
string
]
string
)
error
{
for
key
,
value
:=
range
settings
{
s
.
values
[
key
]
=
value
}
return
nil
}
func
(
s
*
runtimeSettingRepoStub
)
GetAll
(
_
context
.
Context
)
(
map
[
string
]
string
,
error
)
{
out
:=
make
(
map
[
string
]
string
,
len
(
s
.
values
))
for
key
,
value
:=
range
s
.
values
{
out
[
key
]
=
value
}
return
out
,
nil
}
func
(
s
*
runtimeSettingRepoStub
)
Delete
(
_
context
.
Context
,
key
string
)
error
{
if
s
.
deleteFn
!=
nil
{
if
err
:=
s
.
deleteFn
(
key
);
err
!=
nil
{
return
err
}
}
if
_
,
ok
:=
s
.
values
[
key
];
!
ok
{
return
ErrSettingNotFound
}
delete
(
s
.
values
,
key
)
s
.
deleted
[
key
]
=
true
return
nil
}
func
TestUpdateRuntimeLogConfig_InvalidConfigShouldNotApply
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
svc
:=
&
OpsService
{
settingRepo
:
repo
,
cfg
:
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"info"
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
false
,
Initial
:
100
,
Thereafter
:
100
,
},
},
},
}
if
err
:=
logger
.
Init
(
logger
.
InitOptions
{
Level
:
"info"
,
Format
:
"json"
,
ServiceName
:
"sub2api"
,
Environment
:
"test"
,
Output
:
logger
.
OutputOptions
{
ToStdout
:
true
,
ToFile
:
false
,
},
});
err
!=
nil
{
t
.
Fatalf
(
"init logger: %v"
,
err
)
}
_
,
err
:=
svc
.
UpdateRuntimeLogConfig
(
context
.
Background
(),
&
OpsRuntimeLogConfig
{
Level
:
"trace"
,
EnableSampling
:
true
,
SamplingInitial
:
100
,
SamplingNext
:
100
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
RetentionDays
:
30
,
},
1
)
if
err
==
nil
{
t
.
Fatalf
(
"expected validation error"
)
}
if
logger
.
CurrentLevel
()
!=
"info"
{
t
.
Fatalf
(
"logger level changed unexpectedly: %s"
,
logger
.
CurrentLevel
())
}
if
repo
.
setCalls
!=
1
{
// GetRuntimeLogConfig() 会在 key 缺失时写入默认值,此处应只有这一次持久化。
t
.
Fatalf
(
"unexpected set calls: %d"
,
repo
.
setCalls
)
}
}
func
TestResetRuntimeLogConfig_ShouldFallbackToBaseline
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
existing
:=
&
OpsRuntimeLogConfig
{
Level
:
"debug"
,
EnableSampling
:
true
,
SamplingInitial
:
50
,
SamplingNext
:
50
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
RetentionDays
:
60
,
Source
:
"runtime_setting"
,
}
raw
,
_
:=
json
.
Marshal
(
existing
)
repo
.
values
[
SettingKeyOpsRuntimeLogConfig
]
=
string
(
raw
)
svc
:=
&
OpsService
{
settingRepo
:
repo
,
cfg
:
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"warn"
,
Caller
:
false
,
StacktraceLevel
:
"fatal"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
false
,
Initial
:
100
,
Thereafter
:
100
,
},
},
Ops
:
config
.
OpsConfig
{
Cleanup
:
config
.
OpsCleanupConfig
{
ErrorLogRetentionDays
:
45
,
},
},
},
}
if
err
:=
logger
.
Init
(
logger
.
InitOptions
{
Level
:
"debug"
,
Format
:
"json"
,
ServiceName
:
"sub2api"
,
Environment
:
"test"
,
Output
:
logger
.
OutputOptions
{
ToStdout
:
true
,
ToFile
:
false
,
},
});
err
!=
nil
{
t
.
Fatalf
(
"init logger: %v"
,
err
)
}
resetCfg
,
err
:=
svc
.
ResetRuntimeLogConfig
(
context
.
Background
(),
9
)
if
err
!=
nil
{
t
.
Fatalf
(
"ResetRuntimeLogConfig() error: %v"
,
err
)
}
if
resetCfg
.
Source
!=
"baseline"
{
t
.
Fatalf
(
"source = %q, want baseline"
,
resetCfg
.
Source
)
}
if
resetCfg
.
Level
!=
"warn"
{
t
.
Fatalf
(
"level = %q, want warn"
,
resetCfg
.
Level
)
}
if
resetCfg
.
RetentionDays
!=
45
{
t
.
Fatalf
(
"retention_days = %d, want 45"
,
resetCfg
.
RetentionDays
)
}
if
logger
.
CurrentLevel
()
!=
"warn"
{
t
.
Fatalf
(
"logger level = %q, want warn"
,
logger
.
CurrentLevel
())
}
if
!
repo
.
deleted
[
SettingKeyOpsRuntimeLogConfig
]
{
t
.
Fatalf
(
"runtime setting key should be deleted"
)
}
}
func
TestResetRuntimeLogConfig_InvalidOperator
(
t
*
testing
.
T
)
{
svc
:=
&
OpsService
{
settingRepo
:
newRuntimeSettingRepoStub
()}
_
,
err
:=
svc
.
ResetRuntimeLogConfig
(
context
.
Background
(),
0
)
if
err
==
nil
{
t
.
Fatalf
(
"expected invalid operator error"
)
}
if
err
.
Error
()
!=
"invalid operator id"
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
}
func
TestGetRuntimeLogConfig_InvalidJSONFallback
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
repo
.
values
[
SettingKeyOpsRuntimeLogConfig
]
=
`{invalid-json}`
svc
:=
&
OpsService
{
settingRepo
:
repo
,
cfg
:
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"warn"
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
false
,
Initial
:
100
,
Thereafter
:
100
,
},
},
},
}
got
,
err
:=
svc
.
GetRuntimeLogConfig
(
context
.
Background
())
if
err
!=
nil
{
t
.
Fatalf
(
"GetRuntimeLogConfig() error: %v"
,
err
)
}
if
got
.
Level
!=
"warn"
{
t
.
Fatalf
(
"level = %q, want warn"
,
got
.
Level
)
}
}
func
TestUpdateRuntimeLogConfig_PersistFailureRollback
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
oldCfg
:=
&
OpsRuntimeLogConfig
{
Level
:
"info"
,
EnableSampling
:
false
,
SamplingInitial
:
100
,
SamplingNext
:
100
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
RetentionDays
:
30
,
}
raw
,
_
:=
json
.
Marshal
(
oldCfg
)
repo
.
values
[
SettingKeyOpsRuntimeLogConfig
]
=
string
(
raw
)
repo
.
setFn
=
func
(
key
,
value
string
)
error
{
if
key
==
SettingKeyOpsRuntimeLogConfig
{
return
errors
.
New
(
"db down"
)
}
return
nil
}
svc
:=
&
OpsService
{
settingRepo
:
repo
,
cfg
:
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"info"
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
false
,
Initial
:
100
,
Thereafter
:
100
,
},
},
},
}
if
err
:=
logger
.
Init
(
logger
.
InitOptions
{
Level
:
"info"
,
Format
:
"json"
,
ServiceName
:
"sub2api"
,
Environment
:
"test"
,
Output
:
logger
.
OutputOptions
{
ToStdout
:
true
,
ToFile
:
false
,
},
});
err
!=
nil
{
t
.
Fatalf
(
"init logger: %v"
,
err
)
}
_
,
err
:=
svc
.
UpdateRuntimeLogConfig
(
context
.
Background
(),
&
OpsRuntimeLogConfig
{
Level
:
"debug"
,
EnableSampling
:
false
,
SamplingInitial
:
100
,
SamplingNext
:
100
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
RetentionDays
:
30
,
},
5
)
if
err
==
nil
{
t
.
Fatalf
(
"expected persist error"
)
}
// Persist failure should rollback runtime level back to old effective level.
if
logger
.
CurrentLevel
()
!=
"info"
{
t
.
Fatalf
(
"logger level should rollback to info, got %s"
,
logger
.
CurrentLevel
())
}
}
func
TestApplyRuntimeLogConfigOnStartup
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
cfgRaw
:=
`{"level":"debug","enable_sampling":false,"sampling_initial":100,"sampling_thereafter":100,"caller":true,"stacktrace_level":"error","retention_days":30}`
repo
.
values
[
SettingKeyOpsRuntimeLogConfig
]
=
cfgRaw
svc
:=
&
OpsService
{
settingRepo
:
repo
,
cfg
:
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"info"
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
false
,
Initial
:
100
,
Thereafter
:
100
,
},
},
},
}
if
err
:=
logger
.
Init
(
logger
.
InitOptions
{
Level
:
"info"
,
Format
:
"json"
,
ServiceName
:
"sub2api"
,
Environment
:
"test"
,
Output
:
logger
.
OutputOptions
{
ToStdout
:
true
,
ToFile
:
false
,
},
});
err
!=
nil
{
t
.
Fatalf
(
"init logger: %v"
,
err
)
}
svc
.
applyRuntimeLogConfigOnStartup
(
context
.
Background
())
if
logger
.
CurrentLevel
()
!=
"debug"
{
t
.
Fatalf
(
"expected startup apply debug, got %s"
,
logger
.
CurrentLevel
())
}
}
func
TestDefaultNormalizeAndValidateRuntimeLogConfig
(
t
*
testing
.
T
)
{
defaults
:=
defaultOpsRuntimeLogConfig
(
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"DEBUG"
,
Caller
:
false
,
StacktraceLevel
:
"FATAL"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
true
,
Initial
:
50
,
Thereafter
:
20
,
},
},
Ops
:
config
.
OpsConfig
{
Cleanup
:
config
.
OpsCleanupConfig
{
ErrorLogRetentionDays
:
7
,
},
},
})
if
defaults
.
Level
!=
"debug"
||
defaults
.
StacktraceLevel
!=
"fatal"
||
defaults
.
RetentionDays
!=
7
{
t
.
Fatalf
(
"unexpected defaults: %+v"
,
defaults
)
}
cfg
:=
&
OpsRuntimeLogConfig
{
Level
:
" "
,
EnableSampling
:
true
,
SamplingInitial
:
0
,
SamplingNext
:
-
1
,
Caller
:
true
,
StacktraceLevel
:
""
,
RetentionDays
:
0
,
}
normalizeOpsRuntimeLogConfig
(
cfg
,
defaults
)
if
cfg
.
Level
!=
"debug"
||
cfg
.
StacktraceLevel
!=
"fatal"
{
t
.
Fatalf
(
"normalize level/stacktrace failed: %+v"
,
cfg
)
}
if
cfg
.
SamplingInitial
!=
50
||
cfg
.
SamplingNext
!=
20
||
cfg
.
RetentionDays
!=
7
{
t
.
Fatalf
(
"normalize numeric defaults failed: %+v"
,
cfg
)
}
if
err
:=
validateOpsRuntimeLogConfig
(
cfg
);
err
!=
nil
{
t
.
Fatalf
(
"validate normalized config should pass: %v"
,
err
)
}
}
func
TestValidateRuntimeLogConfigErrors
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
name
string
cfg
*
OpsRuntimeLogConfig
}{
{
name
:
"nil"
,
cfg
:
nil
},
{
name
:
"bad level"
,
cfg
:
&
OpsRuntimeLogConfig
{
Level
:
"trace"
,
StacktraceLevel
:
"error"
,
SamplingInitial
:
1
,
SamplingNext
:
1
,
RetentionDays
:
1
}},
{
name
:
"bad stack"
,
cfg
:
&
OpsRuntimeLogConfig
{
Level
:
"info"
,
StacktraceLevel
:
"warn"
,
SamplingInitial
:
1
,
SamplingNext
:
1
,
RetentionDays
:
1
}},
{
name
:
"bad initial"
,
cfg
:
&
OpsRuntimeLogConfig
{
Level
:
"info"
,
StacktraceLevel
:
"error"
,
SamplingInitial
:
0
,
SamplingNext
:
1
,
RetentionDays
:
1
}},
{
name
:
"bad next"
,
cfg
:
&
OpsRuntimeLogConfig
{
Level
:
"info"
,
StacktraceLevel
:
"error"
,
SamplingInitial
:
1
,
SamplingNext
:
0
,
RetentionDays
:
1
}},
{
name
:
"bad retention"
,
cfg
:
&
OpsRuntimeLogConfig
{
Level
:
"info"
,
StacktraceLevel
:
"error"
,
SamplingInitial
:
1
,
SamplingNext
:
1
,
RetentionDays
:
0
}},
}
for
_
,
tc
:=
range
cases
{
t
.
Run
(
tc
.
name
,
func
(
t
*
testing
.
T
)
{
if
err
:=
validateOpsRuntimeLogConfig
(
tc
.
cfg
);
err
==
nil
{
t
.
Fatalf
(
"expected validation error"
)
}
})
}
}
func
TestGetRuntimeLogConfigFallbackAndErrors
(
t
*
testing
.
T
)
{
var
nilSvc
*
OpsService
cfg
,
err
:=
nilSvc
.
GetRuntimeLogConfig
(
context
.
Background
())
if
err
!=
nil
{
t
.
Fatalf
(
"nil svc should fallback default: %v"
,
err
)
}
if
cfg
.
Level
!=
"info"
{
t
.
Fatalf
(
"unexpected nil svc default level: %s"
,
cfg
.
Level
)
}
repo
:=
newRuntimeSettingRepoStub
()
repo
.
getValueFn
=
func
(
key
string
)
(
string
,
error
)
{
return
""
,
errors
.
New
(
"boom"
)
}
svc
:=
&
OpsService
{
settingRepo
:
repo
,
cfg
:
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"warn"
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
false
,
Initial
:
100
,
Thereafter
:
100
,
},
},
},
}
if
_
,
err
:=
svc
.
GetRuntimeLogConfig
(
context
.
Background
());
err
==
nil
{
t
.
Fatalf
(
"expected get value error"
)
}
}
func
TestUpdateRuntimeLogConfig_PreconditionErrors
(
t
*
testing
.
T
)
{
svc
:=
&
OpsService
{}
if
_
,
err
:=
svc
.
UpdateRuntimeLogConfig
(
context
.
Background
(),
&
OpsRuntimeLogConfig
{},
1
);
err
==
nil
{
t
.
Fatalf
(
"expected setting repo not initialized"
)
}
svc
=
&
OpsService
{
settingRepo
:
newRuntimeSettingRepoStub
()}
if
_
,
err
:=
svc
.
UpdateRuntimeLogConfig
(
context
.
Background
(),
nil
,
1
);
err
==
nil
{
t
.
Fatalf
(
"expected invalid config"
)
}
if
_
,
err
:=
svc
.
UpdateRuntimeLogConfig
(
context
.
Background
(),
&
OpsRuntimeLogConfig
{
Level
:
"info"
,
StacktraceLevel
:
"error"
,
SamplingInitial
:
1
,
SamplingNext
:
1
,
RetentionDays
:
1
,
},
0
);
err
==
nil
{
t
.
Fatalf
(
"expected invalid operator"
)
}
}
func
TestUpdateRuntimeLogConfig_Success
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
svc
:=
&
OpsService
{
settingRepo
:
repo
,
cfg
:
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"info"
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
false
,
Initial
:
100
,
Thereafter
:
100
,
},
},
},
}
if
err
:=
logger
.
Init
(
logger
.
InitOptions
{
Level
:
"info"
,
Format
:
"json"
,
ServiceName
:
"sub2api"
,
Environment
:
"test"
,
Output
:
logger
.
OutputOptions
{
ToStdout
:
true
,
ToFile
:
false
,
},
});
err
!=
nil
{
t
.
Fatalf
(
"init logger: %v"
,
err
)
}
next
,
err
:=
svc
.
UpdateRuntimeLogConfig
(
context
.
Background
(),
&
OpsRuntimeLogConfig
{
Level
:
"debug"
,
EnableSampling
:
false
,
SamplingInitial
:
100
,
SamplingNext
:
100
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
RetentionDays
:
30
,
},
2
)
if
err
!=
nil
{
t
.
Fatalf
(
"UpdateRuntimeLogConfig() error: %v"
,
err
)
}
if
next
.
Source
!=
"runtime_setting"
||
next
.
UpdatedByUserID
!=
2
||
next
.
UpdatedAt
==
""
{
t
.
Fatalf
(
"unexpected metadata: %+v"
,
next
)
}
if
logger
.
CurrentLevel
()
!=
"debug"
{
t
.
Fatalf
(
"expected applied level debug, got %s"
,
logger
.
CurrentLevel
())
}
}
func
TestResetRuntimeLogConfig_IgnoreNotFoundDelete
(
t
*
testing
.
T
)
{
repo
:=
newRuntimeSettingRepoStub
()
repo
.
deleteFn
=
func
(
key
string
)
error
{
return
ErrSettingNotFound
}
svc
:=
&
OpsService
{
settingRepo
:
repo
,
cfg
:
&
config
.
Config
{
Log
:
config
.
LogConfig
{
Level
:
"info"
,
Caller
:
true
,
StacktraceLevel
:
"error"
,
Sampling
:
config
.
LogSamplingConfig
{
Enabled
:
false
,
Initial
:
100
,
Thereafter
:
100
,
},
},
},
}
if
_
,
err
:=
svc
.
ResetRuntimeLogConfig
(
context
.
Background
(),
1
);
err
!=
nil
{
t
.
Fatalf
(
"reset should ignore ErrSettingNotFound: %v"
,
err
)
}
}
func
TestApplyRuntimeLogConfigHelpers
(
t
*
testing
.
T
)
{
if
err
:=
applyOpsRuntimeLogConfig
(
nil
);
err
==
nil
{
t
.
Fatalf
(
"expected nil config error"
)
}
normalizeOpsRuntimeLogConfig
(
nil
,
&
OpsRuntimeLogConfig
{
Level
:
"info"
})
normalizeOpsRuntimeLogConfig
(
&
OpsRuntimeLogConfig
{
Level
:
"debug"
},
nil
)
var
nilSvc
*
OpsService
nilSvc
.
applyRuntimeLogConfigOnStartup
(
context
.
Background
())
}
backend/internal/service/ops_models.go
View file @
64236361
...
@@ -2,6 +2,21 @@ package service
...
@@ -2,6 +2,21 @@ package service
import
"time"
import
"time"
type
OpsSystemLog
struct
{
ID
int64
`json:"id"`
CreatedAt
time
.
Time
`json:"created_at"`
Level
string
`json:"level"`
Component
string
`json:"component"`
Message
string
`json:"message"`
RequestID
string
`json:"request_id"`
ClientRequestID
string
`json:"client_request_id"`
UserID
*
int64
`json:"user_id"`
AccountID
*
int64
`json:"account_id"`
Platform
string
`json:"platform"`
Model
string
`json:"model"`
Extra
map
[
string
]
any
`json:"extra,omitempty"`
}
type
OpsErrorLog
struct
{
type
OpsErrorLog
struct
{
ID
int64
`json:"id"`
ID
int64
`json:"id"`
CreatedAt
time
.
Time
`json:"created_at"`
CreatedAt
time
.
Time
`json:"created_at"`
...
...
backend/internal/service/ops_port.go
View file @
64236361
...
@@ -10,6 +10,10 @@ type OpsRepository interface {
...
@@ -10,6 +10,10 @@ type OpsRepository interface {
ListErrorLogs
(
ctx
context
.
Context
,
filter
*
OpsErrorLogFilter
)
(
*
OpsErrorLogList
,
error
)
ListErrorLogs
(
ctx
context
.
Context
,
filter
*
OpsErrorLogFilter
)
(
*
OpsErrorLogList
,
error
)
GetErrorLogByID
(
ctx
context
.
Context
,
id
int64
)
(
*
OpsErrorLogDetail
,
error
)
GetErrorLogByID
(
ctx
context
.
Context
,
id
int64
)
(
*
OpsErrorLogDetail
,
error
)
ListRequestDetails
(
ctx
context
.
Context
,
filter
*
OpsRequestDetailFilter
)
([]
*
OpsRequestDetail
,
int64
,
error
)
ListRequestDetails
(
ctx
context
.
Context
,
filter
*
OpsRequestDetailFilter
)
([]
*
OpsRequestDetail
,
int64
,
error
)
BatchInsertSystemLogs
(
ctx
context
.
Context
,
inputs
[]
*
OpsInsertSystemLogInput
)
(
int64
,
error
)
ListSystemLogs
(
ctx
context
.
Context
,
filter
*
OpsSystemLogFilter
)
(
*
OpsSystemLogList
,
error
)
DeleteSystemLogs
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
)
(
int64
,
error
)
InsertSystemLogCleanupAudit
(
ctx
context
.
Context
,
input
*
OpsSystemLogCleanupAudit
)
error
InsertRetryAttempt
(
ctx
context
.
Context
,
input
*
OpsInsertRetryAttemptInput
)
(
int64
,
error
)
InsertRetryAttempt
(
ctx
context
.
Context
,
input
*
OpsInsertRetryAttemptInput
)
(
int64
,
error
)
UpdateRetryAttempt
(
ctx
context
.
Context
,
input
*
OpsUpdateRetryAttemptInput
)
error
UpdateRetryAttempt
(
ctx
context
.
Context
,
input
*
OpsUpdateRetryAttemptInput
)
error
...
@@ -205,6 +209,69 @@ type OpsInsertSystemMetricsInput struct {
...
@@ -205,6 +209,69 @@ type OpsInsertSystemMetricsInput struct {
ConcurrencyQueueDepth
*
int
ConcurrencyQueueDepth
*
int
}
}
type
OpsInsertSystemLogInput
struct
{
CreatedAt
time
.
Time
Level
string
Component
string
Message
string
RequestID
string
ClientRequestID
string
UserID
*
int64
AccountID
*
int64
Platform
string
Model
string
ExtraJSON
string
}
type
OpsSystemLogFilter
struct
{
StartTime
*
time
.
Time
EndTime
*
time
.
Time
Level
string
Component
string
RequestID
string
ClientRequestID
string
UserID
*
int64
AccountID
*
int64
Platform
string
Model
string
Query
string
Page
int
PageSize
int
}
type
OpsSystemLogCleanupFilter
struct
{
StartTime
*
time
.
Time
EndTime
*
time
.
Time
Level
string
Component
string
RequestID
string
ClientRequestID
string
UserID
*
int64
AccountID
*
int64
Platform
string
Model
string
Query
string
}
type
OpsSystemLogList
struct
{
Logs
[]
*
OpsSystemLog
`json:"logs"`
Total
int
`json:"total"`
Page
int
`json:"page"`
PageSize
int
`json:"page_size"`
}
type
OpsSystemLogCleanupAudit
struct
{
CreatedAt
time
.
Time
OperatorID
int64
Conditions
string
DeletedRows
int64
}
type
OpsSystemMetricsSnapshot
struct
{
type
OpsSystemMetricsSnapshot
struct
{
ID
int64
`json:"id"`
ID
int64
`json:"id"`
CreatedAt
time
.
Time
`json:"created_at"`
CreatedAt
time
.
Time
`json:"created_at"`
...
...
backend/internal/service/ops_repo_mock_test.go
0 → 100644
View file @
64236361
package
service
import
(
"context"
"time"
)
// opsRepoMock is a test-only OpsRepository implementation with optional function hooks.
type
opsRepoMock
struct
{
BatchInsertSystemLogsFn
func
(
ctx
context
.
Context
,
inputs
[]
*
OpsInsertSystemLogInput
)
(
int64
,
error
)
ListSystemLogsFn
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogFilter
)
(
*
OpsSystemLogList
,
error
)
DeleteSystemLogsFn
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
)
(
int64
,
error
)
InsertSystemLogCleanupAuditFn
func
(
ctx
context
.
Context
,
input
*
OpsSystemLogCleanupAudit
)
error
}
func
(
m
*
opsRepoMock
)
InsertErrorLog
(
ctx
context
.
Context
,
input
*
OpsInsertErrorLogInput
)
(
int64
,
error
)
{
return
0
,
nil
}
func
(
m
*
opsRepoMock
)
ListErrorLogs
(
ctx
context
.
Context
,
filter
*
OpsErrorLogFilter
)
(
*
OpsErrorLogList
,
error
)
{
return
&
OpsErrorLogList
{
Errors
:
[]
*
OpsErrorLog
{},
Page
:
1
,
PageSize
:
20
},
nil
}
func
(
m
*
opsRepoMock
)
GetErrorLogByID
(
ctx
context
.
Context
,
id
int64
)
(
*
OpsErrorLogDetail
,
error
)
{
return
&
OpsErrorLogDetail
{},
nil
}
func
(
m
*
opsRepoMock
)
ListRequestDetails
(
ctx
context
.
Context
,
filter
*
OpsRequestDetailFilter
)
([]
*
OpsRequestDetail
,
int64
,
error
)
{
return
[]
*
OpsRequestDetail
{},
0
,
nil
}
func
(
m
*
opsRepoMock
)
BatchInsertSystemLogs
(
ctx
context
.
Context
,
inputs
[]
*
OpsInsertSystemLogInput
)
(
int64
,
error
)
{
if
m
.
BatchInsertSystemLogsFn
!=
nil
{
return
m
.
BatchInsertSystemLogsFn
(
ctx
,
inputs
)
}
return
int64
(
len
(
inputs
)),
nil
}
func
(
m
*
opsRepoMock
)
ListSystemLogs
(
ctx
context
.
Context
,
filter
*
OpsSystemLogFilter
)
(
*
OpsSystemLogList
,
error
)
{
if
m
.
ListSystemLogsFn
!=
nil
{
return
m
.
ListSystemLogsFn
(
ctx
,
filter
)
}
return
&
OpsSystemLogList
{
Logs
:
[]
*
OpsSystemLog
{},
Total
:
0
,
Page
:
1
,
PageSize
:
50
},
nil
}
func
(
m
*
opsRepoMock
)
DeleteSystemLogs
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
)
(
int64
,
error
)
{
if
m
.
DeleteSystemLogsFn
!=
nil
{
return
m
.
DeleteSystemLogsFn
(
ctx
,
filter
)
}
return
0
,
nil
}
func
(
m
*
opsRepoMock
)
InsertSystemLogCleanupAudit
(
ctx
context
.
Context
,
input
*
OpsSystemLogCleanupAudit
)
error
{
if
m
.
InsertSystemLogCleanupAuditFn
!=
nil
{
return
m
.
InsertSystemLogCleanupAuditFn
(
ctx
,
input
)
}
return
nil
}
func
(
m
*
opsRepoMock
)
InsertRetryAttempt
(
ctx
context
.
Context
,
input
*
OpsInsertRetryAttemptInput
)
(
int64
,
error
)
{
return
0
,
nil
}
func
(
m
*
opsRepoMock
)
UpdateRetryAttempt
(
ctx
context
.
Context
,
input
*
OpsUpdateRetryAttemptInput
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
GetLatestRetryAttemptForError
(
ctx
context
.
Context
,
sourceErrorID
int64
)
(
*
OpsRetryAttempt
,
error
)
{
return
nil
,
nil
}
func
(
m
*
opsRepoMock
)
ListRetryAttemptsByErrorID
(
ctx
context
.
Context
,
sourceErrorID
int64
,
limit
int
)
([]
*
OpsRetryAttempt
,
error
)
{
return
[]
*
OpsRetryAttempt
{},
nil
}
func
(
m
*
opsRepoMock
)
UpdateErrorResolution
(
ctx
context
.
Context
,
errorID
int64
,
resolved
bool
,
resolvedByUserID
*
int64
,
resolvedRetryID
*
int64
,
resolvedAt
*
time
.
Time
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
GetWindowStats
(
ctx
context
.
Context
,
filter
*
OpsDashboardFilter
)
(
*
OpsWindowStats
,
error
)
{
return
&
OpsWindowStats
{},
nil
}
func
(
m
*
opsRepoMock
)
GetRealtimeTrafficSummary
(
ctx
context
.
Context
,
filter
*
OpsDashboardFilter
)
(
*
OpsRealtimeTrafficSummary
,
error
)
{
return
&
OpsRealtimeTrafficSummary
{},
nil
}
func
(
m
*
opsRepoMock
)
GetDashboardOverview
(
ctx
context
.
Context
,
filter
*
OpsDashboardFilter
)
(
*
OpsDashboardOverview
,
error
)
{
return
&
OpsDashboardOverview
{},
nil
}
func
(
m
*
opsRepoMock
)
GetThroughputTrend
(
ctx
context
.
Context
,
filter
*
OpsDashboardFilter
,
bucketSeconds
int
)
(
*
OpsThroughputTrendResponse
,
error
)
{
return
&
OpsThroughputTrendResponse
{},
nil
}
func
(
m
*
opsRepoMock
)
GetLatencyHistogram
(
ctx
context
.
Context
,
filter
*
OpsDashboardFilter
)
(
*
OpsLatencyHistogramResponse
,
error
)
{
return
&
OpsLatencyHistogramResponse
{},
nil
}
func
(
m
*
opsRepoMock
)
GetErrorTrend
(
ctx
context
.
Context
,
filter
*
OpsDashboardFilter
,
bucketSeconds
int
)
(
*
OpsErrorTrendResponse
,
error
)
{
return
&
OpsErrorTrendResponse
{},
nil
}
func
(
m
*
opsRepoMock
)
GetErrorDistribution
(
ctx
context
.
Context
,
filter
*
OpsDashboardFilter
)
(
*
OpsErrorDistributionResponse
,
error
)
{
return
&
OpsErrorDistributionResponse
{},
nil
}
func
(
m
*
opsRepoMock
)
GetOpenAITokenStats
(
ctx
context
.
Context
,
filter
*
OpsOpenAITokenStatsFilter
)
(
*
OpsOpenAITokenStatsResponse
,
error
)
{
return
&
OpsOpenAITokenStatsResponse
{},
nil
}
func
(
m
*
opsRepoMock
)
InsertSystemMetrics
(
ctx
context
.
Context
,
input
*
OpsInsertSystemMetricsInput
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
GetLatestSystemMetrics
(
ctx
context
.
Context
,
windowMinutes
int
)
(
*
OpsSystemMetricsSnapshot
,
error
)
{
return
&
OpsSystemMetricsSnapshot
{},
nil
}
func
(
m
*
opsRepoMock
)
UpsertJobHeartbeat
(
ctx
context
.
Context
,
input
*
OpsUpsertJobHeartbeatInput
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
ListJobHeartbeats
(
ctx
context
.
Context
)
([]
*
OpsJobHeartbeat
,
error
)
{
return
[]
*
OpsJobHeartbeat
{},
nil
}
func
(
m
*
opsRepoMock
)
ListAlertRules
(
ctx
context
.
Context
)
([]
*
OpsAlertRule
,
error
)
{
return
[]
*
OpsAlertRule
{},
nil
}
func
(
m
*
opsRepoMock
)
CreateAlertRule
(
ctx
context
.
Context
,
input
*
OpsAlertRule
)
(
*
OpsAlertRule
,
error
)
{
return
input
,
nil
}
func
(
m
*
opsRepoMock
)
UpdateAlertRule
(
ctx
context
.
Context
,
input
*
OpsAlertRule
)
(
*
OpsAlertRule
,
error
)
{
return
input
,
nil
}
func
(
m
*
opsRepoMock
)
DeleteAlertRule
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
ListAlertEvents
(
ctx
context
.
Context
,
filter
*
OpsAlertEventFilter
)
([]
*
OpsAlertEvent
,
error
)
{
return
[]
*
OpsAlertEvent
{},
nil
}
func
(
m
*
opsRepoMock
)
GetAlertEventByID
(
ctx
context
.
Context
,
eventID
int64
)
(
*
OpsAlertEvent
,
error
)
{
return
&
OpsAlertEvent
{},
nil
}
func
(
m
*
opsRepoMock
)
GetActiveAlertEvent
(
ctx
context
.
Context
,
ruleID
int64
)
(
*
OpsAlertEvent
,
error
)
{
return
nil
,
nil
}
func
(
m
*
opsRepoMock
)
GetLatestAlertEvent
(
ctx
context
.
Context
,
ruleID
int64
)
(
*
OpsAlertEvent
,
error
)
{
return
nil
,
nil
}
func
(
m
*
opsRepoMock
)
CreateAlertEvent
(
ctx
context
.
Context
,
event
*
OpsAlertEvent
)
(
*
OpsAlertEvent
,
error
)
{
return
event
,
nil
}
func
(
m
*
opsRepoMock
)
UpdateAlertEventStatus
(
ctx
context
.
Context
,
eventID
int64
,
status
string
,
resolvedAt
*
time
.
Time
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
UpdateAlertEventEmailSent
(
ctx
context
.
Context
,
eventID
int64
,
emailSent
bool
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
CreateAlertSilence
(
ctx
context
.
Context
,
input
*
OpsAlertSilence
)
(
*
OpsAlertSilence
,
error
)
{
return
input
,
nil
}
func
(
m
*
opsRepoMock
)
IsAlertSilenced
(
ctx
context
.
Context
,
ruleID
int64
,
platform
string
,
groupID
*
int64
,
region
*
string
,
now
time
.
Time
)
(
bool
,
error
)
{
return
false
,
nil
}
func
(
m
*
opsRepoMock
)
UpsertHourlyMetrics
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
UpsertDailyMetrics
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
)
error
{
return
nil
}
func
(
m
*
opsRepoMock
)
GetLatestHourlyBucketStart
(
ctx
context
.
Context
)
(
time
.
Time
,
bool
,
error
)
{
return
time
.
Time
{},
false
,
nil
}
func
(
m
*
opsRepoMock
)
GetLatestDailyBucketDate
(
ctx
context
.
Context
)
(
time
.
Time
,
bool
,
error
)
{
return
time
.
Time
{},
false
,
nil
}
var
_
OpsRepository
=
(
*
opsRepoMock
)(
nil
)
backend/internal/service/ops_service.go
View file @
64236361
...
@@ -37,6 +37,7 @@ type OpsService struct {
...
@@ -37,6 +37,7 @@ type OpsService struct {
openAIGatewayService
*
OpenAIGatewayService
openAIGatewayService
*
OpenAIGatewayService
geminiCompatService
*
GeminiMessagesCompatService
geminiCompatService
*
GeminiMessagesCompatService
antigravityGatewayService
*
AntigravityGatewayService
antigravityGatewayService
*
AntigravityGatewayService
systemLogSink
*
OpsSystemLogSink
}
}
func
NewOpsService
(
func
NewOpsService
(
...
@@ -50,8 +51,9 @@ func NewOpsService(
...
@@ -50,8 +51,9 @@ func NewOpsService(
openAIGatewayService
*
OpenAIGatewayService
,
openAIGatewayService
*
OpenAIGatewayService
,
geminiCompatService
*
GeminiMessagesCompatService
,
geminiCompatService
*
GeminiMessagesCompatService
,
antigravityGatewayService
*
AntigravityGatewayService
,
antigravityGatewayService
*
AntigravityGatewayService
,
systemLogSink
*
OpsSystemLogSink
,
)
*
OpsService
{
)
*
OpsService
{
return
&
OpsService
{
svc
:=
&
OpsService
{
opsRepo
:
opsRepo
,
opsRepo
:
opsRepo
,
settingRepo
:
settingRepo
,
settingRepo
:
settingRepo
,
cfg
:
cfg
,
cfg
:
cfg
,
...
@@ -64,7 +66,10 @@ func NewOpsService(
...
@@ -64,7 +66,10 @@ func NewOpsService(
openAIGatewayService
:
openAIGatewayService
,
openAIGatewayService
:
openAIGatewayService
,
geminiCompatService
:
geminiCompatService
,
geminiCompatService
:
geminiCompatService
,
antigravityGatewayService
:
antigravityGatewayService
,
antigravityGatewayService
:
antigravityGatewayService
,
systemLogSink
:
systemLogSink
,
}
}
svc
.
applyRuntimeLogConfigOnStartup
(
context
.
Background
())
return
svc
}
}
func
(
s
*
OpsService
)
RequireMonitoringEnabled
(
ctx
context
.
Context
)
error
{
func
(
s
*
OpsService
)
RequireMonitoringEnabled
(
ctx
context
.
Context
)
error
{
...
...
backend/internal/service/ops_settings_models.go
View file @
64236361
...
@@ -68,6 +68,20 @@ type OpsMetricThresholds struct {
...
@@ -68,6 +68,20 @@ type OpsMetricThresholds struct {
UpstreamErrorRatePercentMax
*
float64
`json:"upstream_error_rate_percent_max,omitempty"`
// 上游错误率高于此值变红
UpstreamErrorRatePercentMax
*
float64
`json:"upstream_error_rate_percent_max,omitempty"`
// 上游错误率高于此值变红
}
}
type
OpsRuntimeLogConfig
struct
{
Level
string
`json:"level"`
EnableSampling
bool
`json:"enable_sampling"`
SamplingInitial
int
`json:"sampling_initial"`
SamplingNext
int
`json:"sampling_thereafter"`
Caller
bool
`json:"caller"`
StacktraceLevel
string
`json:"stacktrace_level"`
RetentionDays
int
`json:"retention_days"`
Source
string
`json:"source,omitempty"`
UpdatedAt
string
`json:"updated_at,omitempty"`
UpdatedByUserID
int64
`json:"updated_by_user_id,omitempty"`
Extra
map
[
string
]
any
`json:"extra,omitempty"`
}
type
OpsAlertRuntimeSettings
struct
{
type
OpsAlertRuntimeSettings
struct
{
EvaluationIntervalSeconds
int
`json:"evaluation_interval_seconds"`
EvaluationIntervalSeconds
int
`json:"evaluation_interval_seconds"`
...
...
backend/internal/service/ops_system_log_service.go
0 → 100644
View file @
64236361
package
service
import
(
"context"
"database/sql"
"encoding/json"
"errors"
"log"
"strings"
"time"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
func
(
s
*
OpsService
)
ListSystemLogs
(
ctx
context
.
Context
,
filter
*
OpsSystemLogFilter
)
(
*
OpsSystemLogList
,
error
)
{
if
err
:=
s
.
RequireMonitoringEnabled
(
ctx
);
err
!=
nil
{
return
nil
,
err
}
if
s
.
opsRepo
==
nil
{
return
&
OpsSystemLogList
{
Logs
:
[]
*
OpsSystemLog
{},
Total
:
0
,
Page
:
1
,
PageSize
:
50
,
},
nil
}
if
filter
==
nil
{
filter
=
&
OpsSystemLogFilter
{}
}
if
filter
.
Page
<=
0
{
filter
.
Page
=
1
}
if
filter
.
PageSize
<=
0
{
filter
.
PageSize
=
50
}
if
filter
.
PageSize
>
200
{
filter
.
PageSize
=
200
}
result
,
err
:=
s
.
opsRepo
.
ListSystemLogs
(
ctx
,
filter
)
if
err
!=
nil
{
return
nil
,
infraerrors
.
InternalServer
(
"OPS_SYSTEM_LOG_LIST_FAILED"
,
"Failed to list system logs"
)
.
WithCause
(
err
)
}
return
result
,
nil
}
func
(
s
*
OpsService
)
CleanupSystemLogs
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
,
operatorID
int64
)
(
int64
,
error
)
{
if
err
:=
s
.
RequireMonitoringEnabled
(
ctx
);
err
!=
nil
{
return
0
,
err
}
if
s
.
opsRepo
==
nil
{
return
0
,
infraerrors
.
ServiceUnavailable
(
"OPS_REPO_UNAVAILABLE"
,
"Ops repository not available"
)
}
if
operatorID
<=
0
{
return
0
,
infraerrors
.
BadRequest
(
"OPS_SYSTEM_LOG_CLEANUP_INVALID_OPERATOR"
,
"invalid operator"
)
}
if
filter
==
nil
{
filter
=
&
OpsSystemLogCleanupFilter
{}
}
if
filter
.
EndTime
!=
nil
&&
filter
.
StartTime
!=
nil
&&
filter
.
StartTime
.
After
(
*
filter
.
EndTime
)
{
return
0
,
infraerrors
.
BadRequest
(
"OPS_SYSTEM_LOG_CLEANUP_INVALID_RANGE"
,
"invalid time range"
)
}
deletedRows
,
err
:=
s
.
opsRepo
.
DeleteSystemLogs
(
ctx
,
filter
)
if
err
!=
nil
{
if
errors
.
Is
(
err
,
sql
.
ErrNoRows
)
{
return
0
,
nil
}
if
strings
.
Contains
(
strings
.
ToLower
(
err
.
Error
()),
"requires at least one filter"
)
{
return
0
,
infraerrors
.
BadRequest
(
"OPS_SYSTEM_LOG_CLEANUP_FILTER_REQUIRED"
,
"cleanup requires at least one filter condition"
)
}
return
0
,
infraerrors
.
InternalServer
(
"OPS_SYSTEM_LOG_CLEANUP_FAILED"
,
"Failed to cleanup system logs"
)
.
WithCause
(
err
)
}
if
auditErr
:=
s
.
opsRepo
.
InsertSystemLogCleanupAudit
(
ctx
,
&
OpsSystemLogCleanupAudit
{
CreatedAt
:
time
.
Now
()
.
UTC
(),
OperatorID
:
operatorID
,
Conditions
:
marshalSystemLogCleanupConditions
(
filter
),
DeletedRows
:
deletedRows
,
});
auditErr
!=
nil
{
// 审计失败不影响主流程,避免运维清理被阻塞。
log
.
Printf
(
"[OpsSystemLog] cleanup audit failed: %v"
,
auditErr
)
}
return
deletedRows
,
nil
}
func
marshalSystemLogCleanupConditions
(
filter
*
OpsSystemLogCleanupFilter
)
string
{
if
filter
==
nil
{
return
"{}"
}
payload
:=
map
[
string
]
any
{
"level"
:
strings
.
TrimSpace
(
filter
.
Level
),
"component"
:
strings
.
TrimSpace
(
filter
.
Component
),
"request_id"
:
strings
.
TrimSpace
(
filter
.
RequestID
),
"client_request_id"
:
strings
.
TrimSpace
(
filter
.
ClientRequestID
),
"platform"
:
strings
.
TrimSpace
(
filter
.
Platform
),
"model"
:
strings
.
TrimSpace
(
filter
.
Model
),
"query"
:
strings
.
TrimSpace
(
filter
.
Query
),
}
if
filter
.
UserID
!=
nil
{
payload
[
"user_id"
]
=
*
filter
.
UserID
}
if
filter
.
AccountID
!=
nil
{
payload
[
"account_id"
]
=
*
filter
.
AccountID
}
if
filter
.
StartTime
!=
nil
&&
!
filter
.
StartTime
.
IsZero
()
{
payload
[
"start_time"
]
=
filter
.
StartTime
.
UTC
()
.
Format
(
time
.
RFC3339Nano
)
}
if
filter
.
EndTime
!=
nil
&&
!
filter
.
EndTime
.
IsZero
()
{
payload
[
"end_time"
]
=
filter
.
EndTime
.
UTC
()
.
Format
(
time
.
RFC3339Nano
)
}
raw
,
err
:=
json
.
Marshal
(
payload
)
if
err
!=
nil
{
return
"{}"
}
return
string
(
raw
)
}
func
(
s
*
OpsService
)
GetSystemLogSinkHealth
()
OpsSystemLogSinkHealth
{
if
s
==
nil
||
s
.
systemLogSink
==
nil
{
return
OpsSystemLogSinkHealth
{}
}
return
s
.
systemLogSink
.
Health
()
}
backend/internal/service/ops_system_log_service_test.go
0 → 100644
View file @
64236361
package
service
import
(
"context"
"database/sql"
"errors"
"strings"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
)
func
TestOpsServiceListSystemLogs_DefaultClampAndSuccess
(
t
*
testing
.
T
)
{
var
gotFilter
*
OpsSystemLogFilter
repo
:=
&
opsRepoMock
{
ListSystemLogsFn
:
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogFilter
)
(
*
OpsSystemLogList
,
error
)
{
gotFilter
=
filter
return
&
OpsSystemLogList
{
Logs
:
[]
*
OpsSystemLog
{{
ID
:
1
,
Level
:
"warn"
,
Message
:
"x"
}},
Total
:
1
,
Page
:
filter
.
Page
,
PageSize
:
filter
.
PageSize
,
},
nil
},
}
svc
:=
NewOpsService
(
repo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
out
,
err
:=
svc
.
ListSystemLogs
(
context
.
Background
(),
&
OpsSystemLogFilter
{
Page
:
0
,
PageSize
:
999
,
})
if
err
!=
nil
{
t
.
Fatalf
(
"ListSystemLogs() error: %v"
,
err
)
}
if
gotFilter
==
nil
{
t
.
Fatalf
(
"expected repository to receive filter"
)
}
if
gotFilter
.
Page
!=
1
||
gotFilter
.
PageSize
!=
200
{
t
.
Fatalf
(
"filter normalized unexpectedly: page=%d pageSize=%d"
,
gotFilter
.
Page
,
gotFilter
.
PageSize
)
}
if
out
.
Total
!=
1
||
len
(
out
.
Logs
)
!=
1
{
t
.
Fatalf
(
"unexpected result: %+v"
,
out
)
}
}
func
TestOpsServiceListSystemLogs_MonitoringDisabled
(
t
*
testing
.
T
)
{
svc
:=
NewOpsService
(
&
opsRepoMock
{},
nil
,
&
config
.
Config
{
Ops
:
config
.
OpsConfig
{
Enabled
:
false
}},
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
)
_
,
err
:=
svc
.
ListSystemLogs
(
context
.
Background
(),
&
OpsSystemLogFilter
{})
if
err
==
nil
{
t
.
Fatalf
(
"expected disabled error"
)
}
}
func
TestOpsServiceListSystemLogs_NilRepoReturnsEmpty
(
t
*
testing
.
T
)
{
svc
:=
NewOpsService
(
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
out
,
err
:=
svc
.
ListSystemLogs
(
context
.
Background
(),
nil
)
if
err
!=
nil
{
t
.
Fatalf
(
"ListSystemLogs() error: %v"
,
err
)
}
if
out
==
nil
||
out
.
Page
!=
1
||
out
.
PageSize
!=
50
||
out
.
Total
!=
0
||
len
(
out
.
Logs
)
!=
0
{
t
.
Fatalf
(
"unexpected nil-repo result: %+v"
,
out
)
}
}
func
TestOpsServiceListSystemLogs_RepoErrorMapped
(
t
*
testing
.
T
)
{
repo
:=
&
opsRepoMock
{
ListSystemLogsFn
:
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogFilter
)
(
*
OpsSystemLogList
,
error
)
{
return
nil
,
errors
.
New
(
"db down"
)
},
}
svc
:=
NewOpsService
(
repo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
_
,
err
:=
svc
.
ListSystemLogs
(
context
.
Background
(),
&
OpsSystemLogFilter
{})
if
err
==
nil
{
t
.
Fatalf
(
"expected mapped internal error"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"OPS_SYSTEM_LOG_LIST_FAILED"
)
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
}
func
TestOpsServiceCleanupSystemLogs_SuccessAndAudit
(
t
*
testing
.
T
)
{
var
audit
*
OpsSystemLogCleanupAudit
repo
:=
&
opsRepoMock
{
DeleteSystemLogsFn
:
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
)
(
int64
,
error
)
{
return
3
,
nil
},
InsertSystemLogCleanupAuditFn
:
func
(
ctx
context
.
Context
,
input
*
OpsSystemLogCleanupAudit
)
error
{
audit
=
input
return
nil
},
}
svc
:=
NewOpsService
(
repo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
userID
:=
int64
(
7
)
now
:=
time
.
Now
()
.
UTC
()
filter
:=
&
OpsSystemLogCleanupFilter
{
StartTime
:
&
now
,
Level
:
"warn"
,
RequestID
:
"req-1"
,
ClientRequestID
:
"creq-1"
,
UserID
:
&
userID
,
Query
:
"timeout"
,
}
deleted
,
err
:=
svc
.
CleanupSystemLogs
(
context
.
Background
(),
filter
,
99
)
if
err
!=
nil
{
t
.
Fatalf
(
"CleanupSystemLogs() error: %v"
,
err
)
}
if
deleted
!=
3
{
t
.
Fatalf
(
"deleted=%d, want 3"
,
deleted
)
}
if
audit
==
nil
{
t
.
Fatalf
(
"expected cleanup audit"
)
}
if
!
strings
.
Contains
(
audit
.
Conditions
,
`"client_request_id":"creq-1"`
)
{
t
.
Fatalf
(
"audit conditions should include client_request_id: %s"
,
audit
.
Conditions
)
}
if
!
strings
.
Contains
(
audit
.
Conditions
,
`"user_id":7`
)
{
t
.
Fatalf
(
"audit conditions should include user_id: %s"
,
audit
.
Conditions
)
}
}
func
TestOpsServiceCleanupSystemLogs_RepoUnavailableAndInvalidOperator
(
t
*
testing
.
T
)
{
svc
:=
NewOpsService
(
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
if
_
,
err
:=
svc
.
CleanupSystemLogs
(
context
.
Background
(),
&
OpsSystemLogCleanupFilter
{
RequestID
:
"r"
},
1
);
err
==
nil
{
t
.
Fatalf
(
"expected repo unavailable error"
)
}
svc
=
NewOpsService
(
&
opsRepoMock
{},
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
if
_
,
err
:=
svc
.
CleanupSystemLogs
(
context
.
Background
(),
&
OpsSystemLogCleanupFilter
{
RequestID
:
"r"
},
0
);
err
==
nil
{
t
.
Fatalf
(
"expected invalid operator error"
)
}
}
func
TestOpsServiceCleanupSystemLogs_FilterRequired
(
t
*
testing
.
T
)
{
repo
:=
&
opsRepoMock
{
DeleteSystemLogsFn
:
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"cleanup requires at least one filter condition"
)
},
}
svc
:=
NewOpsService
(
repo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
_
,
err
:=
svc
.
CleanupSystemLogs
(
context
.
Background
(),
&
OpsSystemLogCleanupFilter
{},
1
)
if
err
==
nil
{
t
.
Fatalf
(
"expected filter required error"
)
}
if
!
strings
.
Contains
(
strings
.
ToLower
(
err
.
Error
()),
"filter"
)
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
}
func
TestOpsServiceCleanupSystemLogs_InvalidRange
(
t
*
testing
.
T
)
{
repo
:=
&
opsRepoMock
{}
svc
:=
NewOpsService
(
repo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
start
:=
time
.
Now
()
.
UTC
()
end
:=
start
.
Add
(
-
time
.
Hour
)
_
,
err
:=
svc
.
CleanupSystemLogs
(
context
.
Background
(),
&
OpsSystemLogCleanupFilter
{
StartTime
:
&
start
,
EndTime
:
&
end
,
},
1
)
if
err
==
nil
{
t
.
Fatalf
(
"expected invalid range error"
)
}
}
func
TestOpsServiceCleanupSystemLogs_NoRowsAndInternalError
(
t
*
testing
.
T
)
{
repo
:=
&
opsRepoMock
{
DeleteSystemLogsFn
:
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
)
(
int64
,
error
)
{
return
0
,
sql
.
ErrNoRows
},
}
svc
:=
NewOpsService
(
repo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
deleted
,
err
:=
svc
.
CleanupSystemLogs
(
context
.
Background
(),
&
OpsSystemLogCleanupFilter
{
RequestID
:
"req-1"
,
},
1
)
if
err
!=
nil
||
deleted
!=
0
{
t
.
Fatalf
(
"expected no rows shortcut, deleted=%d err=%v"
,
deleted
,
err
)
}
repo
.
DeleteSystemLogsFn
=
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"boom"
)
}
if
_
,
err
:=
svc
.
CleanupSystemLogs
(
context
.
Background
(),
&
OpsSystemLogCleanupFilter
{
RequestID
:
"req-1"
,
},
1
);
err
==
nil
{
t
.
Fatalf
(
"expected internal cleanup error"
)
}
}
func
TestOpsServiceCleanupSystemLogs_AuditFailureIgnored
(
t
*
testing
.
T
)
{
repo
:=
&
opsRepoMock
{
DeleteSystemLogsFn
:
func
(
ctx
context
.
Context
,
filter
*
OpsSystemLogCleanupFilter
)
(
int64
,
error
)
{
return
5
,
nil
},
InsertSystemLogCleanupAuditFn
:
func
(
ctx
context
.
Context
,
input
*
OpsSystemLogCleanupAudit
)
error
{
return
errors
.
New
(
"audit down"
)
},
}
svc
:=
NewOpsService
(
repo
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
deleted
,
err
:=
svc
.
CleanupSystemLogs
(
context
.
Background
(),
&
OpsSystemLogCleanupFilter
{
RequestID
:
"r1"
,
},
1
)
if
err
!=
nil
||
deleted
!=
5
{
t
.
Fatalf
(
"audit failure should not break cleanup, deleted=%d err=%v"
,
deleted
,
err
)
}
}
func
TestMarshalSystemLogCleanupConditions_NilAndMarshalError
(
t
*
testing
.
T
)
{
if
got
:=
marshalSystemLogCleanupConditions
(
nil
);
got
!=
"{}"
{
t
.
Fatalf
(
"nil filter should return {}, got %s"
,
got
)
}
now
:=
time
.
Now
()
.
UTC
()
userID
:=
int64
(
1
)
filter
:=
&
OpsSystemLogCleanupFilter
{
StartTime
:
&
now
,
EndTime
:
&
now
,
UserID
:
&
userID
,
}
got
:=
marshalSystemLogCleanupConditions
(
filter
)
if
!
strings
.
Contains
(
got
,
`"start_time"`
)
||
!
strings
.
Contains
(
got
,
`"user_id":1`
)
{
t
.
Fatalf
(
"unexpected marshal payload: %s"
,
got
)
}
}
func
TestOpsServiceGetSystemLogSinkHealth
(
t
*
testing
.
T
)
{
svc
:=
NewOpsService
(
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
health
:=
svc
.
GetSystemLogSinkHealth
()
if
health
.
QueueCapacity
!=
0
||
health
.
QueueDepth
!=
0
{
t
.
Fatalf
(
"unexpected health for nil sink: %+v"
,
health
)
}
sink
:=
NewOpsSystemLogSink
(
&
opsRepoMock
{})
svc
=
NewOpsService
(
&
opsRepoMock
{},
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
sink
)
health
=
svc
.
GetSystemLogSinkHealth
()
if
health
.
QueueCapacity
<=
0
{
t
.
Fatalf
(
"expected non-zero queue capacity: %+v"
,
health
)
}
}
backend/internal/service/ops_system_log_sink.go
0 → 100644
View file @
64236361
package
service
import
(
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/util/logredact"
)
type
OpsSystemLogSinkHealth
struct
{
QueueDepth
int64
`json:"queue_depth"`
QueueCapacity
int64
`json:"queue_capacity"`
DroppedCount
uint64
`json:"dropped_count"`
WriteFailed
uint64
`json:"write_failed_count"`
WrittenCount
uint64
`json:"written_count"`
AvgWriteDelayMs
uint64
`json:"avg_write_delay_ms"`
LastError
string
`json:"last_error"`
}
type
OpsSystemLogSink
struct
{
opsRepo
OpsRepository
queue
chan
*
logger
.
LogEvent
batchSize
int
flushInterval
time
.
Duration
ctx
context
.
Context
cancel
context
.
CancelFunc
wg
sync
.
WaitGroup
droppedCount
uint64
writeFailed
uint64
writtenCount
uint64
totalDelayNs
uint64
lastError
atomic
.
Value
}
func
NewOpsSystemLogSink
(
opsRepo
OpsRepository
)
*
OpsSystemLogSink
{
ctx
,
cancel
:=
context
.
WithCancel
(
context
.
Background
())
s
:=
&
OpsSystemLogSink
{
opsRepo
:
opsRepo
,
queue
:
make
(
chan
*
logger
.
LogEvent
,
5000
),
batchSize
:
200
,
flushInterval
:
time
.
Second
,
ctx
:
ctx
,
cancel
:
cancel
,
}
s
.
lastError
.
Store
(
""
)
return
s
}
func
(
s
*
OpsSystemLogSink
)
Start
()
{
if
s
==
nil
||
s
.
opsRepo
==
nil
{
return
}
s
.
wg
.
Add
(
1
)
go
s
.
run
()
}
func
(
s
*
OpsSystemLogSink
)
Stop
()
{
if
s
==
nil
{
return
}
s
.
cancel
()
s
.
wg
.
Wait
()
}
func
(
s
*
OpsSystemLogSink
)
WriteLogEvent
(
event
*
logger
.
LogEvent
)
{
if
s
==
nil
||
event
==
nil
||
!
s
.
shouldIndex
(
event
)
{
return
}
if
s
.
ctx
!=
nil
{
select
{
case
<-
s
.
ctx
.
Done
()
:
return
default
:
}
}
select
{
case
s
.
queue
<-
event
:
default
:
atomic
.
AddUint64
(
&
s
.
droppedCount
,
1
)
}
}
func
(
s
*
OpsSystemLogSink
)
shouldIndex
(
event
*
logger
.
LogEvent
)
bool
{
level
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
event
.
Level
))
switch
level
{
case
"warn"
,
"warning"
,
"error"
,
"fatal"
,
"panic"
,
"dpanic"
:
return
true
}
component
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
event
.
Component
))
// zap 的 LoggerName 往往为空或不等于业务组件名;业务组件名通常以字段 component 透传。
if
event
.
Fields
!=
nil
{
if
fc
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
asString
(
event
.
Fields
[
"component"
])));
fc
!=
""
{
component
=
fc
}
}
if
strings
.
Contains
(
component
,
"http.access"
)
{
return
true
}
if
strings
.
Contains
(
component
,
"audit"
)
{
return
true
}
return
false
}
func
(
s
*
OpsSystemLogSink
)
run
()
{
defer
s
.
wg
.
Done
()
ticker
:=
time
.
NewTicker
(
s
.
flushInterval
)
defer
ticker
.
Stop
()
batch
:=
make
([]
*
logger
.
LogEvent
,
0
,
s
.
batchSize
)
flush
:=
func
(
baseCtx
context
.
Context
)
{
if
len
(
batch
)
==
0
{
return
}
started
:=
time
.
Now
()
inserted
,
err
:=
s
.
flushBatch
(
baseCtx
,
batch
)
delay
:=
time
.
Since
(
started
)
if
err
!=
nil
{
atomic
.
AddUint64
(
&
s
.
writeFailed
,
uint64
(
len
(
batch
)))
s
.
lastError
.
Store
(
err
.
Error
())
_
,
_
=
fmt
.
Fprintf
(
os
.
Stderr
,
"time=%s level=WARN msg=
\"
ops system log sink flush failed
\"
err=%v batch=%d
\n
"
,
time
.
Now
()
.
Format
(
time
.
RFC3339Nano
),
err
,
len
(
batch
),
)
}
else
{
atomic
.
AddUint64
(
&
s
.
writtenCount
,
uint64
(
inserted
))
atomic
.
AddUint64
(
&
s
.
totalDelayNs
,
uint64
(
delay
.
Nanoseconds
()))
s
.
lastError
.
Store
(
""
)
}
batch
=
batch
[
:
0
]
}
drainAndFlush
:=
func
()
{
for
{
select
{
case
item
:=
<-
s
.
queue
:
if
item
==
nil
{
continue
}
batch
=
append
(
batch
,
item
)
if
len
(
batch
)
>=
s
.
batchSize
{
flush
(
context
.
Background
())
}
default
:
flush
(
context
.
Background
())
return
}
}
}
for
{
select
{
case
<-
s
.
ctx
.
Done
()
:
drainAndFlush
()
return
case
item
:=
<-
s
.
queue
:
if
item
==
nil
{
continue
}
batch
=
append
(
batch
,
item
)
if
len
(
batch
)
>=
s
.
batchSize
{
flush
(
s
.
ctx
)
}
case
<-
ticker
.
C
:
flush
(
s
.
ctx
)
}
}
}
func
(
s
*
OpsSystemLogSink
)
flushBatch
(
baseCtx
context
.
Context
,
batch
[]
*
logger
.
LogEvent
)
(
int
,
error
)
{
inputs
:=
make
([]
*
OpsInsertSystemLogInput
,
0
,
len
(
batch
))
for
_
,
event
:=
range
batch
{
if
event
==
nil
{
continue
}
createdAt
:=
event
.
Time
.
UTC
()
if
createdAt
.
IsZero
()
{
createdAt
=
time
.
Now
()
.
UTC
()
}
fields
:=
copyMap
(
event
.
Fields
)
requestID
:=
asString
(
fields
[
"request_id"
])
clientRequestID
:=
asString
(
fields
[
"client_request_id"
])
platform
:=
asString
(
fields
[
"platform"
])
model
:=
asString
(
fields
[
"model"
])
component
:=
strings
.
TrimSpace
(
event
.
Component
)
if
fieldComponent
:=
asString
(
fields
[
"component"
]);
fieldComponent
!=
""
{
component
=
fieldComponent
}
if
component
==
""
{
component
=
"app"
}
userID
:=
asInt64Ptr
(
fields
[
"user_id"
])
accountID
:=
asInt64Ptr
(
fields
[
"account_id"
])
// 统一脱敏后写入索引。
message
:=
logredact
.
RedactText
(
strings
.
TrimSpace
(
event
.
Message
))
redactedExtra
:=
logredact
.
RedactMap
(
fields
)
extraJSONBytes
,
_
:=
json
.
Marshal
(
redactedExtra
)
extraJSON
:=
string
(
extraJSONBytes
)
if
strings
.
TrimSpace
(
extraJSON
)
==
""
{
extraJSON
=
"{}"
}
inputs
=
append
(
inputs
,
&
OpsInsertSystemLogInput
{
CreatedAt
:
createdAt
,
Level
:
strings
.
ToLower
(
strings
.
TrimSpace
(
event
.
Level
)),
Component
:
component
,
Message
:
message
,
RequestID
:
requestID
,
ClientRequestID
:
clientRequestID
,
UserID
:
userID
,
AccountID
:
accountID
,
Platform
:
platform
,
Model
:
model
,
ExtraJSON
:
extraJSON
,
})
}
if
len
(
inputs
)
==
0
{
return
0
,
nil
}
if
baseCtx
==
nil
||
baseCtx
.
Err
()
!=
nil
{
baseCtx
=
context
.
Background
()
}
ctx
,
cancel
:=
context
.
WithTimeout
(
baseCtx
,
5
*
time
.
Second
)
defer
cancel
()
inserted
,
err
:=
s
.
opsRepo
.
BatchInsertSystemLogs
(
ctx
,
inputs
)
if
err
!=
nil
{
return
0
,
err
}
return
int
(
inserted
),
nil
}
func
(
s
*
OpsSystemLogSink
)
Health
()
OpsSystemLogSinkHealth
{
if
s
==
nil
{
return
OpsSystemLogSinkHealth
{}
}
written
:=
atomic
.
LoadUint64
(
&
s
.
writtenCount
)
totalDelay
:=
atomic
.
LoadUint64
(
&
s
.
totalDelayNs
)
var
avgDelay
uint64
if
written
>
0
{
avgDelay
=
(
totalDelay
/
written
)
/
uint64
(
time
.
Millisecond
)
}
lastErr
,
_
:=
s
.
lastError
.
Load
()
.
(
string
)
return
OpsSystemLogSinkHealth
{
QueueDepth
:
int64
(
len
(
s
.
queue
)),
QueueCapacity
:
int64
(
cap
(
s
.
queue
)),
DroppedCount
:
atomic
.
LoadUint64
(
&
s
.
droppedCount
),
WriteFailed
:
atomic
.
LoadUint64
(
&
s
.
writeFailed
),
WrittenCount
:
written
,
AvgWriteDelayMs
:
avgDelay
,
LastError
:
strings
.
TrimSpace
(
lastErr
),
}
}
func
copyMap
(
in
map
[
string
]
any
)
map
[
string
]
any
{
if
len
(
in
)
==
0
{
return
map
[
string
]
any
{}
}
out
:=
make
(
map
[
string
]
any
,
len
(
in
))
for
k
,
v
:=
range
in
{
out
[
k
]
=
v
}
return
out
}
func
asString
(
v
any
)
string
{
switch
t
:=
v
.
(
type
)
{
case
string
:
return
strings
.
TrimSpace
(
t
)
case
fmt
.
Stringer
:
return
strings
.
TrimSpace
(
t
.
String
())
default
:
return
""
}
}
func
asInt64Ptr
(
v
any
)
*
int64
{
switch
t
:=
v
.
(
type
)
{
case
int
:
n
:=
int64
(
t
)
if
n
<=
0
{
return
nil
}
return
&
n
case
int64
:
n
:=
t
if
n
<=
0
{
return
nil
}
return
&
n
case
float64
:
n
:=
int64
(
t
)
if
n
<=
0
{
return
nil
}
return
&
n
case
json
.
Number
:
if
n
,
err
:=
t
.
Int64
();
err
==
nil
{
if
n
<=
0
{
return
nil
}
return
&
n
}
case
string
:
raw
:=
strings
.
TrimSpace
(
t
)
if
raw
==
""
{
return
nil
}
if
n
,
err
:=
strconv
.
ParseInt
(
raw
,
10
,
64
);
err
==
nil
{
if
n
<=
0
{
return
nil
}
return
&
n
}
}
return
nil
}
backend/internal/service/ops_system_log_sink_test.go
0 → 100644
View file @
64236361
package
service
import
(
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
)
func
TestOpsSystemLogSink_ShouldIndex
(
t
*
testing
.
T
)
{
sink
:=
&
OpsSystemLogSink
{}
cases
:=
[]
struct
{
name
string
event
*
logger
.
LogEvent
want
bool
}{
{
name
:
"warn level"
,
event
:
&
logger
.
LogEvent
{
Level
:
"warn"
,
Component
:
"app"
},
want
:
true
,
},
{
name
:
"error level"
,
event
:
&
logger
.
LogEvent
{
Level
:
"error"
,
Component
:
"app"
},
want
:
true
,
},
{
name
:
"access component"
,
event
:
&
logger
.
LogEvent
{
Level
:
"info"
,
Component
:
"http.access"
},
want
:
true
,
},
{
name
:
"access component from fields (real zap path)"
,
event
:
&
logger
.
LogEvent
{
Level
:
"info"
,
Component
:
""
,
Fields
:
map
[
string
]
any
{
"component"
:
"http.access"
},
},
want
:
true
,
},
{
name
:
"audit component"
,
event
:
&
logger
.
LogEvent
{
Level
:
"info"
,
Component
:
"audit.log_config_change"
},
want
:
true
,
},
{
name
:
"audit component from fields (real zap path)"
,
event
:
&
logger
.
LogEvent
{
Level
:
"info"
,
Component
:
""
,
Fields
:
map
[
string
]
any
{
"component"
:
"audit.log_config_change"
},
},
want
:
true
,
},
{
name
:
"plain info"
,
event
:
&
logger
.
LogEvent
{
Level
:
"info"
,
Component
:
"app"
},
want
:
false
,
},
}
for
_
,
tc
:=
range
cases
{
if
got
:=
sink
.
shouldIndex
(
tc
.
event
);
got
!=
tc
.
want
{
t
.
Fatalf
(
"%s: shouldIndex()=%v, want %v"
,
tc
.
name
,
got
,
tc
.
want
)
}
}
}
func
TestOpsSystemLogSink_WriteLogEvent_ShouldDropWhenQueueFull
(
t
*
testing
.
T
)
{
sink
:=
&
OpsSystemLogSink
{
queue
:
make
(
chan
*
logger
.
LogEvent
,
1
),
}
sink
.
WriteLogEvent
(
&
logger
.
LogEvent
{
Level
:
"warn"
,
Component
:
"app"
})
sink
.
WriteLogEvent
(
&
logger
.
LogEvent
{
Level
:
"warn"
,
Component
:
"app"
})
if
got
:=
len
(
sink
.
queue
);
got
!=
1
{
t
.
Fatalf
(
"queue len = %d, want 1"
,
got
)
}
if
dropped
:=
atomic
.
LoadUint64
(
&
sink
.
droppedCount
);
dropped
!=
1
{
t
.
Fatalf
(
"droppedCount = %d, want 1"
,
dropped
)
}
}
func
TestOpsSystemLogSink_Health
(
t
*
testing
.
T
)
{
sink
:=
&
OpsSystemLogSink
{
queue
:
make
(
chan
*
logger
.
LogEvent
,
10
),
}
sink
.
lastError
.
Store
(
"db timeout"
)
atomic
.
StoreUint64
(
&
sink
.
droppedCount
,
3
)
atomic
.
StoreUint64
(
&
sink
.
writeFailed
,
2
)
atomic
.
StoreUint64
(
&
sink
.
writtenCount
,
5
)
atomic
.
StoreUint64
(
&
sink
.
totalDelayNs
,
uint64
(
5000000
))
// 5ms total -> avg 1ms
sink
.
queue
<-
&
logger
.
LogEvent
{
Level
:
"warn"
,
Component
:
"app"
}
sink
.
queue
<-
&
logger
.
LogEvent
{
Level
:
"warn"
,
Component
:
"app"
}
health
:=
sink
.
Health
()
if
health
.
QueueDepth
!=
2
{
t
.
Fatalf
(
"queue depth = %d, want 2"
,
health
.
QueueDepth
)
}
if
health
.
QueueCapacity
!=
10
{
t
.
Fatalf
(
"queue capacity = %d, want 10"
,
health
.
QueueCapacity
)
}
if
health
.
DroppedCount
!=
3
{
t
.
Fatalf
(
"dropped = %d, want 3"
,
health
.
DroppedCount
)
}
if
health
.
WriteFailed
!=
2
{
t
.
Fatalf
(
"write failed = %d, want 2"
,
health
.
WriteFailed
)
}
if
health
.
WrittenCount
!=
5
{
t
.
Fatalf
(
"written = %d, want 5"
,
health
.
WrittenCount
)
}
if
health
.
AvgWriteDelayMs
!=
1
{
t
.
Fatalf
(
"avg delay ms = %d, want 1"
,
health
.
AvgWriteDelayMs
)
}
if
health
.
LastError
!=
"db timeout"
{
t
.
Fatalf
(
"last error = %q, want db timeout"
,
health
.
LastError
)
}
}
func
TestOpsSystemLogSink_StartStopAndFlushSuccess
(
t
*
testing
.
T
)
{
done
:=
make
(
chan
struct
{},
1
)
var
captured
[]
*
OpsInsertSystemLogInput
repo
:=
&
opsRepoMock
{
BatchInsertSystemLogsFn
:
func
(
_
context
.
Context
,
inputs
[]
*
OpsInsertSystemLogInput
)
(
int64
,
error
)
{
captured
=
append
(
captured
,
inputs
...
)
select
{
case
done
<-
struct
{}{}
:
default
:
}
return
int64
(
len
(
inputs
)),
nil
},
}
sink
:=
NewOpsSystemLogSink
(
repo
)
sink
.
batchSize
=
1
sink
.
flushInterval
=
10
*
time
.
Millisecond
sink
.
Start
()
defer
sink
.
Stop
()
sink
.
WriteLogEvent
(
&
logger
.
LogEvent
{
Time
:
time
.
Now
()
.
UTC
(),
Level
:
"warn"
,
Component
:
"http.access"
,
Message
:
`authorization="Bearer sk-test-123"`
,
Fields
:
map
[
string
]
any
{
"component"
:
"http.access"
,
"request_id"
:
"req-1"
,
"client_request_id"
:
"creq-1"
,
"user_id"
:
"12"
,
"account_id"
:
json
.
Number
(
"34"
),
"platform"
:
"openai"
,
"model"
:
"gpt-5"
,
},
})
select
{
case
<-
done
:
case
<-
time
.
After
(
2
*
time
.
Second
)
:
t
.
Fatalf
(
"timeout waiting for sink flush"
)
}
if
len
(
captured
)
!=
1
{
t
.
Fatalf
(
"captured len = %d, want 1"
,
len
(
captured
))
}
item
:=
captured
[
0
]
if
item
.
RequestID
!=
"req-1"
||
item
.
ClientRequestID
!=
"creq-1"
{
t
.
Fatalf
(
"unexpected request ids: %+v"
,
item
)
}
if
item
.
UserID
==
nil
||
*
item
.
UserID
!=
12
{
t
.
Fatalf
(
"unexpected user_id: %+v"
,
item
.
UserID
)
}
if
item
.
AccountID
==
nil
||
*
item
.
AccountID
!=
34
{
t
.
Fatalf
(
"unexpected account_id: %+v"
,
item
.
AccountID
)
}
if
strings
.
TrimSpace
(
item
.
Message
)
==
""
{
t
.
Fatalf
(
"message should not be empty"
)
}
health
:=
sink
.
Health
()
if
health
.
WrittenCount
==
0
{
t
.
Fatalf
(
"written_count should be >0"
)
}
}
func
TestOpsSystemLogSink_FlushFailureUpdatesHealth
(
t
*
testing
.
T
)
{
repo
:=
&
opsRepoMock
{
BatchInsertSystemLogsFn
:
func
(
_
context
.
Context
,
inputs
[]
*
OpsInsertSystemLogInput
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"db unavailable"
)
},
}
sink
:=
NewOpsSystemLogSink
(
repo
)
sink
.
batchSize
=
1
sink
.
flushInterval
=
10
*
time
.
Millisecond
sink
.
Start
()
defer
sink
.
Stop
()
sink
.
WriteLogEvent
(
&
logger
.
LogEvent
{
Time
:
time
.
Now
()
.
UTC
(),
Level
:
"warn"
,
Component
:
"app"
,
Message
:
"boom"
,
Fields
:
map
[
string
]
any
{},
})
deadline
:=
time
.
Now
()
.
Add
(
2
*
time
.
Second
)
for
time
.
Now
()
.
Before
(
deadline
)
{
health
:=
sink
.
Health
()
if
health
.
WriteFailed
>
0
{
if
!
strings
.
Contains
(
health
.
LastError
,
"db unavailable"
)
{
t
.
Fatalf
(
"unexpected last error: %s"
,
health
.
LastError
)
}
return
}
time
.
Sleep
(
20
*
time
.
Millisecond
)
}
t
.
Fatalf
(
"write_failed_count not updated"
)
}
func
TestOpsSystemLogSink_StopFlushUsesActiveContextAndDrainsQueue
(
t
*
testing
.
T
)
{
var
inserted
int64
var
canceledCtxCalls
int64
repo
:=
&
opsRepoMock
{
BatchInsertSystemLogsFn
:
func
(
ctx
context
.
Context
,
inputs
[]
*
OpsInsertSystemLogInput
)
(
int64
,
error
)
{
if
err
:=
ctx
.
Err
();
err
!=
nil
{
atomic
.
AddInt64
(
&
canceledCtxCalls
,
1
)
return
0
,
err
}
atomic
.
AddInt64
(
&
inserted
,
int64
(
len
(
inputs
)))
return
int64
(
len
(
inputs
)),
nil
},
}
sink
:=
NewOpsSystemLogSink
(
repo
)
sink
.
batchSize
=
200
sink
.
flushInterval
=
time
.
Hour
sink
.
Start
()
sink
.
WriteLogEvent
(
&
logger
.
LogEvent
{
Time
:
time
.
Now
()
.
UTC
(),
Level
:
"warn"
,
Component
:
"app"
,
Message
:
"pending-on-shutdown"
,
Fields
:
map
[
string
]
any
{
"component"
:
"http.access"
},
})
sink
.
Stop
()
if
got
:=
atomic
.
LoadInt64
(
&
inserted
);
got
!=
1
{
t
.
Fatalf
(
"inserted = %d, want 1"
,
got
)
}
if
got
:=
atomic
.
LoadInt64
(
&
canceledCtxCalls
);
got
!=
0
{
t
.
Fatalf
(
"canceled ctx calls = %d, want 0"
,
got
)
}
health
:=
sink
.
Health
()
if
health
.
WrittenCount
!=
1
{
t
.
Fatalf
(
"written_count = %d, want 1"
,
health
.
WrittenCount
)
}
}
type
stringerValue
string
func
(
s
stringerValue
)
String
()
string
{
return
string
(
s
)
}
func
TestOpsSystemLogSink_HelperFunctions
(
t
*
testing
.
T
)
{
src
:=
map
[
string
]
any
{
"a"
:
1
}
cloned
:=
copyMap
(
src
)
src
[
"a"
]
=
2
v
,
ok
:=
cloned
[
"a"
]
.
(
int
)
if
!
ok
||
v
!=
1
{
t
.
Fatalf
(
"copyMap should create copy"
)
}
if
got
:=
asString
(
stringerValue
(
" hello "
));
got
!=
"hello"
{
t
.
Fatalf
(
"asString stringer = %q"
,
got
)
}
if
got
:=
asString
(
fmt
.
Errorf
(
"x"
));
got
!=
""
{
t
.
Fatalf
(
"asString error should be empty, got %q"
,
got
)
}
if
got
:=
asString
(
123
);
got
!=
""
{
t
.
Fatalf
(
"asString non-string should be empty, got %q"
,
got
)
}
cases
:=
[]
struct
{
in
any
want
int64
ok
bool
}{
{
in
:
5
,
want
:
5
,
ok
:
true
},
{
in
:
int64
(
6
),
want
:
6
,
ok
:
true
},
{
in
:
float64
(
7
),
want
:
7
,
ok
:
true
},
{
in
:
json
.
Number
(
"8"
),
want
:
8
,
ok
:
true
},
{
in
:
"9"
,
want
:
9
,
ok
:
true
},
{
in
:
"0"
,
ok
:
false
},
{
in
:
-
1
,
ok
:
false
},
{
in
:
"abc"
,
ok
:
false
},
}
for
_
,
tc
:=
range
cases
{
got
:=
asInt64Ptr
(
tc
.
in
)
if
tc
.
ok
{
if
got
==
nil
||
*
got
!=
tc
.
want
{
t
.
Fatalf
(
"asInt64Ptr(%v) = %+v, want %d"
,
tc
.
in
,
got
,
tc
.
want
)
}
}
else
if
got
!=
nil
{
t
.
Fatalf
(
"asInt64Ptr(%v) should be nil, got %d"
,
tc
.
in
,
*
got
)
}
}
}
backend/internal/service/pricing_service.go
View file @
64236361
...
@@ -6,7 +6,6 @@ import (
...
@@ -6,7 +6,6 @@ import (
"encoding/hex"
"encoding/hex"
"encoding/json"
"encoding/json"
"fmt"
"fmt"
"log"
"os"
"os"
"path/filepath"
"path/filepath"
"regexp"
"regexp"
...
@@ -15,6 +14,7 @@ import (
...
@@ -15,6 +14,7 @@ import (
"time"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
)
)
...
@@ -84,12 +84,12 @@ func NewPricingService(cfg *config.Config, remoteClient PricingRemoteClient) *Pr
...
@@ -84,12 +84,12 @@ func NewPricingService(cfg *config.Config, remoteClient PricingRemoteClient) *Pr
func
(
s
*
PricingService
)
Initialize
()
error
{
func
(
s
*
PricingService
)
Initialize
()
error
{
// 确保数据目录存在
// 确保数据目录存在
if
err
:=
os
.
MkdirAll
(
s
.
cfg
.
Pricing
.
DataDir
,
0755
);
err
!=
nil
{
if
err
:=
os
.
MkdirAll
(
s
.
cfg
.
Pricing
.
DataDir
,
0755
);
err
!=
nil
{
log
.
Printf
(
"[Pricing] Failed to create data directory: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Failed to create data directory: %v"
,
err
)
}
}
// 首次加载价格数据
// 首次加载价格数据
if
err
:=
s
.
checkAndUpdatePricing
();
err
!=
nil
{
if
err
:=
s
.
checkAndUpdatePricing
();
err
!=
nil
{
log
.
Printf
(
"[Pricing] Initial load failed, using fallback: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Initial load failed, using fallback: %v"
,
err
)
if
err
:=
s
.
useFallbackPricing
();
err
!=
nil
{
if
err
:=
s
.
useFallbackPricing
();
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to load pricing data: %w"
,
err
)
return
fmt
.
Errorf
(
"failed to load pricing data: %w"
,
err
)
}
}
...
@@ -98,7 +98,7 @@ func (s *PricingService) Initialize() error {
...
@@ -98,7 +98,7 @@ func (s *PricingService) Initialize() error {
// 启动定时更新
// 启动定时更新
s
.
startUpdateScheduler
()
s
.
startUpdateScheduler
()
log
.
Printf
(
"[Pricing] Service initialized with %d models"
,
len
(
s
.
pricingData
))
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Service initialized with %d models"
,
len
(
s
.
pricingData
))
return
nil
return
nil
}
}
...
@@ -106,7 +106,7 @@ func (s *PricingService) Initialize() error {
...
@@ -106,7 +106,7 @@ func (s *PricingService) Initialize() error {
func
(
s
*
PricingService
)
Stop
()
{
func
(
s
*
PricingService
)
Stop
()
{
close
(
s
.
stopCh
)
close
(
s
.
stopCh
)
s
.
wg
.
Wait
()
s
.
wg
.
Wait
()
log
.
Println
(
"[Pricing] Service stopped"
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"%s"
,
"[Pricing] Service stopped"
)
}
}
// startUpdateScheduler 启动定时更新调度器
// startUpdateScheduler 启动定时更新调度器
...
@@ -127,7 +127,7 @@ func (s *PricingService) startUpdateScheduler() {
...
@@ -127,7 +127,7 @@ func (s *PricingService) startUpdateScheduler() {
select
{
select
{
case
<-
ticker
.
C
:
case
<-
ticker
.
C
:
if
err
:=
s
.
syncWithRemote
();
err
!=
nil
{
if
err
:=
s
.
syncWithRemote
();
err
!=
nil
{
log
.
Printf
(
"[Pricing] Sync failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Sync failed: %v"
,
err
)
}
}
case
<-
s
.
stopCh
:
case
<-
s
.
stopCh
:
return
return
...
@@ -135,7 +135,7 @@ func (s *PricingService) startUpdateScheduler() {
...
@@ -135,7 +135,7 @@ func (s *PricingService) startUpdateScheduler() {
}
}
}()
}()
log
.
Printf
(
"[Pricing] Update scheduler started (check every %v)"
,
hashInterval
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Update scheduler started (check every %v)"
,
hashInterval
)
}
}
// checkAndUpdatePricing 检查并更新价格数据
// checkAndUpdatePricing 检查并更新价格数据
...
@@ -144,7 +144,7 @@ func (s *PricingService) checkAndUpdatePricing() error {
...
@@ -144,7 +144,7 @@ func (s *PricingService) checkAndUpdatePricing() error {
// 检查本地文件是否存在
// 检查本地文件是否存在
if
_
,
err
:=
os
.
Stat
(
pricingFile
);
os
.
IsNotExist
(
err
)
{
if
_
,
err
:=
os
.
Stat
(
pricingFile
);
os
.
IsNotExist
(
err
)
{
log
.
Println
(
"[Pricing] Local pricing file not found, downloading..."
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"%s"
,
"[Pricing] Local pricing file not found, downloading..."
)
return
s
.
downloadPricingData
()
return
s
.
downloadPricingData
()
}
}
...
@@ -158,9 +158,9 @@ func (s *PricingService) checkAndUpdatePricing() error {
...
@@ -158,9 +158,9 @@ func (s *PricingService) checkAndUpdatePricing() error {
maxAge
:=
time
.
Duration
(
s
.
cfg
.
Pricing
.
UpdateIntervalHours
)
*
time
.
Hour
maxAge
:=
time
.
Duration
(
s
.
cfg
.
Pricing
.
UpdateIntervalHours
)
*
time
.
Hour
if
fileAge
>
maxAge
{
if
fileAge
>
maxAge
{
log
.
Printf
(
"[Pricing] Local file is %v old, updating..."
,
fileAge
.
Round
(
time
.
Hour
))
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Local file is %v old, updating..."
,
fileAge
.
Round
(
time
.
Hour
))
if
err
:=
s
.
downloadPricingData
();
err
!=
nil
{
if
err
:=
s
.
downloadPricingData
();
err
!=
nil
{
log
.
Printf
(
"[Pricing] Download failed, using existing file: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Download failed, using existing file: %v"
,
err
)
}
}
}
}
...
@@ -175,7 +175,7 @@ func (s *PricingService) syncWithRemote() error {
...
@@ -175,7 +175,7 @@ func (s *PricingService) syncWithRemote() error {
// 计算本地文件哈希
// 计算本地文件哈希
localHash
,
err
:=
s
.
computeFileHash
(
pricingFile
)
localHash
,
err
:=
s
.
computeFileHash
(
pricingFile
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Pricing] Failed to compute local hash: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Failed to compute local hash: %v"
,
err
)
return
s
.
downloadPricingData
()
return
s
.
downloadPricingData
()
}
}
...
@@ -183,15 +183,15 @@ func (s *PricingService) syncWithRemote() error {
...
@@ -183,15 +183,15 @@ func (s *PricingService) syncWithRemote() error {
if
s
.
cfg
.
Pricing
.
HashURL
!=
""
{
if
s
.
cfg
.
Pricing
.
HashURL
!=
""
{
remoteHash
,
err
:=
s
.
fetchRemoteHash
()
remoteHash
,
err
:=
s
.
fetchRemoteHash
()
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Pricing] Failed to fetch remote hash: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Failed to fetch remote hash: %v"
,
err
)
return
nil
// 哈希获取失败不影响正常使用
return
nil
// 哈希获取失败不影响正常使用
}
}
if
remoteHash
!=
localHash
{
if
remoteHash
!=
localHash
{
log
.
Println
(
"[Pricing] Remote hash differs, downloading new version..."
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"%s"
,
"[Pricing] Remote hash differs, downloading new version..."
)
return
s
.
downloadPricingData
()
return
s
.
downloadPricingData
()
}
}
log
.
Println
(
"[Pricing] Hash check passed, no update needed"
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"%s"
,
"[Pricing] Hash check passed, no update needed"
)
return
nil
return
nil
}
}
...
@@ -205,7 +205,7 @@ func (s *PricingService) syncWithRemote() error {
...
@@ -205,7 +205,7 @@ func (s *PricingService) syncWithRemote() error {
maxAge
:=
time
.
Duration
(
s
.
cfg
.
Pricing
.
UpdateIntervalHours
)
*
time
.
Hour
maxAge
:=
time
.
Duration
(
s
.
cfg
.
Pricing
.
UpdateIntervalHours
)
*
time
.
Hour
if
fileAge
>
maxAge
{
if
fileAge
>
maxAge
{
log
.
Printf
(
"[Pricing] File is %v old, downloading..."
,
fileAge
.
Round
(
time
.
Hour
))
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] File is %v old, downloading..."
,
fileAge
.
Round
(
time
.
Hour
))
return
s
.
downloadPricingData
()
return
s
.
downloadPricingData
()
}
}
...
@@ -218,7 +218,7 @@ func (s *PricingService) downloadPricingData() error {
...
@@ -218,7 +218,7 @@ func (s *PricingService) downloadPricingData() error {
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
log
.
Printf
(
"[Pricing] Downloading from %s"
,
remoteURL
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Downloading from %s"
,
remoteURL
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
30
*
time
.
Second
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
30
*
time
.
Second
)
defer
cancel
()
defer
cancel
()
...
@@ -252,7 +252,7 @@ func (s *PricingService) downloadPricingData() error {
...
@@ -252,7 +252,7 @@ func (s *PricingService) downloadPricingData() error {
// 保存到本地文件
// 保存到本地文件
pricingFile
:=
s
.
getPricingFilePath
()
pricingFile
:=
s
.
getPricingFilePath
()
if
err
:=
os
.
WriteFile
(
pricingFile
,
body
,
0644
);
err
!=
nil
{
if
err
:=
os
.
WriteFile
(
pricingFile
,
body
,
0644
);
err
!=
nil
{
log
.
Printf
(
"[Pricing] Failed to save file: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Failed to save file: %v"
,
err
)
}
}
// 保存哈希
// 保存哈希
...
@@ -260,7 +260,7 @@ func (s *PricingService) downloadPricingData() error {
...
@@ -260,7 +260,7 @@ func (s *PricingService) downloadPricingData() error {
hashStr
:=
hex
.
EncodeToString
(
hash
[
:
])
hashStr
:=
hex
.
EncodeToString
(
hash
[
:
])
hashFile
:=
s
.
getHashFilePath
()
hashFile
:=
s
.
getHashFilePath
()
if
err
:=
os
.
WriteFile
(
hashFile
,
[]
byte
(
hashStr
+
"
\n
"
),
0644
);
err
!=
nil
{
if
err
:=
os
.
WriteFile
(
hashFile
,
[]
byte
(
hashStr
+
"
\n
"
),
0644
);
err
!=
nil
{
log
.
Printf
(
"[Pricing] Failed to save hash: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Failed to save hash: %v"
,
err
)
}
}
// 更新内存数据
// 更新内存数据
...
@@ -270,7 +270,7 @@ func (s *PricingService) downloadPricingData() error {
...
@@ -270,7 +270,7 @@ func (s *PricingService) downloadPricingData() error {
s
.
localHash
=
hashStr
s
.
localHash
=
hashStr
s
.
mu
.
Unlock
()
s
.
mu
.
Unlock
()
log
.
Printf
(
"[Pricing] Downloaded %d models successfully"
,
len
(
data
))
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Downloaded %d models successfully"
,
len
(
data
))
return
nil
return
nil
}
}
...
@@ -329,7 +329,7 @@ func (s *PricingService) parsePricingData(body []byte) (map[string]*LiteLLMModel
...
@@ -329,7 +329,7 @@ func (s *PricingService) parsePricingData(body []byte) (map[string]*LiteLLMModel
}
}
if
skipped
>
0
{
if
skipped
>
0
{
log
.
Printf
(
"[Pricing] Skipped %d invalid entries"
,
skipped
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Skipped %d invalid entries"
,
skipped
)
}
}
if
len
(
result
)
==
0
{
if
len
(
result
)
==
0
{
...
@@ -368,7 +368,7 @@ func (s *PricingService) loadPricingData(filePath string) error {
...
@@ -368,7 +368,7 @@ func (s *PricingService) loadPricingData(filePath string) error {
}
}
s
.
mu
.
Unlock
()
s
.
mu
.
Unlock
()
log
.
Printf
(
"[Pricing] Loaded %d models from %s"
,
len
(
pricingData
),
filePath
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Loaded %d models from %s"
,
len
(
pricingData
),
filePath
)
return
nil
return
nil
}
}
...
@@ -380,7 +380,7 @@ func (s *PricingService) useFallbackPricing() error {
...
@@ -380,7 +380,7 @@ func (s *PricingService) useFallbackPricing() error {
return
fmt
.
Errorf
(
"fallback file not found: %s"
,
fallbackFile
)
return
fmt
.
Errorf
(
"fallback file not found: %s"
,
fallbackFile
)
}
}
log
.
Printf
(
"[Pricing] Using fallback file: %s"
,
fallbackFile
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Using fallback file: %s"
,
fallbackFile
)
// 复制到数据目录
// 复制到数据目录
data
,
err
:=
os
.
ReadFile
(
fallbackFile
)
data
,
err
:=
os
.
ReadFile
(
fallbackFile
)
...
@@ -390,7 +390,7 @@ func (s *PricingService) useFallbackPricing() error {
...
@@ -390,7 +390,7 @@ func (s *PricingService) useFallbackPricing() error {
pricingFile
:=
s
.
getPricingFilePath
()
pricingFile
:=
s
.
getPricingFilePath
()
if
err
:=
os
.
WriteFile
(
pricingFile
,
data
,
0644
);
err
!=
nil
{
if
err
:=
os
.
WriteFile
(
pricingFile
,
data
,
0644
);
err
!=
nil
{
log
.
Printf
(
"[Pricing] Failed to copy fallback: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Failed to copy fallback: %v"
,
err
)
}
}
return
s
.
loadPricingData
(
fallbackFile
)
return
s
.
loadPricingData
(
fallbackFile
)
...
@@ -639,7 +639,7 @@ func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing {
...
@@ -639,7 +639,7 @@ func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing {
for
key
,
pricing
:=
range
s
.
pricingData
{
for
key
,
pricing
:=
range
s
.
pricingData
{
keyLower
:=
strings
.
ToLower
(
key
)
keyLower
:=
strings
.
ToLower
(
key
)
if
strings
.
Contains
(
keyLower
,
pattern
)
{
if
strings
.
Contains
(
keyLower
,
pattern
)
{
log
.
Printf
(
"[Pricing] Fuzzy matched %s -> %s"
,
model
,
key
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] Fuzzy matched %s -> %s"
,
model
,
key
)
return
pricing
return
pricing
}
}
}
}
...
@@ -660,14 +660,14 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
...
@@ -660,14 +660,14 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
for
_
,
variant
:=
range
variants
{
for
_
,
variant
:=
range
variants
{
if
pricing
,
ok
:=
s
.
pricingData
[
variant
];
ok
{
if
pricing
,
ok
:=
s
.
pricingData
[
variant
];
ok
{
log
.
Printf
(
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
variant
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
variant
)
return
pricing
return
pricing
}
}
}
}
if
strings
.
HasPrefix
(
model
,
"gpt-5.3-codex"
)
{
if
strings
.
HasPrefix
(
model
,
"gpt-5.3-codex"
)
{
if
pricing
,
ok
:=
s
.
pricingData
[
"gpt-5.2-codex"
];
ok
{
if
pricing
,
ok
:=
s
.
pricingData
[
"gpt-5.2-codex"
];
ok
{
log
.
Printf
(
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
"gpt-5.2-codex"
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] OpenAI fallback matched %s -> %s"
,
model
,
"gpt-5.2-codex"
)
return
pricing
return
pricing
}
}
}
}
...
@@ -675,7 +675,7 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
...
@@ -675,7 +675,7 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
// 最终回退到 DefaultTestModel
// 最终回退到 DefaultTestModel
defaultModel
:=
strings
.
ToLower
(
openai
.
DefaultTestModel
)
defaultModel
:=
strings
.
ToLower
(
openai
.
DefaultTestModel
)
if
pricing
,
ok
:=
s
.
pricingData
[
defaultModel
];
ok
{
if
pricing
,
ok
:=
s
.
pricingData
[
defaultModel
];
ok
{
log
.
Printf
(
"[Pricing] OpenAI fallback to default model %s -> %s"
,
model
,
defaultModel
)
log
ger
.
LegacyPrintf
(
"service.pricing"
,
"[Pricing] OpenAI fallback to default model %s -> %s"
,
model
,
defaultModel
)
return
pricing
return
pricing
}
}
...
...
backend/internal/service/scheduler_snapshot_service.go
View file @
64236361
...
@@ -4,13 +4,13 @@ import (
...
@@ -4,13 +4,13 @@ import (
"context"
"context"
"encoding/json"
"encoding/json"
"errors"
"errors"
"log"
"log/slog"
"log/slog"
"strconv"
"strconv"
"sync"
"sync"
"time"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
)
)
var
(
var
(
...
@@ -104,7 +104,7 @@ func (s *SchedulerSnapshotService) ListSchedulableAccounts(ctx context.Context,
...
@@ -104,7 +104,7 @@ func (s *SchedulerSnapshotService) ListSchedulableAccounts(ctx context.Context,
if
s
.
cache
!=
nil
{
if
s
.
cache
!=
nil
{
cached
,
hit
,
err
:=
s
.
cache
.
GetSnapshot
(
ctx
,
bucket
)
cached
,
hit
,
err
:=
s
.
cache
.
GetSnapshot
(
ctx
,
bucket
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] cache read failed: bucket=%s err=%v"
,
bucket
.
String
(),
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] cache read failed: bucket=%s err=%v"
,
bucket
.
String
(),
err
)
}
else
if
hit
{
}
else
if
hit
{
return
derefAccounts
(
cached
),
useMixed
,
nil
return
derefAccounts
(
cached
),
useMixed
,
nil
}
}
...
@@ -124,7 +124,7 @@ func (s *SchedulerSnapshotService) ListSchedulableAccounts(ctx context.Context,
...
@@ -124,7 +124,7 @@ func (s *SchedulerSnapshotService) ListSchedulableAccounts(ctx context.Context,
if
s
.
cache
!=
nil
{
if
s
.
cache
!=
nil
{
if
err
:=
s
.
cache
.
SetSnapshot
(
fallbackCtx
,
bucket
,
accounts
);
err
!=
nil
{
if
err
:=
s
.
cache
.
SetSnapshot
(
fallbackCtx
,
bucket
,
accounts
);
err
!=
nil
{
log
.
Printf
(
"[Scheduler] cache write failed: bucket=%s err=%v"
,
bucket
.
String
(),
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] cache write failed: bucket=%s err=%v"
,
bucket
.
String
(),
err
)
}
}
}
}
...
@@ -138,7 +138,7 @@ func (s *SchedulerSnapshotService) GetAccount(ctx context.Context, accountID int
...
@@ -138,7 +138,7 @@ func (s *SchedulerSnapshotService) GetAccount(ctx context.Context, accountID int
if
s
.
cache
!=
nil
{
if
s
.
cache
!=
nil
{
account
,
err
:=
s
.
cache
.
GetAccount
(
ctx
,
accountID
)
account
,
err
:=
s
.
cache
.
GetAccount
(
ctx
,
accountID
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] account cache read failed: id=%d err=%v"
,
accountID
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] account cache read failed: id=%d err=%v"
,
accountID
,
err
)
}
else
if
account
!=
nil
{
}
else
if
account
!=
nil
{
return
account
,
nil
return
account
,
nil
}
}
...
@@ -168,17 +168,17 @@ func (s *SchedulerSnapshotService) runInitialRebuild() {
...
@@ -168,17 +168,17 @@ func (s *SchedulerSnapshotService) runInitialRebuild() {
defer
cancel
()
defer
cancel
()
buckets
,
err
:=
s
.
cache
.
ListBuckets
(
ctx
)
buckets
,
err
:=
s
.
cache
.
ListBuckets
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] list buckets failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] list buckets failed: %v"
,
err
)
}
}
if
len
(
buckets
)
==
0
{
if
len
(
buckets
)
==
0
{
buckets
,
err
=
s
.
defaultBuckets
(
ctx
)
buckets
,
err
=
s
.
defaultBuckets
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] default buckets failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] default buckets failed: %v"
,
err
)
return
return
}
}
}
}
if
err
:=
s
.
rebuildBuckets
(
ctx
,
buckets
,
"startup"
);
err
!=
nil
{
if
err
:=
s
.
rebuildBuckets
(
ctx
,
buckets
,
"startup"
);
err
!=
nil
{
log
.
Printf
(
"[Scheduler] rebuild startup failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] rebuild startup failed: %v"
,
err
)
}
}
}
}
...
@@ -205,7 +205,7 @@ func (s *SchedulerSnapshotService) runFullRebuildWorker(interval time.Duration)
...
@@ -205,7 +205,7 @@ func (s *SchedulerSnapshotService) runFullRebuildWorker(interval time.Duration)
select
{
select
{
case
<-
ticker
.
C
:
case
<-
ticker
.
C
:
if
err
:=
s
.
triggerFullRebuild
(
"interval"
);
err
!=
nil
{
if
err
:=
s
.
triggerFullRebuild
(
"interval"
);
err
!=
nil
{
log
.
Printf
(
"[Scheduler] full rebuild failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] full rebuild failed: %v"
,
err
)
}
}
case
<-
s
.
stopCh
:
case
<-
s
.
stopCh
:
return
return
...
@@ -222,13 +222,13 @@ func (s *SchedulerSnapshotService) pollOutbox() {
...
@@ -222,13 +222,13 @@ func (s *SchedulerSnapshotService) pollOutbox() {
watermark
,
err
:=
s
.
cache
.
GetOutboxWatermark
(
ctx
)
watermark
,
err
:=
s
.
cache
.
GetOutboxWatermark
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] outbox watermark read failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox watermark read failed: %v"
,
err
)
return
return
}
}
events
,
err
:=
s
.
outboxRepo
.
ListAfter
(
ctx
,
watermark
,
200
)
events
,
err
:=
s
.
outboxRepo
.
ListAfter
(
ctx
,
watermark
,
200
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] outbox poll failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox poll failed: %v"
,
err
)
return
return
}
}
if
len
(
events
)
==
0
{
if
len
(
events
)
==
0
{
...
@@ -241,14 +241,14 @@ func (s *SchedulerSnapshotService) pollOutbox() {
...
@@ -241,14 +241,14 @@ func (s *SchedulerSnapshotService) pollOutbox() {
err
:=
s
.
handleOutboxEvent
(
eventCtx
,
event
)
err
:=
s
.
handleOutboxEvent
(
eventCtx
,
event
)
cancel
()
cancel
()
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] outbox handle failed: id=%d type=%s err=%v"
,
event
.
ID
,
event
.
EventType
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox handle failed: id=%d type=%s err=%v"
,
event
.
ID
,
event
.
EventType
,
err
)
return
return
}
}
}
}
lastID
:=
events
[
len
(
events
)
-
1
]
.
ID
lastID
:=
events
[
len
(
events
)
-
1
]
.
ID
if
err
:=
s
.
cache
.
SetOutboxWatermark
(
ctx
,
lastID
);
err
!=
nil
{
if
err
:=
s
.
cache
.
SetOutboxWatermark
(
ctx
,
lastID
);
err
!=
nil
{
log
.
Printf
(
"[Scheduler] outbox watermark write failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox watermark write failed: %v"
,
err
)
}
else
{
}
else
{
watermarkForCheck
=
lastID
watermarkForCheck
=
lastID
}
}
...
@@ -445,11 +445,11 @@ func (s *SchedulerSnapshotService) rebuildBucket(ctx context.Context, bucket Sch
...
@@ -445,11 +445,11 @@ func (s *SchedulerSnapshotService) rebuildBucket(ctx context.Context, bucket Sch
accounts
,
err
:=
s
.
loadAccountsFromDB
(
rebuildCtx
,
bucket
,
bucket
.
Mode
==
SchedulerModeMixed
)
accounts
,
err
:=
s
.
loadAccountsFromDB
(
rebuildCtx
,
bucket
,
bucket
.
Mode
==
SchedulerModeMixed
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] rebuild failed: bucket=%s reason=%s err=%v"
,
bucket
.
String
(),
reason
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] rebuild failed: bucket=%s reason=%s err=%v"
,
bucket
.
String
(),
reason
,
err
)
return
err
return
err
}
}
if
err
:=
s
.
cache
.
SetSnapshot
(
rebuildCtx
,
bucket
,
accounts
);
err
!=
nil
{
if
err
:=
s
.
cache
.
SetSnapshot
(
rebuildCtx
,
bucket
,
accounts
);
err
!=
nil
{
log
.
Printf
(
"[Scheduler] rebuild cache failed: bucket=%s reason=%s err=%v"
,
bucket
.
String
(),
reason
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] rebuild cache failed: bucket=%s reason=%s err=%v"
,
bucket
.
String
(),
reason
,
err
)
return
err
return
err
}
}
slog
.
Debug
(
"[Scheduler] rebuild ok"
,
"bucket"
,
bucket
.
String
(),
"reason"
,
reason
,
"size"
,
len
(
accounts
))
slog
.
Debug
(
"[Scheduler] rebuild ok"
,
"bucket"
,
bucket
.
String
(),
"reason"
,
reason
,
"size"
,
len
(
accounts
))
...
@@ -465,13 +465,13 @@ func (s *SchedulerSnapshotService) triggerFullRebuild(reason string) error {
...
@@ -465,13 +465,13 @@ func (s *SchedulerSnapshotService) triggerFullRebuild(reason string) error {
buckets
,
err
:=
s
.
cache
.
ListBuckets
(
ctx
)
buckets
,
err
:=
s
.
cache
.
ListBuckets
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] list buckets failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] list buckets failed: %v"
,
err
)
return
err
return
err
}
}
if
len
(
buckets
)
==
0
{
if
len
(
buckets
)
==
0
{
buckets
,
err
=
s
.
defaultBuckets
(
ctx
)
buckets
,
err
=
s
.
defaultBuckets
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Scheduler] default buckets failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] default buckets failed: %v"
,
err
)
return
err
return
err
}
}
}
}
...
@@ -485,7 +485,7 @@ func (s *SchedulerSnapshotService) checkOutboxLag(ctx context.Context, oldest Sc
...
@@ -485,7 +485,7 @@ func (s *SchedulerSnapshotService) checkOutboxLag(ctx context.Context, oldest Sc
lag
:=
time
.
Since
(
oldest
.
CreatedAt
)
lag
:=
time
.
Since
(
oldest
.
CreatedAt
)
if
lagSeconds
:=
int
(
lag
.
Seconds
());
lagSeconds
>=
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagWarnSeconds
&&
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagWarnSeconds
>
0
{
if
lagSeconds
:=
int
(
lag
.
Seconds
());
lagSeconds
>=
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagWarnSeconds
&&
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagWarnSeconds
>
0
{
log
.
Printf
(
"[Scheduler] outbox lag warning: %ds"
,
lagSeconds
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox lag warning: %ds"
,
lagSeconds
)
}
}
if
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagRebuildSeconds
>
0
&&
int
(
lag
.
Seconds
())
>=
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagRebuildSeconds
{
if
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagRebuildSeconds
>
0
&&
int
(
lag
.
Seconds
())
>=
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagRebuildSeconds
{
...
@@ -495,12 +495,12 @@ func (s *SchedulerSnapshotService) checkOutboxLag(ctx context.Context, oldest Sc
...
@@ -495,12 +495,12 @@ func (s *SchedulerSnapshotService) checkOutboxLag(ctx context.Context, oldest Sc
s
.
lagMu
.
Unlock
()
s
.
lagMu
.
Unlock
()
if
failures
>=
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagRebuildFailures
{
if
failures
>=
s
.
cfg
.
Gateway
.
Scheduling
.
OutboxLagRebuildFailures
{
log
.
Printf
(
"[Scheduler] outbox lag rebuild triggered: lag=%s failures=%d"
,
lag
,
failures
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox lag rebuild triggered: lag=%s failures=%d"
,
lag
,
failures
)
s
.
lagMu
.
Lock
()
s
.
lagMu
.
Lock
()
s
.
lagFailures
=
0
s
.
lagFailures
=
0
s
.
lagMu
.
Unlock
()
s
.
lagMu
.
Unlock
()
if
err
:=
s
.
triggerFullRebuild
(
"outbox_lag"
);
err
!=
nil
{
if
err
:=
s
.
triggerFullRebuild
(
"outbox_lag"
);
err
!=
nil
{
log
.
Printf
(
"[Scheduler] outbox lag rebuild failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox lag rebuild failed: %v"
,
err
)
}
}
}
}
}
else
{
}
else
{
...
@@ -518,9 +518,9 @@ func (s *SchedulerSnapshotService) checkOutboxLag(ctx context.Context, oldest Sc
...
@@ -518,9 +518,9 @@ func (s *SchedulerSnapshotService) checkOutboxLag(ctx context.Context, oldest Sc
return
return
}
}
if
maxID
-
watermark
>=
int64
(
threshold
)
{
if
maxID
-
watermark
>=
int64
(
threshold
)
{
log
.
Printf
(
"[Scheduler] outbox backlog rebuild triggered: backlog=%d"
,
maxID
-
watermark
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox backlog rebuild triggered: backlog=%d"
,
maxID
-
watermark
)
if
err
:=
s
.
triggerFullRebuild
(
"outbox_backlog"
);
err
!=
nil
{
if
err
:=
s
.
triggerFullRebuild
(
"outbox_backlog"
);
err
!=
nil
{
log
.
Printf
(
"[Scheduler] outbox backlog rebuild failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.scheduler_snapshot"
,
"[Scheduler] outbox backlog rebuild failed: %v"
,
err
)
}
}
}
}
}
}
...
...
backend/internal/service/sora_media_cleanup_service.go
View file @
64236361
package
service
package
service
import
(
import
(
"log"
"os"
"os"
"path/filepath"
"path/filepath"
"strings"
"strings"
...
@@ -9,6 +8,7 @@ import (
...
@@ -9,6 +8,7 @@ import (
"time"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/robfig/cron/v3"
"github.com/robfig/cron/v3"
)
)
...
@@ -37,18 +37,18 @@ func (s *SoraMediaCleanupService) Start() {
...
@@ -37,18 +37,18 @@ func (s *SoraMediaCleanupService) Start() {
return
return
}
}
if
!
s
.
cfg
.
Sora
.
Storage
.
Cleanup
.
Enabled
{
if
!
s
.
cfg
.
Sora
.
Storage
.
Cleanup
.
Enabled
{
log
.
Printf
(
"[SoraCleanup] not started (disabled)"
)
log
ger
.
LegacyPrintf
(
"service.sora_media_cleanup"
,
"[SoraCleanup] not started (disabled)"
)
return
return
}
}
if
s
.
storage
==
nil
||
!
s
.
storage
.
Enabled
()
{
if
s
.
storage
==
nil
||
!
s
.
storage
.
Enabled
()
{
log
.
Printf
(
"[SoraCleanup] not started (storage disabled)"
)
log
ger
.
LegacyPrintf
(
"service.sora_media_cleanup"
,
"[SoraCleanup] not started (storage disabled)"
)
return
return
}
}
s
.
startOnce
.
Do
(
func
()
{
s
.
startOnce
.
Do
(
func
()
{
schedule
:=
strings
.
TrimSpace
(
s
.
cfg
.
Sora
.
Storage
.
Cleanup
.
Schedule
)
schedule
:=
strings
.
TrimSpace
(
s
.
cfg
.
Sora
.
Storage
.
Cleanup
.
Schedule
)
if
schedule
==
""
{
if
schedule
==
""
{
log
.
Printf
(
"[SoraCleanup] not started (empty schedule)"
)
log
ger
.
LegacyPrintf
(
"service.sora_media_cleanup"
,
"[SoraCleanup] not started (empty schedule)"
)
return
return
}
}
loc
:=
time
.
Local
loc
:=
time
.
Local
...
@@ -59,12 +59,12 @@ func (s *SoraMediaCleanupService) Start() {
...
@@ -59,12 +59,12 @@ func (s *SoraMediaCleanupService) Start() {
}
}
c
:=
cron
.
New
(
cron
.
WithParser
(
soraCleanupCronParser
),
cron
.
WithLocation
(
loc
))
c
:=
cron
.
New
(
cron
.
WithParser
(
soraCleanupCronParser
),
cron
.
WithLocation
(
loc
))
if
_
,
err
:=
c
.
AddFunc
(
schedule
,
func
()
{
s
.
runCleanup
()
});
err
!=
nil
{
if
_
,
err
:=
c
.
AddFunc
(
schedule
,
func
()
{
s
.
runCleanup
()
});
err
!=
nil
{
log
.
Printf
(
"[SoraCleanup] not started (invalid schedule=%q): %v"
,
schedule
,
err
)
log
ger
.
LegacyPrintf
(
"service.sora_media_cleanup"
,
"[SoraCleanup] not started (invalid schedule=%q): %v"
,
schedule
,
err
)
return
return
}
}
s
.
cron
=
c
s
.
cron
=
c
s
.
cron
.
Start
()
s
.
cron
.
Start
()
log
.
Printf
(
"[SoraCleanup] started (schedule=%q tz=%s)"
,
schedule
,
loc
.
String
())
log
ger
.
LegacyPrintf
(
"service.sora_media_cleanup"
,
"[SoraCleanup] started (schedule=%q tz=%s)"
,
schedule
,
loc
.
String
())
})
})
}
}
...
@@ -78,7 +78,7 @@ func (s *SoraMediaCleanupService) Stop() {
...
@@ -78,7 +78,7 @@ func (s *SoraMediaCleanupService) Stop() {
select
{
select
{
case
<-
ctx
.
Done
()
:
case
<-
ctx
.
Done
()
:
case
<-
time
.
After
(
3
*
time
.
Second
)
:
case
<-
time
.
After
(
3
*
time
.
Second
)
:
log
.
Printf
(
"[SoraCleanup] cron stop timed out"
)
log
ger
.
LegacyPrintf
(
"service.sora_media_cleanup"
,
"[SoraCleanup] cron stop timed out"
)
}
}
}
}
})
})
...
@@ -90,7 +90,7 @@ func (s *SoraMediaCleanupService) runCleanup() {
...
@@ -90,7 +90,7 @@ func (s *SoraMediaCleanupService) runCleanup() {
}
}
retention
:=
s
.
cfg
.
Sora
.
Storage
.
Cleanup
.
RetentionDays
retention
:=
s
.
cfg
.
Sora
.
Storage
.
Cleanup
.
RetentionDays
if
retention
<=
0
{
if
retention
<=
0
{
log
.
Printf
(
"[SoraCleanup] skipped (retention_days=%d)"
,
retention
)
log
ger
.
LegacyPrintf
(
"service.sora_media_cleanup"
,
"[SoraCleanup] skipped (retention_days=%d)"
,
retention
)
return
return
}
}
cutoff
:=
time
.
Now
()
.
AddDate
(
0
,
0
,
-
retention
)
cutoff
:=
time
.
Now
()
.
AddDate
(
0
,
0
,
-
retention
)
...
@@ -116,5 +116,5 @@ func (s *SoraMediaCleanupService) runCleanup() {
...
@@ -116,5 +116,5 @@ func (s *SoraMediaCleanupService) runCleanup() {
return
nil
return
nil
})
})
}
}
log
.
Printf
(
"[SoraCleanup] cleanup finished, deleted=%d"
,
deleted
)
log
ger
.
LegacyPrintf
(
"service.sora_media_cleanup"
,
"[SoraCleanup] cleanup finished, deleted=%d"
,
deleted
)
}
}
backend/internal/service/timing_wheel_service.go
View file @
64236361
...
@@ -2,10 +2,10 @@ package service
...
@@ -2,10 +2,10 @@ package service
import
(
import
(
"fmt"
"fmt"
"log"
"sync"
"sync"
"time"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/zeromicro/go-zero/core/collection"
"github.com/zeromicro/go-zero/core/collection"
)
)
...
@@ -34,21 +34,21 @@ func NewTimingWheelService() (*TimingWheelService, error) {
...
@@ -34,21 +34,21 @@ func NewTimingWheelService() (*TimingWheelService, error) {
// Start starts the timing wheel
// Start starts the timing wheel
func
(
s
*
TimingWheelService
)
Start
()
{
func
(
s
*
TimingWheelService
)
Start
()
{
log
.
Println
(
"[TimingWheel] Started (auto-start by go-zero)"
)
log
ger
.
LegacyPrintf
(
"service.timing_wheel"
,
"%s"
,
"[TimingWheel] Started (auto-start by go-zero)"
)
}
}
// Stop stops the timing wheel
// Stop stops the timing wheel
func
(
s
*
TimingWheelService
)
Stop
()
{
func
(
s
*
TimingWheelService
)
Stop
()
{
s
.
stopOnce
.
Do
(
func
()
{
s
.
stopOnce
.
Do
(
func
()
{
s
.
tw
.
Stop
()
s
.
tw
.
Stop
()
log
.
Println
(
"[TimingWheel] Stopped"
)
log
ger
.
LegacyPrintf
(
"service.timing_wheel"
,
"%s"
,
"[TimingWheel] Stopped"
)
})
})
}
}
// Schedule schedules a one-time task
// Schedule schedules a one-time task
func
(
s
*
TimingWheelService
)
Schedule
(
name
string
,
delay
time
.
Duration
,
fn
func
())
{
func
(
s
*
TimingWheelService
)
Schedule
(
name
string
,
delay
time
.
Duration
,
fn
func
())
{
if
err
:=
s
.
tw
.
SetTimer
(
name
,
fn
,
delay
);
err
!=
nil
{
if
err
:=
s
.
tw
.
SetTimer
(
name
,
fn
,
delay
);
err
!=
nil
{
log
.
Printf
(
"[TimingWheel] SetTimer failed for %q: %v"
,
name
,
err
)
log
ger
.
LegacyPrintf
(
"service.timing_wheel"
,
"[TimingWheel] SetTimer failed for %q: %v"
,
name
,
err
)
}
}
}
}
...
@@ -58,11 +58,11 @@ func (s *TimingWheelService) ScheduleRecurring(name string, interval time.Durati
...
@@ -58,11 +58,11 @@ func (s *TimingWheelService) ScheduleRecurring(name string, interval time.Durati
schedule
=
func
()
{
schedule
=
func
()
{
fn
()
fn
()
if
err
:=
s
.
tw
.
SetTimer
(
name
,
schedule
,
interval
);
err
!=
nil
{
if
err
:=
s
.
tw
.
SetTimer
(
name
,
schedule
,
interval
);
err
!=
nil
{
log
.
Printf
(
"[TimingWheel] recurring SetTimer failed for %q: %v"
,
name
,
err
)
log
ger
.
LegacyPrintf
(
"service.timing_wheel"
,
"[TimingWheel] recurring SetTimer failed for %q: %v"
,
name
,
err
)
}
}
}
}
if
err
:=
s
.
tw
.
SetTimer
(
name
,
schedule
,
interval
);
err
!=
nil
{
if
err
:=
s
.
tw
.
SetTimer
(
name
,
schedule
,
interval
);
err
!=
nil
{
log
.
Printf
(
"[TimingWheel] initial SetTimer failed for %q: %v"
,
name
,
err
)
log
ger
.
LegacyPrintf
(
"service.timing_wheel"
,
"[TimingWheel] initial SetTimer failed for %q: %v"
,
name
,
err
)
}
}
}
}
...
...
backend/internal/service/token_refresh_service.go
View file @
64236361
...
@@ -3,7 +3,6 @@ package service
...
@@ -3,7 +3,6 @@ package service
import
(
import
(
"context"
"context"
"fmt"
"fmt"
"log"
"log/slog"
"log/slog"
"strings"
"strings"
"sync"
"sync"
...
@@ -70,22 +69,24 @@ func (s *TokenRefreshService) SetSoraAccountRepo(repo SoraAccountRepository) {
...
@@ -70,22 +69,24 @@ func (s *TokenRefreshService) SetSoraAccountRepo(repo SoraAccountRepository) {
// Start 启动后台刷新服务
// Start 启动后台刷新服务
func
(
s
*
TokenRefreshService
)
Start
()
{
func
(
s
*
TokenRefreshService
)
Start
()
{
if
!
s
.
cfg
.
Enabled
{
if
!
s
.
cfg
.
Enabled
{
log
.
Println
(
"[T
oken
R
efresh
] S
ervice
disabled
by configuration
"
)
s
log
.
Info
(
"t
oken
_r
efresh
.s
ervice
_
disabled"
)
return
return
}
}
s
.
wg
.
Add
(
1
)
s
.
wg
.
Add
(
1
)
go
s
.
refreshLoop
()
go
s
.
refreshLoop
()
log
.
Printf
(
"[TokenRefresh] Service started (check every %d minutes, refresh %v hours before expiry)"
,
slog
.
Info
(
"token_refresh.service_started"
,
s
.
cfg
.
CheckIntervalMinutes
,
s
.
cfg
.
RefreshBeforeExpiryHours
)
"check_interval_minutes"
,
s
.
cfg
.
CheckIntervalMinutes
,
"refresh_before_expiry_hours"
,
s
.
cfg
.
RefreshBeforeExpiryHours
,
)
}
}
// Stop 停止刷新服务
// Stop 停止刷新服务
func
(
s
*
TokenRefreshService
)
Stop
()
{
func
(
s
*
TokenRefreshService
)
Stop
()
{
close
(
s
.
stopCh
)
close
(
s
.
stopCh
)
s
.
wg
.
Wait
()
s
.
wg
.
Wait
()
log
.
Println
(
"[T
oken
R
efresh
] S
ervice
stopped"
)
s
log
.
Info
(
"t
oken
_r
efresh
.s
ervice
_
stopped"
)
}
}
// refreshLoop 刷新循环
// refreshLoop 刷新循环
...
@@ -124,7 +125,7 @@ func (s *TokenRefreshService) processRefresh() {
...
@@ -124,7 +125,7 @@ func (s *TokenRefreshService) processRefresh() {
// 获取所有active状态的账号
// 获取所有active状态的账号
accounts
,
err
:=
s
.
listActiveAccounts
(
ctx
)
accounts
,
err
:=
s
.
listActiveAccounts
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[T
oken
R
efresh
] Failed to
list
accounts
: %v
"
,
err
)
s
log
.
Error
(
"t
oken
_r
efresh
.
list
_
accounts
_failed"
,
"error
"
,
err
)
return
return
}
}
...
@@ -153,10 +154,17 @@ func (s *TokenRefreshService) processRefresh() {
...
@@ -153,10 +154,17 @@ func (s *TokenRefreshService) processRefresh() {
// 执行刷新
// 执行刷新
if
err
:=
s
.
refreshWithRetry
(
ctx
,
account
,
refresher
);
err
!=
nil
{
if
err
:=
s
.
refreshWithRetry
(
ctx
,
account
,
refresher
);
err
!=
nil
{
log
.
Printf
(
"[TokenRefresh] Account %d (%s) failed: %v"
,
account
.
ID
,
account
.
Name
,
err
)
slog
.
Warn
(
"token_refresh.account_refresh_failed"
,
"account_id"
,
account
.
ID
,
"account_name"
,
account
.
Name
,
"error"
,
err
,
)
failed
++
failed
++
}
else
{
}
else
{
log
.
Printf
(
"[TokenRefresh] Account %d (%s) refreshed successfully"
,
account
.
ID
,
account
.
Name
)
slog
.
Info
(
"token_refresh.account_refreshed"
,
"account_id"
,
account
.
ID
,
"account_name"
,
account
.
Name
,
)
refreshed
++
refreshed
++
}
}
...
@@ -167,12 +175,17 @@ func (s *TokenRefreshService) processRefresh() {
...
@@ -167,12 +175,17 @@ func (s *TokenRefreshService) processRefresh() {
// 无刷新活动时降级为 Debug,有实际刷新活动时保持 Info
// 无刷新活动时降级为 Debug,有实际刷新活动时保持 Info
if
needsRefresh
==
0
&&
failed
==
0
{
if
needsRefresh
==
0
&&
failed
==
0
{
slog
.
Debug
(
"
[T
oken
R
efresh
] C
ycle
complete"
,
slog
.
Debug
(
"
t
oken
_r
efresh
.c
ycle
_
complete
d
"
,
"total"
,
totalAccounts
,
"oauth"
,
oauthAccounts
,
"total"
,
totalAccounts
,
"oauth"
,
oauthAccounts
,
"needs_refresh"
,
needsRefresh
,
"refreshed"
,
refreshed
,
"failed"
,
failed
)
"needs_refresh"
,
needsRefresh
,
"refreshed"
,
refreshed
,
"failed"
,
failed
)
}
else
{
}
else
{
log
.
Printf
(
"[TokenRefresh] Cycle complete: total=%d, oauth=%d, needs_refresh=%d, refreshed=%d, failed=%d"
,
slog
.
Info
(
"token_refresh.cycle_completed"
,
totalAccounts
,
oauthAccounts
,
needsRefresh
,
refreshed
,
failed
)
"total"
,
totalAccounts
,
"oauth"
,
oauthAccounts
,
"needs_refresh"
,
needsRefresh
,
"refreshed"
,
refreshed
,
"failed"
,
failed
,
)
}
}
}
}
...
@@ -207,26 +220,35 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
...
@@ -207,26 +220,35 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
account
.
Status
==
StatusError
&&
account
.
Status
==
StatusError
&&
strings
.
Contains
(
account
.
ErrorMessage
,
"missing_project_id:"
)
{
strings
.
Contains
(
account
.
ErrorMessage
,
"missing_project_id:"
)
{
if
clearErr
:=
s
.
accountRepo
.
ClearError
(
ctx
,
account
.
ID
);
clearErr
!=
nil
{
if
clearErr
:=
s
.
accountRepo
.
ClearError
(
ctx
,
account
.
ID
);
clearErr
!=
nil
{
log
.
Printf
(
"[TokenRefresh] Failed to clear error status for account %d: %v"
,
account
.
ID
,
clearErr
)
slog
.
Warn
(
"token_refresh.clear_account_error_failed"
,
"account_id"
,
account
.
ID
,
"error"
,
clearErr
,
)
}
else
{
}
else
{
log
.
Printf
(
"[T
oken
R
efresh
] Account %d:
cleared
missing_project_id
error"
,
account
.
ID
)
s
log
.
Info
(
"t
oken
_r
efresh
.
cleared
_
missing_project_id
_
error"
,
"account_id"
,
account
.
ID
)
}
}
}
}
// 对所有 OAuth 账号调用缓存失效(InvalidateToken 内部根据平台判断是否需要处理)
// 对所有 OAuth 账号调用缓存失效(InvalidateToken 内部根据平台判断是否需要处理)
if
s
.
cacheInvalidator
!=
nil
&&
account
.
Type
==
AccountTypeOAuth
{
if
s
.
cacheInvalidator
!=
nil
&&
account
.
Type
==
AccountTypeOAuth
{
if
err
:=
s
.
cacheInvalidator
.
InvalidateToken
(
ctx
,
account
);
err
!=
nil
{
if
err
:=
s
.
cacheInvalidator
.
InvalidateToken
(
ctx
,
account
);
err
!=
nil
{
log
.
Printf
(
"[TokenRefresh] Failed to invalidate token cache for account %d: %v"
,
account
.
ID
,
err
)
slog
.
Warn
(
"token_refresh.invalidate_token_cache_failed"
,
"account_id"
,
account
.
ID
,
"error"
,
err
,
)
}
else
{
}
else
{
log
.
Printf
(
"[T
oken
R
efresh
] T
oken
cache
invalidated
for
account
%
d"
,
account
.
ID
)
s
log
.
Debug
(
"t
oken
_r
efresh
.t
oken
_
cache
_
invalidated
"
,
"
account
_i
d"
,
account
.
ID
)
}
}
}
}
// 同步更新调度器缓存,确保调度获取的 Account 对象包含最新的 credentials
// 同步更新调度器缓存,确保调度获取的 Account 对象包含最新的 credentials
// 这解决了 token 刷新后调度器缓存数据不一致的问题(#445)
// 这解决了 token 刷新后调度器缓存数据不一致的问题(#445)
if
s
.
schedulerCache
!=
nil
{
if
s
.
schedulerCache
!=
nil
{
if
err
:=
s
.
schedulerCache
.
SetAccount
(
ctx
,
account
);
err
!=
nil
{
if
err
:=
s
.
schedulerCache
.
SetAccount
(
ctx
,
account
);
err
!=
nil
{
log
.
Printf
(
"[TokenRefresh] Failed to sync scheduler cache for account %d: %v"
,
account
.
ID
,
err
)
slog
.
Warn
(
"token_refresh.sync_scheduler_cache_failed"
,
"account_id"
,
account
.
ID
,
"error"
,
err
,
)
}
else
{
}
else
{
log
.
Printf
(
"[T
oken
R
efresh
] S
cheduler
cache
synced
for
account
%
d"
,
account
.
ID
)
s
log
.
Debug
(
"t
oken
_r
efresh
.s
cheduler
_
cache
_
synced
"
,
"
account
_i
d"
,
account
.
ID
)
}
}
}
}
return
nil
return
nil
...
@@ -236,14 +258,21 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
...
@@ -236,14 +258,21 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
if
account
.
Platform
==
PlatformAntigravity
&&
isNonRetryableRefreshError
(
err
)
{
if
account
.
Platform
==
PlatformAntigravity
&&
isNonRetryableRefreshError
(
err
)
{
errorMsg
:=
fmt
.
Sprintf
(
"Token refresh failed (non-retryable): %v"
,
err
)
errorMsg
:=
fmt
.
Sprintf
(
"Token refresh failed (non-retryable): %v"
,
err
)
if
setErr
:=
s
.
accountRepo
.
SetError
(
ctx
,
account
.
ID
,
errorMsg
);
setErr
!=
nil
{
if
setErr
:=
s
.
accountRepo
.
SetError
(
ctx
,
account
.
ID
,
errorMsg
);
setErr
!=
nil
{
log
.
Printf
(
"[TokenRefresh] Failed to set error status for account %d: %v"
,
account
.
ID
,
setErr
)
slog
.
Error
(
"token_refresh.set_error_status_failed"
,
"account_id"
,
account
.
ID
,
"error"
,
setErr
,
)
}
}
return
err
return
err
}
}
lastErr
=
err
lastErr
=
err
log
.
Printf
(
"[TokenRefresh] Account %d attempt %d/%d failed: %v"
,
slog
.
Warn
(
"token_refresh.retry_attempt_failed"
,
account
.
ID
,
attempt
,
s
.
cfg
.
MaxRetries
,
err
)
"account_id"
,
account
.
ID
,
"attempt"
,
attempt
,
"max_retries"
,
s
.
cfg
.
MaxRetries
,
"error"
,
err
,
)
// 如果还有重试机会,等待后重试
// 如果还有重试机会,等待后重试
if
attempt
<
s
.
cfg
.
MaxRetries
{
if
attempt
<
s
.
cfg
.
MaxRetries
{
...
@@ -256,11 +285,18 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
...
@@ -256,11 +285,18 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
// Antigravity 账户:其他错误仅记录日志,不标记 error(可能是临时网络问题)
// Antigravity 账户:其他错误仅记录日志,不标记 error(可能是临时网络问题)
// 其他平台账户:重试失败后标记 error
// 其他平台账户:重试失败后标记 error
if
account
.
Platform
==
PlatformAntigravity
{
if
account
.
Platform
==
PlatformAntigravity
{
log
.
Printf
(
"[TokenRefresh] Account %d: refresh failed after %d retries: %v"
,
account
.
ID
,
s
.
cfg
.
MaxRetries
,
lastErr
)
slog
.
Warn
(
"token_refresh.retry_exhausted_antigravity"
,
"account_id"
,
account
.
ID
,
"max_retries"
,
s
.
cfg
.
MaxRetries
,
"error"
,
lastErr
,
)
}
else
{
}
else
{
errorMsg
:=
fmt
.
Sprintf
(
"Token refresh failed after %d retries: %v"
,
s
.
cfg
.
MaxRetries
,
lastErr
)
errorMsg
:=
fmt
.
Sprintf
(
"Token refresh failed after %d retries: %v"
,
s
.
cfg
.
MaxRetries
,
lastErr
)
if
err
:=
s
.
accountRepo
.
SetError
(
ctx
,
account
.
ID
,
errorMsg
);
err
!=
nil
{
if
err
:=
s
.
accountRepo
.
SetError
(
ctx
,
account
.
ID
,
errorMsg
);
err
!=
nil
{
log
.
Printf
(
"[TokenRefresh] Failed to set error status for account %d: %v"
,
account
.
ID
,
err
)
slog
.
Error
(
"token_refresh.set_error_status_failed"
,
"account_id"
,
account
.
ID
,
"error"
,
err
,
)
}
}
}
}
...
...
backend/internal/service/turnstile_service.go
View file @
64236361
...
@@ -3,9 +3,9 @@ package service
...
@@ -3,9 +3,9 @@ package service
import
(
import
(
"context"
"context"
"fmt"
"fmt"
"log"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
)
)
var
(
var
(
...
@@ -47,36 +47,36 @@ func NewTurnstileService(settingService *SettingService, verifier TurnstileVerif
...
@@ -47,36 +47,36 @@ func NewTurnstileService(settingService *SettingService, verifier TurnstileVerif
func
(
s
*
TurnstileService
)
VerifyToken
(
ctx
context
.
Context
,
token
string
,
remoteIP
string
)
error
{
func
(
s
*
TurnstileService
)
VerifyToken
(
ctx
context
.
Context
,
token
string
,
remoteIP
string
)
error
{
// 检查是否启用 Turnstile
// 检查是否启用 Turnstile
if
!
s
.
settingService
.
IsTurnstileEnabled
(
ctx
)
{
if
!
s
.
settingService
.
IsTurnstileEnabled
(
ctx
)
{
log
.
Println
(
"[Turnstile] Disabled, skipping verification"
)
log
ger
.
LegacyPrintf
(
"service.turnstile"
,
"%s"
,
"[Turnstile] Disabled, skipping verification"
)
return
nil
return
nil
}
}
// 获取 Secret Key
// 获取 Secret Key
secretKey
:=
s
.
settingService
.
GetTurnstileSecretKey
(
ctx
)
secretKey
:=
s
.
settingService
.
GetTurnstileSecretKey
(
ctx
)
if
secretKey
==
""
{
if
secretKey
==
""
{
log
.
Println
(
"[Turnstile] Secret key not configured"
)
log
ger
.
LegacyPrintf
(
"service.turnstile"
,
"%s"
,
"[Turnstile] Secret key not configured"
)
return
ErrTurnstileNotConfigured
return
ErrTurnstileNotConfigured
}
}
// 如果 token 为空,返回错误
// 如果 token 为空,返回错误
if
token
==
""
{
if
token
==
""
{
log
.
Println
(
"[Turnstile] Token is empty"
)
log
ger
.
LegacyPrintf
(
"service.turnstile"
,
"%s"
,
"[Turnstile] Token is empty"
)
return
ErrTurnstileVerificationFailed
return
ErrTurnstileVerificationFailed
}
}
log
.
Printf
(
"[Turnstile] Verifying token for IP: %s"
,
remoteIP
)
log
ger
.
LegacyPrintf
(
"service.turnstile"
,
"[Turnstile] Verifying token for IP: %s"
,
remoteIP
)
result
,
err
:=
s
.
verifier
.
VerifyToken
(
ctx
,
secretKey
,
token
,
remoteIP
)
result
,
err
:=
s
.
verifier
.
VerifyToken
(
ctx
,
secretKey
,
token
,
remoteIP
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[Turnstile] Request failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.turnstile"
,
"[Turnstile] Request failed: %v"
,
err
)
return
fmt
.
Errorf
(
"send request: %w"
,
err
)
return
fmt
.
Errorf
(
"send request: %w"
,
err
)
}
}
if
!
result
.
Success
{
if
!
result
.
Success
{
log
.
Printf
(
"[Turnstile] Verification failed, error codes: %v"
,
result
.
ErrorCodes
)
log
ger
.
LegacyPrintf
(
"service.turnstile"
,
"[Turnstile] Verification failed, error codes: %v"
,
result
.
ErrorCodes
)
return
ErrTurnstileVerificationFailed
return
ErrTurnstileVerificationFailed
}
}
log
.
Println
(
"[Turnstile] Verification successful"
)
log
ger
.
LegacyPrintf
(
"service.turnstile"
,
"%s"
,
"[Turnstile] Verification successful"
)
return
nil
return
nil
}
}
...
...
backend/internal/service/usage_cleanup_service.go
View file @
64236361
...
@@ -5,7 +5,6 @@ import (
...
@@ -5,7 +5,6 @@ import (
"database/sql"
"database/sql"
"errors"
"errors"
"fmt"
"fmt"
"log"
"log/slog"
"log/slog"
"net/http"
"net/http"
"strings"
"strings"
...
@@ -15,6 +14,7 @@ import (
...
@@ -15,6 +14,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
)
)
...
@@ -82,18 +82,18 @@ func (s *UsageCleanupService) Start() {
...
@@ -82,18 +82,18 @@ func (s *UsageCleanupService) Start() {
return
return
}
}
if
s
.
cfg
!=
nil
&&
!
s
.
cfg
.
UsageCleanup
.
Enabled
{
if
s
.
cfg
!=
nil
&&
!
s
.
cfg
.
UsageCleanup
.
Enabled
{
log
.
Printf
(
"[UsageCleanup] not started (disabled)"
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] not started (disabled)"
)
return
return
}
}
if
s
.
repo
==
nil
||
s
.
timingWheel
==
nil
{
if
s
.
repo
==
nil
||
s
.
timingWheel
==
nil
{
log
.
Printf
(
"[UsageCleanup] not started (missing deps)"
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] not started (missing deps)"
)
return
return
}
}
interval
:=
s
.
workerInterval
()
interval
:=
s
.
workerInterval
()
s
.
startOnce
.
Do
(
func
()
{
s
.
startOnce
.
Do
(
func
()
{
s
.
timingWheel
.
ScheduleRecurring
(
usageCleanupWorkerName
,
interval
,
s
.
runOnce
)
s
.
timingWheel
.
ScheduleRecurring
(
usageCleanupWorkerName
,
interval
,
s
.
runOnce
)
log
.
Printf
(
"[UsageCleanup] started (interval=%s max_range_days=%d batch_size=%d task_timeout=%s)"
,
interval
,
s
.
maxRangeDays
(),
s
.
batchSize
(),
s
.
taskTimeout
())
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] started (interval=%s max_range_days=%d batch_size=%d task_timeout=%s)"
,
interval
,
s
.
maxRangeDays
(),
s
.
batchSize
(),
s
.
taskTimeout
())
})
})
}
}
...
@@ -108,7 +108,7 @@ func (s *UsageCleanupService) Stop() {
...
@@ -108,7 +108,7 @@ func (s *UsageCleanupService) Stop() {
if
s
.
timingWheel
!=
nil
{
if
s
.
timingWheel
!=
nil
{
s
.
timingWheel
.
Cancel
(
usageCleanupWorkerName
)
s
.
timingWheel
.
Cancel
(
usageCleanupWorkerName
)
}
}
log
.
Printf
(
"[UsageCleanup] stopped"
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] stopped"
)
})
})
}
}
...
@@ -130,10 +130,10 @@ func (s *UsageCleanupService) CreateTask(ctx context.Context, filters UsageClean
...
@@ -130,10 +130,10 @@ func (s *UsageCleanupService) CreateTask(ctx context.Context, filters UsageClean
return
nil
,
infraerrors
.
BadRequest
(
"USAGE_CLEANUP_INVALID_CREATOR"
,
"invalid creator"
)
return
nil
,
infraerrors
.
BadRequest
(
"USAGE_CLEANUP_INVALID_CREATOR"
,
"invalid creator"
)
}
}
log
.
Printf
(
"[UsageCleanup] create_task requested: operator=%d %s"
,
createdBy
,
describeUsageCleanupFilters
(
filters
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] create_task requested: operator=%d %s"
,
createdBy
,
describeUsageCleanupFilters
(
filters
))
sanitizeUsageCleanupFilters
(
&
filters
)
sanitizeUsageCleanupFilters
(
&
filters
)
if
err
:=
s
.
validateFilters
(
filters
);
err
!=
nil
{
if
err
:=
s
.
validateFilters
(
filters
);
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] create_task rejected: operator=%d err=%v %s"
,
createdBy
,
err
,
describeUsageCleanupFilters
(
filters
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] create_task rejected: operator=%d err=%v %s"
,
createdBy
,
err
,
describeUsageCleanupFilters
(
filters
))
return
nil
,
err
return
nil
,
err
}
}
...
@@ -143,10 +143,10 @@ func (s *UsageCleanupService) CreateTask(ctx context.Context, filters UsageClean
...
@@ -143,10 +143,10 @@ func (s *UsageCleanupService) CreateTask(ctx context.Context, filters UsageClean
CreatedBy
:
createdBy
,
CreatedBy
:
createdBy
,
}
}
if
err
:=
s
.
repo
.
CreateTask
(
ctx
,
task
);
err
!=
nil
{
if
err
:=
s
.
repo
.
CreateTask
(
ctx
,
task
);
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] create_task persist failed: operator=%d err=%v %s"
,
createdBy
,
err
,
describeUsageCleanupFilters
(
filters
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] create_task persist failed: operator=%d err=%v %s"
,
createdBy
,
err
,
describeUsageCleanupFilters
(
filters
))
return
nil
,
fmt
.
Errorf
(
"create cleanup task: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"create cleanup task: %w"
,
err
)
}
}
log
.
Printf
(
"[UsageCleanup] create_task persisted: task=%d operator=%d status=%s deleted_rows=%d %s"
,
task
.
ID
,
createdBy
,
task
.
Status
,
task
.
DeletedRows
,
describeUsageCleanupFilters
(
filters
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] create_task persisted: task=%d operator=%d status=%s deleted_rows=%d %s"
,
task
.
ID
,
createdBy
,
task
.
Status
,
task
.
DeletedRows
,
describeUsageCleanupFilters
(
filters
))
go
s
.
runOnce
()
go
s
.
runOnce
()
return
task
,
nil
return
task
,
nil
}
}
...
@@ -157,7 +157,7 @@ func (s *UsageCleanupService) runOnce() {
...
@@ -157,7 +157,7 @@ func (s *UsageCleanupService) runOnce() {
return
return
}
}
if
!
atomic
.
CompareAndSwapInt32
(
&
svc
.
running
,
0
,
1
)
{
if
!
atomic
.
CompareAndSwapInt32
(
&
svc
.
running
,
0
,
1
)
{
log
.
Printf
(
"[UsageCleanup] run_once skipped: already_running=true"
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] run_once skipped: already_running=true"
)
return
return
}
}
defer
atomic
.
StoreInt32
(
&
svc
.
running
,
0
)
defer
atomic
.
StoreInt32
(
&
svc
.
running
,
0
)
...
@@ -171,7 +171,7 @@ func (s *UsageCleanupService) runOnce() {
...
@@ -171,7 +171,7 @@ func (s *UsageCleanupService) runOnce() {
task
,
err
:=
svc
.
repo
.
ClaimNextPendingTask
(
ctx
,
int64
(
svc
.
taskTimeout
()
.
Seconds
()))
task
,
err
:=
svc
.
repo
.
ClaimNextPendingTask
(
ctx
,
int64
(
svc
.
taskTimeout
()
.
Seconds
()))
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] claim pending task failed: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] claim pending task failed: %v"
,
err
)
return
return
}
}
if
task
==
nil
{
if
task
==
nil
{
...
@@ -179,7 +179,7 @@ func (s *UsageCleanupService) runOnce() {
...
@@ -179,7 +179,7 @@ func (s *UsageCleanupService) runOnce() {
return
return
}
}
log
.
Printf
(
"[UsageCleanup] task claimed: task=%d status=%s created_by=%d deleted_rows=%d %s"
,
task
.
ID
,
task
.
Status
,
task
.
CreatedBy
,
task
.
DeletedRows
,
describeUsageCleanupFilters
(
task
.
Filters
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task claimed: task=%d status=%s created_by=%d deleted_rows=%d %s"
,
task
.
ID
,
task
.
Status
,
task
.
CreatedBy
,
task
.
DeletedRows
,
describeUsageCleanupFilters
(
task
.
Filters
))
svc
.
executeTask
(
ctx
,
task
)
svc
.
executeTask
(
ctx
,
task
)
}
}
...
@@ -191,12 +191,12 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
...
@@ -191,12 +191,12 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
batchSize
:=
s
.
batchSize
()
batchSize
:=
s
.
batchSize
()
deletedTotal
:=
task
.
DeletedRows
deletedTotal
:=
task
.
DeletedRows
start
:=
time
.
Now
()
start
:=
time
.
Now
()
log
.
Printf
(
"[UsageCleanup] task started: task=%d batch_size=%d deleted_rows=%d %s"
,
task
.
ID
,
batchSize
,
deletedTotal
,
describeUsageCleanupFilters
(
task
.
Filters
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task started: task=%d batch_size=%d deleted_rows=%d %s"
,
task
.
ID
,
batchSize
,
deletedTotal
,
describeUsageCleanupFilters
(
task
.
Filters
))
var
batchNum
int
var
batchNum
int
for
{
for
{
if
ctx
!=
nil
&&
ctx
.
Err
()
!=
nil
{
if
ctx
!=
nil
&&
ctx
.
Err
()
!=
nil
{
log
.
Printf
(
"[UsageCleanup] task interrupted: task=%d err=%v"
,
task
.
ID
,
ctx
.
Err
())
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task interrupted: task=%d err=%v"
,
task
.
ID
,
ctx
.
Err
())
return
return
}
}
canceled
,
err
:=
s
.
isTaskCanceled
(
ctx
,
task
.
ID
)
canceled
,
err
:=
s
.
isTaskCanceled
(
ctx
,
task
.
ID
)
...
@@ -205,7 +205,7 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
...
@@ -205,7 +205,7 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
return
return
}
}
if
canceled
{
if
canceled
{
log
.
Printf
(
"[UsageCleanup] task canceled: task=%d deleted_rows=%d duration=%s"
,
task
.
ID
,
deletedTotal
,
time
.
Since
(
start
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task canceled: task=%d deleted_rows=%d duration=%s"
,
task
.
ID
,
deletedTotal
,
time
.
Since
(
start
))
return
return
}
}
...
@@ -214,7 +214,7 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
...
@@ -214,7 +214,7 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
if
err
!=
nil
{
if
err
!=
nil
{
if
errors
.
Is
(
err
,
context
.
Canceled
)
||
errors
.
Is
(
err
,
context
.
DeadlineExceeded
)
{
if
errors
.
Is
(
err
,
context
.
Canceled
)
||
errors
.
Is
(
err
,
context
.
DeadlineExceeded
)
{
// 任务被中断(例如服务停止/超时),保持 running 状态,后续通过 stale reclaim 续跑。
// 任务被中断(例如服务停止/超时),保持 running 状态,后续通过 stale reclaim 续跑。
log
.
Printf
(
"[UsageCleanup] task interrupted: task=%d err=%v"
,
task
.
ID
,
err
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task interrupted: task=%d err=%v"
,
task
.
ID
,
err
)
return
return
}
}
s
.
markTaskFailed
(
task
.
ID
,
deletedTotal
,
err
)
s
.
markTaskFailed
(
task
.
ID
,
deletedTotal
,
err
)
...
@@ -224,12 +224,12 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
...
@@ -224,12 +224,12 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
if
deleted
>
0
{
if
deleted
>
0
{
updateCtx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
3
*
time
.
Second
)
updateCtx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
3
*
time
.
Second
)
if
err
:=
s
.
repo
.
UpdateTaskProgress
(
updateCtx
,
task
.
ID
,
deletedTotal
);
err
!=
nil
{
if
err
:=
s
.
repo
.
UpdateTaskProgress
(
updateCtx
,
task
.
ID
,
deletedTotal
);
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] task progress update failed: task=%d deleted_rows=%d err=%v"
,
task
.
ID
,
deletedTotal
,
err
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task progress update failed: task=%d deleted_rows=%d err=%v"
,
task
.
ID
,
deletedTotal
,
err
)
}
}
cancel
()
cancel
()
}
}
if
batchNum
<=
3
||
batchNum
%
20
==
0
||
deleted
<
int64
(
batchSize
)
{
if
batchNum
<=
3
||
batchNum
%
20
==
0
||
deleted
<
int64
(
batchSize
)
{
log
.
Printf
(
"[UsageCleanup] task batch done: task=%d batch=%d deleted=%d deleted_total=%d"
,
task
.
ID
,
batchNum
,
deleted
,
deletedTotal
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task batch done: task=%d batch=%d deleted=%d deleted_total=%d"
,
task
.
ID
,
batchNum
,
deleted
,
deletedTotal
)
}
}
if
deleted
==
0
||
deleted
<
int64
(
batchSize
)
{
if
deleted
==
0
||
deleted
<
int64
(
batchSize
)
{
break
break
...
@@ -239,16 +239,16 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
...
@@ -239,16 +239,16 @@ func (s *UsageCleanupService) executeTask(ctx context.Context, task *UsageCleanu
updateCtx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
updateCtx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
defer
cancel
()
defer
cancel
()
if
err
:=
s
.
repo
.
MarkTaskSucceeded
(
updateCtx
,
task
.
ID
,
deletedTotal
);
err
!=
nil
{
if
err
:=
s
.
repo
.
MarkTaskSucceeded
(
updateCtx
,
task
.
ID
,
deletedTotal
);
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] update task succeeded failed: task=%d err=%v"
,
task
.
ID
,
err
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] update task succeeded failed: task=%d err=%v"
,
task
.
ID
,
err
)
}
else
{
}
else
{
log
.
Printf
(
"[UsageCleanup] task succeeded: task=%d deleted_rows=%d duration=%s"
,
task
.
ID
,
deletedTotal
,
time
.
Since
(
start
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task succeeded: task=%d deleted_rows=%d duration=%s"
,
task
.
ID
,
deletedTotal
,
time
.
Since
(
start
))
}
}
if
s
.
dashboard
!=
nil
{
if
s
.
dashboard
!=
nil
{
if
err
:=
s
.
dashboard
.
TriggerRecomputeRange
(
task
.
Filters
.
StartTime
,
task
.
Filters
.
EndTime
);
err
!=
nil
{
if
err
:=
s
.
dashboard
.
TriggerRecomputeRange
(
task
.
Filters
.
StartTime
,
task
.
Filters
.
EndTime
);
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] trigger dashboard recompute failed: task=%d err=%v"
,
task
.
ID
,
err
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] trigger dashboard recompute failed: task=%d err=%v"
,
task
.
ID
,
err
)
}
else
{
}
else
{
log
.
Printf
(
"[UsageCleanup] trigger dashboard recompute: task=%d start=%s end=%s"
,
task
.
ID
,
task
.
Filters
.
StartTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
task
.
Filters
.
EndTime
.
UTC
()
.
Format
(
time
.
RFC3339
))
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] trigger dashboard recompute: task=%d start=%s end=%s"
,
task
.
ID
,
task
.
Filters
.
StartTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
task
.
Filters
.
EndTime
.
UTC
()
.
Format
(
time
.
RFC3339
))
}
}
}
}
}
}
...
@@ -258,11 +258,11 @@ func (s *UsageCleanupService) markTaskFailed(taskID int64, deletedRows int64, er
...
@@ -258,11 +258,11 @@ func (s *UsageCleanupService) markTaskFailed(taskID int64, deletedRows int64, er
if
len
(
msg
)
>
500
{
if
len
(
msg
)
>
500
{
msg
=
msg
[
:
500
]
msg
=
msg
[
:
500
]
}
}
log
.
Printf
(
"[UsageCleanup] task failed: task=%d deleted_rows=%d err=%s"
,
taskID
,
deletedRows
,
msg
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task failed: task=%d deleted_rows=%d err=%s"
,
taskID
,
deletedRows
,
msg
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
defer
cancel
()
defer
cancel
()
if
updateErr
:=
s
.
repo
.
MarkTaskFailed
(
ctx
,
taskID
,
deletedRows
,
msg
);
updateErr
!=
nil
{
if
updateErr
:=
s
.
repo
.
MarkTaskFailed
(
ctx
,
taskID
,
deletedRows
,
msg
);
updateErr
!=
nil
{
log
.
Printf
(
"[UsageCleanup] update task failed failed: task=%d err=%v"
,
taskID
,
updateErr
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] update task failed failed: task=%d err=%v"
,
taskID
,
updateErr
)
}
}
}
}
...
@@ -280,7 +280,7 @@ func (s *UsageCleanupService) isTaskCanceled(ctx context.Context, taskID int64)
...
@@ -280,7 +280,7 @@ func (s *UsageCleanupService) isTaskCanceled(ctx context.Context, taskID int64)
return
false
,
err
return
false
,
err
}
}
if
status
==
UsageCleanupStatusCanceled
{
if
status
==
UsageCleanupStatusCanceled
{
log
.
Printf
(
"[UsageCleanup] task cancel detected: task=%d"
,
taskID
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] task cancel detected: task=%d"
,
taskID
)
}
}
return
status
==
UsageCleanupStatusCanceled
,
nil
return
status
==
UsageCleanupStatusCanceled
,
nil
}
}
...
@@ -319,7 +319,7 @@ func (s *UsageCleanupService) CancelTask(ctx context.Context, taskID int64, canc
...
@@ -319,7 +319,7 @@ func (s *UsageCleanupService) CancelTask(ctx context.Context, taskID int64, canc
}
}
return
err
return
err
}
}
log
.
Printf
(
"[UsageCleanup] cancel_task requested: task=%d operator=%d status=%s"
,
taskID
,
canceledBy
,
status
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] cancel_task requested: task=%d operator=%d status=%s"
,
taskID
,
canceledBy
,
status
)
if
status
!=
UsageCleanupStatusPending
&&
status
!=
UsageCleanupStatusRunning
{
if
status
!=
UsageCleanupStatusPending
&&
status
!=
UsageCleanupStatusRunning
{
return
infraerrors
.
New
(
http
.
StatusConflict
,
"USAGE_CLEANUP_CANCEL_CONFLICT"
,
"cleanup task cannot be canceled in current status"
)
return
infraerrors
.
New
(
http
.
StatusConflict
,
"USAGE_CLEANUP_CANCEL_CONFLICT"
,
"cleanup task cannot be canceled in current status"
)
}
}
...
@@ -331,7 +331,7 @@ func (s *UsageCleanupService) CancelTask(ctx context.Context, taskID int64, canc
...
@@ -331,7 +331,7 @@ func (s *UsageCleanupService) CancelTask(ctx context.Context, taskID int64, canc
// 状态可能并发改变
// 状态可能并发改变
return
infraerrors
.
New
(
http
.
StatusConflict
,
"USAGE_CLEANUP_CANCEL_CONFLICT"
,
"cleanup task cannot be canceled in current status"
)
return
infraerrors
.
New
(
http
.
StatusConflict
,
"USAGE_CLEANUP_CANCEL_CONFLICT"
,
"cleanup task cannot be canceled in current status"
)
}
}
log
.
Printf
(
"[UsageCleanup] cancel_task done: task=%d operator=%d"
,
taskID
,
canceledBy
)
log
ger
.
LegacyPrintf
(
"service.usage_cleanup"
,
"[UsageCleanup] cancel_task done: task=%d operator=%d"
,
taskID
,
canceledBy
)
return
nil
return
nil
}
}
...
...
backend/internal/service/wire.go
View file @
64236361
...
@@ -6,6 +6,7 @@ import (
...
@@ -6,6 +6,7 @@ import (
"time"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/google/wire"
"github.com/google/wire"
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/v9"
)
)
...
@@ -193,6 +194,13 @@ func ProvideOpsCleanupService(
...
@@ -193,6 +194,13 @@ func ProvideOpsCleanupService(
return
svc
return
svc
}
}
func
ProvideOpsSystemLogSink
(
opsRepo
OpsRepository
)
*
OpsSystemLogSink
{
sink
:=
NewOpsSystemLogSink
(
opsRepo
)
sink
.
Start
()
logger
.
SetSink
(
sink
)
return
sink
}
// ProvideSoraMediaStorage 初始化 Sora 媒体存储
// ProvideSoraMediaStorage 初始化 Sora 媒体存储
func
ProvideSoraMediaStorage
(
cfg
*
config
.
Config
)
*
SoraMediaStorage
{
func
ProvideSoraMediaStorage
(
cfg
*
config
.
Config
)
*
SoraMediaStorage
{
return
NewSoraMediaStorage
(
cfg
)
return
NewSoraMediaStorage
(
cfg
)
...
@@ -268,6 +276,7 @@ var ProviderSet = wire.NewSet(
...
@@ -268,6 +276,7 @@ var ProviderSet = wire.NewSet(
NewAccountUsageService
,
NewAccountUsageService
,
NewAccountTestService
,
NewAccountTestService
,
NewSettingService
,
NewSettingService
,
ProvideOpsSystemLogSink
,
NewOpsService
,
NewOpsService
,
ProvideOpsMetricsCollector
,
ProvideOpsMetricsCollector
,
ProvideOpsAggregationService
,
ProvideOpsAggregationService
,
...
...
backend/internal/setup/setup.go
View file @
64236361
...
@@ -7,11 +7,12 @@ import (
...
@@ -7,11 +7,12 @@ import (
"database/sql"
"database/sql"
"encoding/hex"
"encoding/hex"
"fmt"
"fmt"
"log"
"os"
"os"
"strconv"
"strconv"
"strings"
"time"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/repository"
"github.com/Wei-Shaw/sub2api/internal/repository"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/Wei-Shaw/sub2api/internal/service"
...
@@ -103,6 +104,36 @@ type JWTConfig struct {
...
@@ -103,6 +104,36 @@ type JWTConfig struct {
ExpireHour
int
`json:"expire_hour" yaml:"expire_hour"`
ExpireHour
int
`json:"expire_hour" yaml:"expire_hour"`
}
}
const
(
adminBootstrapReasonEmptyDatabase
=
"empty_database"
adminBootstrapReasonAdminExists
=
"admin_exists"
adminBootstrapReasonUsersExistWithoutAdmin
=
"users_exist_without_admin"
)
type
adminBootstrapDecision
struct
{
shouldCreate
bool
reason
string
}
func
decideAdminBootstrap
(
totalUsers
,
adminUsers
int64
)
adminBootstrapDecision
{
if
adminUsers
>
0
{
return
adminBootstrapDecision
{
shouldCreate
:
false
,
reason
:
adminBootstrapReasonAdminExists
,
}
}
if
totalUsers
>
0
{
return
adminBootstrapDecision
{
shouldCreate
:
false
,
reason
:
adminBootstrapReasonUsersExistWithoutAdmin
,
}
}
return
adminBootstrapDecision
{
shouldCreate
:
true
,
reason
:
adminBootstrapReasonEmptyDatabase
,
}
}
// NeedsSetup checks if the system needs initial setup
// NeedsSetup checks if the system needs initial setup
// Uses multiple checks to prevent attackers from forcing re-setup by deleting config
// Uses multiple checks to prevent attackers from forcing re-setup by deleting config
func
NeedsSetup
()
bool
{
func
NeedsSetup
()
bool
{
...
@@ -137,7 +168,7 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error {
...
@@ -137,7 +168,7 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error {
return
return
}
}
if
err
:=
db
.
Close
();
err
!=
nil
{
if
err
:=
db
.
Close
();
err
!=
nil
{
log
.
Printf
(
"failed to close postgres connection: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"setup"
,
"failed to close postgres connection: %v"
,
err
)
}
}
}()
}()
...
@@ -164,12 +195,12 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error {
...
@@ -164,12 +195,12 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error {
if
err
!=
nil
{
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to create database '%s': %w"
,
cfg
.
DBName
,
err
)
return
fmt
.
Errorf
(
"failed to create database '%s': %w"
,
cfg
.
DBName
,
err
)
}
}
log
.
Printf
(
"Database '%s' created successfully"
,
cfg
.
DBName
)
log
ger
.
LegacyPrintf
(
"setup"
,
"Database '%s' created successfully"
,
cfg
.
DBName
)
}
}
// Now connect to the target database to verify
// Now connect to the target database to verify
if
err
:=
db
.
Close
();
err
!=
nil
{
if
err
:=
db
.
Close
();
err
!=
nil
{
log
.
Printf
(
"failed to close postgres connection: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"setup"
,
"failed to close postgres connection: %v"
,
err
)
}
}
db
=
nil
db
=
nil
...
@@ -185,7 +216,7 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error {
...
@@ -185,7 +216,7 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error {
defer
func
()
{
defer
func
()
{
if
err
:=
targetDB
.
Close
();
err
!=
nil
{
if
err
:=
targetDB
.
Close
();
err
!=
nil
{
log
.
Printf
(
"failed to close postgres connection: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"setup"
,
"failed to close postgres connection: %v"
,
err
)
}
}
}()
}()
...
@@ -217,7 +248,7 @@ func TestRedisConnection(cfg *RedisConfig) error {
...
@@ -217,7 +248,7 @@ func TestRedisConnection(cfg *RedisConfig) error {
rdb
:=
redis
.
NewClient
(
opts
)
rdb
:=
redis
.
NewClient
(
opts
)
defer
func
()
{
defer
func
()
{
if
err
:=
rdb
.
Close
();
err
!=
nil
{
if
err
:=
rdb
.
Close
();
err
!=
nil
{
log
.
Printf
(
"failed to close redis client: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"setup"
,
"failed to close redis client: %v"
,
err
)
}
}
}()
}()
...
@@ -245,7 +276,7 @@ func Install(cfg *SetupConfig) error {
...
@@ -245,7 +276,7 @@ func Install(cfg *SetupConfig) error {
return
fmt
.
Errorf
(
"failed to generate jwt secret: %w"
,
err
)
return
fmt
.
Errorf
(
"failed to generate jwt secret: %w"
,
err
)
}
}
cfg
.
JWT
.
Secret
=
secret
cfg
.
JWT
.
Secret
=
secret
log
.
Println
(
"Warning: JWT secret auto-generated. Consider setting a fixed secret for production."
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Warning: JWT secret auto-generated. Consider setting a fixed secret for production."
)
}
}
// Test connections
// Test connections
...
@@ -262,8 +293,8 @@ func Install(cfg *SetupConfig) error {
...
@@ -262,8 +293,8 @@ func Install(cfg *SetupConfig) error {
return
fmt
.
Errorf
(
"database initialization failed: %w"
,
err
)
return
fmt
.
Errorf
(
"database initialization failed: %w"
,
err
)
}
}
// Create admin user
// Create admin user
(only when database is empty and no admin exists).
if
err
:=
createAdminUser
(
cfg
);
err
!=
nil
{
if
_
,
_
,
err
:=
createAdminUser
(
cfg
);
err
!=
nil
{
return
fmt
.
Errorf
(
"admin user creation failed: %w"
,
err
)
return
fmt
.
Errorf
(
"admin user creation failed: %w"
,
err
)
}
}
...
@@ -300,7 +331,7 @@ func initializeDatabase(cfg *SetupConfig) error {
...
@@ -300,7 +331,7 @@ func initializeDatabase(cfg *SetupConfig) error {
defer
func
()
{
defer
func
()
{
if
err
:=
db
.
Close
();
err
!=
nil
{
if
err
:=
db
.
Close
();
err
!=
nil
{
log
.
Printf
(
"failed to close postgres connection: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"setup"
,
"failed to close postgres connection: %v"
,
err
)
}
}
}()
}()
...
@@ -309,7 +340,7 @@ func initializeDatabase(cfg *SetupConfig) error {
...
@@ -309,7 +340,7 @@ func initializeDatabase(cfg *SetupConfig) error {
return
repository
.
ApplyMigrations
(
migrationCtx
,
db
)
return
repository
.
ApplyMigrations
(
migrationCtx
,
db
)
}
}
func
createAdminUser
(
cfg
*
SetupConfig
)
error
{
func
createAdminUser
(
cfg
*
SetupConfig
)
(
bool
,
string
,
error
)
{
dsn
:=
fmt
.
Sprintf
(
dsn
:=
fmt
.
Sprintf
(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s"
,
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s"
,
cfg
.
Database
.
Host
,
cfg
.
Database
.
Port
,
cfg
.
Database
.
User
,
cfg
.
Database
.
Host
,
cfg
.
Database
.
Port
,
cfg
.
Database
.
User
,
...
@@ -318,12 +349,12 @@ func createAdminUser(cfg *SetupConfig) error {
...
@@ -318,12 +349,12 @@ func createAdminUser(cfg *SetupConfig) error {
db
,
err
:=
sql
.
Open
(
"postgres"
,
dsn
)
db
,
err
:=
sql
.
Open
(
"postgres"
,
dsn
)
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
false
,
""
,
err
}
}
defer
func
()
{
defer
func
()
{
if
err
:=
db
.
Close
();
err
!=
nil
{
if
err
:=
db
.
Close
();
err
!=
nil
{
log
.
Printf
(
"failed to close postgres connection: %v"
,
err
)
log
ger
.
LegacyPrintf
(
"setup"
,
"failed to close postgres connection: %v"
,
err
)
}
}
}()
}()
...
@@ -331,13 +362,27 @@ func createAdminUser(cfg *SetupConfig) error {
...
@@ -331,13 +362,27 @@ func createAdminUser(cfg *SetupConfig) error {
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
defer
cancel
()
defer
cancel
()
// Check if admin already exists
var
totalUsers
int64
var
count
int64
if
err
:=
db
.
QueryRowContext
(
ctx
,
"SELECT COUNT(1) FROM users"
)
.
Scan
(
&
totalUsers
);
err
!=
nil
{
if
err
:=
db
.
QueryRowContext
(
ctx
,
"SELECT COUNT(1) FROM users WHERE role = $1"
,
service
.
RoleAdmin
)
.
Scan
(
&
count
);
err
!=
nil
{
return
false
,
""
,
err
return
err
}
var
adminUsers
int64
if
err
:=
db
.
QueryRowContext
(
ctx
,
"SELECT COUNT(1) FROM users WHERE role = $1"
,
service
.
RoleAdmin
)
.
Scan
(
&
adminUsers
);
err
!=
nil
{
return
false
,
""
,
err
}
}
if
count
>
0
{
decision
:=
decideAdminBootstrap
(
totalUsers
,
adminUsers
)
return
nil
// Admin already exists
if
!
decision
.
shouldCreate
{
return
false
,
decision
.
reason
,
nil
}
if
strings
.
TrimSpace
(
cfg
.
Admin
.
Password
)
==
""
{
password
,
genErr
:=
generateSecret
(
16
)
if
genErr
!=
nil
{
return
false
,
""
,
fmt
.
Errorf
(
"failed to generate admin password: %w"
,
genErr
)
}
cfg
.
Admin
.
Password
=
password
fmt
.
Printf
(
"Generated admin password (one-time): %s
\n
"
,
cfg
.
Admin
.
Password
)
fmt
.
Println
(
"IMPORTANT: Save this password! It will not be shown again."
)
}
}
admin
:=
&
service
.
User
{
admin
:=
&
service
.
User
{
...
@@ -351,7 +396,7 @@ func createAdminUser(cfg *SetupConfig) error {
...
@@ -351,7 +396,7 @@ func createAdminUser(cfg *SetupConfig) error {
}
}
if
err
:=
admin
.
SetPassword
(
cfg
.
Admin
.
Password
);
err
!=
nil
{
if
err
:=
admin
.
SetPassword
(
cfg
.
Admin
.
Password
);
err
!=
nil
{
return
err
return
false
,
""
,
err
}
}
_
,
err
=
db
.
ExecContext
(
_
,
err
=
db
.
ExecContext
(
...
@@ -367,7 +412,10 @@ func createAdminUser(cfg *SetupConfig) error {
...
@@ -367,7 +412,10 @@ func createAdminUser(cfg *SetupConfig) error {
admin
.
CreatedAt
,
admin
.
CreatedAt
,
admin
.
UpdatedAt
,
admin
.
UpdatedAt
,
)
)
return
err
if
err
!=
nil
{
return
false
,
""
,
err
}
return
true
,
decision
.
reason
,
nil
}
}
func
writeConfigFile
(
cfg
*
SetupConfig
)
error
{
func
writeConfigFile
(
cfg
*
SetupConfig
)
error
{
...
@@ -476,8 +524,8 @@ func getEnvIntOrDefault(key string, defaultValue int) int {
...
@@ -476,8 +524,8 @@ func getEnvIntOrDefault(key string, defaultValue int) int {
// AutoSetupFromEnv performs automatic setup using environment variables
// AutoSetupFromEnv performs automatic setup using environment variables
// This is designed for Docker deployment where all config is passed via env vars
// This is designed for Docker deployment where all config is passed via env vars
func
AutoSetupFromEnv
()
error
{
func
AutoSetupFromEnv
()
error
{
log
.
Println
(
"Auto setup enabled, configuring from environment variables..."
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Auto setup enabled, configuring from environment variables..."
)
log
.
Printf
(
"Data directory: %s"
,
GetDataDir
())
log
ger
.
LegacyPrintf
(
"setup"
,
"Data directory: %s"
,
GetDataDir
())
// Get timezone from TZ or TIMEZONE env var (TZ is standard for Docker)
// Get timezone from TZ or TIMEZONE env var (TZ is standard for Docker)
tz
:=
getEnvOrDefault
(
"TZ"
,
""
)
tz
:=
getEnvOrDefault
(
"TZ"
,
""
)
...
@@ -525,61 +573,62 @@ func AutoSetupFromEnv() error {
...
@@ -525,61 +573,62 @@ func AutoSetupFromEnv() error {
return
fmt
.
Errorf
(
"failed to generate jwt secret: %w"
,
err
)
return
fmt
.
Errorf
(
"failed to generate jwt secret: %w"
,
err
)
}
}
cfg
.
JWT
.
Secret
=
secret
cfg
.
JWT
.
Secret
=
secret
log
.
Println
(
"Warning: JWT secret auto-generated. Consider setting a fixed secret for production."
)
logger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Warning: JWT secret auto-generated. Consider setting a fixed secret for production."
)
}
// Generate admin password if not provided
if
cfg
.
Admin
.
Password
==
""
{
password
,
err
:=
generateSecret
(
16
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to generate admin password: %w"
,
err
)
}
cfg
.
Admin
.
Password
=
password
fmt
.
Printf
(
"Generated admin password (one-time): %s
\n
"
,
cfg
.
Admin
.
Password
)
fmt
.
Println
(
"IMPORTANT: Save this password! It will not be shown again."
)
}
}
// Test database connection
// Test database connection
log
.
Println
(
"Testing database connection..."
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Testing database connection..."
)
if
err
:=
TestDatabaseConnection
(
&
cfg
.
Database
);
err
!=
nil
{
if
err
:=
TestDatabaseConnection
(
&
cfg
.
Database
);
err
!=
nil
{
return
fmt
.
Errorf
(
"database connection failed: %w"
,
err
)
return
fmt
.
Errorf
(
"database connection failed: %w"
,
err
)
}
}
log
.
Println
(
"Database connection successful"
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Database connection successful"
)
// Test Redis connection
// Test Redis connection
log
.
Println
(
"Testing Redis connection..."
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Testing Redis connection..."
)
if
err
:=
TestRedisConnection
(
&
cfg
.
Redis
);
err
!=
nil
{
if
err
:=
TestRedisConnection
(
&
cfg
.
Redis
);
err
!=
nil
{
return
fmt
.
Errorf
(
"redis connection failed: %w"
,
err
)
return
fmt
.
Errorf
(
"redis connection failed: %w"
,
err
)
}
}
log
.
Println
(
"Redis connection successful"
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Redis connection successful"
)
// Initialize database
// Initialize database
log
.
Println
(
"Initializing database..."
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Initializing database..."
)
if
err
:=
initializeDatabase
(
cfg
);
err
!=
nil
{
if
err
:=
initializeDatabase
(
cfg
);
err
!=
nil
{
return
fmt
.
Errorf
(
"database initialization failed: %w"
,
err
)
return
fmt
.
Errorf
(
"database initialization failed: %w"
,
err
)
}
}
log
.
Println
(
"Database initialized successfully"
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Database initialized successfully"
)
// Create admin user
// Create admin user
log
.
Println
(
"Creating admin user..."
)
logger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Creating admin user..."
)
if
err
:=
createAdminUser
(
cfg
);
err
!=
nil
{
created
,
reason
,
err
:=
createAdminUser
(
cfg
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"admin user creation failed: %w"
,
err
)
return
fmt
.
Errorf
(
"admin user creation failed: %w"
,
err
)
}
}
log
.
Printf
(
"Admin user created: %s"
,
cfg
.
Admin
.
Email
)
if
created
{
logger
.
LegacyPrintf
(
"setup"
,
"Admin user created: %s"
,
cfg
.
Admin
.
Email
)
}
else
{
switch
reason
{
case
adminBootstrapReasonAdminExists
:
logger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Admin user already exists, skipping admin bootstrap"
)
case
adminBootstrapReasonUsersExistWithoutAdmin
:
logger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Database already has user data; skipping auto admin bootstrap to avoid password overwrite"
)
default
:
logger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Admin bootstrap skipped"
)
}
}
// Write config file
// Write config file
log
.
Println
(
"Writing configuration file..."
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Writing configuration file..."
)
if
err
:=
writeConfigFile
(
cfg
);
err
!=
nil
{
if
err
:=
writeConfigFile
(
cfg
);
err
!=
nil
{
return
fmt
.
Errorf
(
"config file creation failed: %w"
,
err
)
return
fmt
.
Errorf
(
"config file creation failed: %w"
,
err
)
}
}
log
.
Println
(
"Configuration file created"
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Configuration file created"
)
// Create installation lock file
// Create installation lock file
if
err
:=
createInstallLock
();
err
!=
nil
{
if
err
:=
createInstallLock
();
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to create install lock: %w"
,
err
)
return
fmt
.
Errorf
(
"failed to create install lock: %w"
,
err
)
}
}
log
.
Println
(
"Installation lock created"
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Installation lock created"
)
log
.
Println
(
"Auto setup completed successfully!"
)
log
ger
.
LegacyPrintf
(
"setup"
,
"%s"
,
"Auto setup completed successfully!"
)
return
nil
return
nil
}
}
backend/internal/setup/setup_test.go
0 → 100644
View file @
64236361
package
setup
import
"testing"
func
TestDecideAdminBootstrap
(
t
*
testing
.
T
)
{
t
.
Parallel
()
tests
:=
[]
struct
{
name
string
totalUsers
int64
adminUsers
int64
should
bool
reason
string
}{
{
name
:
"empty database should create admin"
,
totalUsers
:
0
,
adminUsers
:
0
,
should
:
true
,
reason
:
adminBootstrapReasonEmptyDatabase
,
},
{
name
:
"admin exists should skip"
,
totalUsers
:
10
,
adminUsers
:
1
,
should
:
false
,
reason
:
adminBootstrapReasonAdminExists
,
},
{
name
:
"users exist without admin should skip"
,
totalUsers
:
5
,
adminUsers
:
0
,
should
:
false
,
reason
:
adminBootstrapReasonUsersExistWithoutAdmin
,
},
}
for
_
,
tc
:=
range
tests
{
tc
:=
tc
t
.
Run
(
tc
.
name
,
func
(
t
*
testing
.
T
)
{
t
.
Parallel
()
got
:=
decideAdminBootstrap
(
tc
.
totalUsers
,
tc
.
adminUsers
)
if
got
.
shouldCreate
!=
tc
.
should
{
t
.
Fatalf
(
"shouldCreate=%v, want %v"
,
got
.
shouldCreate
,
tc
.
should
)
}
if
got
.
reason
!=
tc
.
reason
{
t
.
Fatalf
(
"reason=%q, want %q"
,
got
.
reason
,
tc
.
reason
)
}
})
}
}
Prev
1
2
3
4
5
Next
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