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
ab5839b4
Commit
ab5839b4
authored
Jan 11, 2026
by
yangjianbo
Browse files
fix(仪表盘): 修复缓存稳定性并补充测试
parent
c2c865b0
Changes
4
Hide whitespace changes
Inline
Side-by-side
backend/internal/repository/dashboard_cache.go
View file @
ab5839b4
...
@@ -22,6 +22,9 @@ func NewDashboardCache(rdb *redis.Client, cfg *config.Config) service.DashboardS
...
@@ -22,6 +22,9 @@ func NewDashboardCache(rdb *redis.Client, cfg *config.Config) service.DashboardS
if
cfg
!=
nil
{
if
cfg
!=
nil
{
prefix
=
strings
.
TrimSpace
(
cfg
.
Dashboard
.
KeyPrefix
)
prefix
=
strings
.
TrimSpace
(
cfg
.
Dashboard
.
KeyPrefix
)
}
}
if
prefix
!=
""
&&
!
strings
.
HasSuffix
(
prefix
,
":"
)
{
prefix
+=
":"
}
return
&
dashboardCache
{
return
&
dashboardCache
{
rdb
:
rdb
,
rdb
:
rdb
,
keyPrefix
:
prefix
,
keyPrefix
:
prefix
,
...
@@ -49,3 +52,7 @@ func (c *dashboardCache) buildKey() string {
...
@@ -49,3 +52,7 @@ func (c *dashboardCache) buildKey() string {
}
}
return
c
.
keyPrefix
+
dashboardStatsCacheKey
return
c
.
keyPrefix
+
dashboardStatsCacheKey
}
}
func
(
c
*
dashboardCache
)
DeleteDashboardStats
(
ctx
context
.
Context
)
error
{
return
c
.
rdb
.
Del
(
ctx
,
c
.
buildKey
())
.
Err
()
}
backend/internal/repository/dashboard_cache_test.go
0 → 100644
View file @
ab5839b4
package
repository
import
(
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
func
TestNewDashboardCacheKeyPrefix
(
t
*
testing
.
T
)
{
cache
:=
NewDashboardCache
(
nil
,
&
config
.
Config
{
Dashboard
:
config
.
DashboardCacheConfig
{
KeyPrefix
:
"prod"
,
},
})
impl
,
ok
:=
cache
.
(
*
dashboardCache
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"prod:"
,
impl
.
keyPrefix
)
cache
=
NewDashboardCache
(
nil
,
&
config
.
Config
{
Dashboard
:
config
.
DashboardCacheConfig
{
KeyPrefix
:
"staging:"
,
},
})
impl
,
ok
=
cache
.
(
*
dashboardCache
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"staging:"
,
impl
.
keyPrefix
)
}
backend/internal/service/dashboard_service.go
View file @
ab5839b4
...
@@ -26,6 +26,7 @@ var ErrDashboardStatsCacheMiss = errors.New("仪表盘缓存未命中")
...
@@ -26,6 +26,7 @@ var ErrDashboardStatsCacheMiss = errors.New("仪表盘缓存未命中")
type
DashboardStatsCache
interface
{
type
DashboardStatsCache
interface
{
GetDashboardStats
(
ctx
context
.
Context
)
(
string
,
error
)
GetDashboardStats
(
ctx
context
.
Context
)
(
string
,
error
)
SetDashboardStats
(
ctx
context
.
Context
,
data
string
,
ttl
time
.
Duration
)
error
SetDashboardStats
(
ctx
context
.
Context
,
data
string
,
ttl
time
.
Duration
)
error
DeleteDashboardStats
(
ctx
context
.
Context
)
error
}
}
type
dashboardStatsCacheEntry
struct
{
type
dashboardStatsCacheEntry
struct
{
...
@@ -115,10 +116,12 @@ func (s *DashboardService) getCachedDashboardStats(ctx context.Context) (*usages
...
@@ -115,10 +116,12 @@ func (s *DashboardService) getCachedDashboardStats(ctx context.Context) (*usages
var
entry
dashboardStatsCacheEntry
var
entry
dashboardStatsCacheEntry
if
err
:=
json
.
Unmarshal
([]
byte
(
data
),
&
entry
);
err
!=
nil
{
if
err
:=
json
.
Unmarshal
([]
byte
(
data
),
&
entry
);
err
!=
nil
{
return
nil
,
false
,
err
s
.
evictDashboardStatsCache
(
err
)
return
nil
,
false
,
ErrDashboardStatsCacheMiss
}
}
if
entry
.
Stats
==
nil
{
if
entry
.
Stats
==
nil
{
return
nil
,
false
,
errors
.
New
(
"仪表盘缓存缺少统计数据"
)
s
.
evictDashboardStatsCache
(
errors
.
New
(
"仪表盘缓存缺少统计数据"
))
return
nil
,
false
,
ErrDashboardStatsCacheMiss
}
}
age
:=
time
.
Since
(
time
.
Unix
(
entry
.
UpdatedAt
,
0
))
age
:=
time
.
Since
(
time
.
Unix
(
entry
.
UpdatedAt
,
0
))
...
@@ -130,7 +133,9 @@ func (s *DashboardService) refreshDashboardStats(ctx context.Context) (*usagesta
...
@@ -130,7 +133,9 @@ func (s *DashboardService) refreshDashboardStats(ctx context.Context) (*usagesta
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
s
.
saveDashboardStatsCache
(
ctx
,
stats
)
cacheCtx
,
cancel
:=
s
.
cacheOperationContext
()
defer
cancel
()
s
.
saveDashboardStatsCache
(
cacheCtx
,
stats
)
return
stats
,
nil
return
stats
,
nil
}
}
...
@@ -153,7 +158,9 @@ func (s *DashboardService) refreshDashboardStatsAsync() {
...
@@ -153,7 +158,9 @@ func (s *DashboardService) refreshDashboardStatsAsync() {
log
.
Printf
(
"[Dashboard] 仪表盘缓存异步刷新失败: %v"
,
err
)
log
.
Printf
(
"[Dashboard] 仪表盘缓存异步刷新失败: %v"
,
err
)
return
return
}
}
s
.
saveDashboardStatsCache
(
ctx
,
stats
)
cacheCtx
,
cancel
:=
s
.
cacheOperationContext
()
defer
cancel
()
s
.
saveDashboardStatsCache
(
cacheCtx
,
stats
)
}()
}()
}
}
...
@@ -177,6 +184,25 @@ func (s *DashboardService) saveDashboardStatsCache(ctx context.Context, stats *u
...
@@ -177,6 +184,25 @@ func (s *DashboardService) saveDashboardStatsCache(ctx context.Context, stats *u
}
}
}
}
func
(
s
*
DashboardService
)
evictDashboardStatsCache
(
reason
error
)
{
if
s
.
cache
==
nil
{
return
}
cacheCtx
,
cancel
:=
s
.
cacheOperationContext
()
defer
cancel
()
if
err
:=
s
.
cache
.
DeleteDashboardStats
(
cacheCtx
);
err
!=
nil
{
log
.
Printf
(
"[Dashboard] 仪表盘缓存清理失败: %v"
,
err
)
}
if
reason
!=
nil
{
log
.
Printf
(
"[Dashboard] 仪表盘缓存异常,已清理: %v"
,
reason
)
}
}
func
(
s
*
DashboardService
)
cacheOperationContext
()
(
context
.
Context
,
context
.
CancelFunc
)
{
return
context
.
WithTimeout
(
context
.
Background
(),
s
.
refreshTimeout
)
}
func
(
s
*
DashboardService
)
GetAPIKeyUsageTrend
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
limit
int
)
([]
usagestats
.
APIKeyUsageTrendPoint
,
error
)
{
func
(
s
*
DashboardService
)
GetAPIKeyUsageTrend
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
limit
int
)
([]
usagestats
.
APIKeyUsageTrendPoint
,
error
)
{
trend
,
err
:=
s
.
usageRepo
.
GetAPIKeyUsageTrend
(
ctx
,
startTime
,
endTime
,
granularity
,
limit
)
trend
,
err
:=
s
.
usageRepo
.
GetAPIKeyUsageTrend
(
ctx
,
startTime
,
endTime
,
granularity
,
limit
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
backend/internal/service/dashboard_service_test.go
View file @
ab5839b4
...
@@ -3,6 +3,7 @@ package service
...
@@ -3,6 +3,7 @@ package service
import
(
import
(
"context"
"context"
"encoding/json"
"encoding/json"
"errors"
"sync"
"sync"
"sync/atomic"
"sync/atomic"
"testing"
"testing"
...
@@ -38,8 +39,10 @@ func (s *usageRepoStub) GetDashboardStats(ctx context.Context) (*usagestats.Dash
...
@@ -38,8 +39,10 @@ func (s *usageRepoStub) GetDashboardStats(ctx context.Context) (*usagestats.Dash
type
dashboardCacheStub
struct
{
type
dashboardCacheStub
struct
{
get
func
(
ctx
context
.
Context
)
(
string
,
error
)
get
func
(
ctx
context
.
Context
)
(
string
,
error
)
set
func
(
ctx
context
.
Context
,
data
string
,
ttl
time
.
Duration
)
error
set
func
(
ctx
context
.
Context
,
data
string
,
ttl
time
.
Duration
)
error
del
func
(
ctx
context
.
Context
)
error
getCalls
int32
getCalls
int32
setCalls
int32
setCalls
int32
delCalls
int32
lastSetMu
sync
.
Mutex
lastSetMu
sync
.
Mutex
lastSet
string
lastSet
string
}
}
...
@@ -63,6 +66,14 @@ func (c *dashboardCacheStub) SetDashboardStats(ctx context.Context, data string,
...
@@ -63,6 +66,14 @@ func (c *dashboardCacheStub) SetDashboardStats(ctx context.Context, data string,
return
nil
return
nil
}
}
func
(
c
*
dashboardCacheStub
)
DeleteDashboardStats
(
ctx
context
.
Context
)
error
{
atomic
.
AddInt32
(
&
c
.
delCalls
,
1
)
if
c
.
del
!=
nil
{
return
c
.
del
(
ctx
)
}
return
nil
}
func
(
c
*
dashboardCacheStub
)
readLastEntry
(
t
*
testing
.
T
)
dashboardStatsCacheEntry
{
func
(
c
*
dashboardCacheStub
)
readLastEntry
(
t
*
testing
.
T
)
dashboardStatsCacheEntry
{
t
.
Helper
()
t
.
Helper
()
c
.
lastSetMu
.
Lock
()
c
.
lastSetMu
.
Lock
()
...
@@ -187,3 +198,36 @@ func TestDashboardService_CacheHitStale_TriggersAsyncRefresh(t *testing.T) {
...
@@ -187,3 +198,36 @@ func TestDashboardService_CacheHitStale_TriggersAsyncRefresh(t *testing.T) {
return
atomic
.
LoadInt32
(
&
cache
.
setCalls
)
>=
1
return
atomic
.
LoadInt32
(
&
cache
.
setCalls
)
>=
1
},
1
*
time
.
Second
,
10
*
time
.
Millisecond
)
},
1
*
time
.
Second
,
10
*
time
.
Millisecond
)
}
}
func
TestDashboardService_CacheParseError_EvictsAndRefetches
(
t
*
testing
.
T
)
{
cache
:=
&
dashboardCacheStub
{
get
:
func
(
ctx
context
.
Context
)
(
string
,
error
)
{
return
"not-json"
,
nil
},
}
stats
:=
&
usagestats
.
DashboardStats
{
TotalUsers
:
9
}
repo
:=
&
usageRepoStub
{
stats
:
stats
}
cfg
:=
&
config
.
Config
{
Dashboard
:
config
.
DashboardCacheConfig
{
Enabled
:
true
}}
svc
:=
NewDashboardService
(
repo
,
cache
,
cfg
)
got
,
err
:=
svc
.
GetDashboardStats
(
context
.
Background
())
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
stats
,
got
)
require
.
Equal
(
t
,
int32
(
1
),
atomic
.
LoadInt32
(
&
cache
.
delCalls
))
require
.
Equal
(
t
,
int32
(
1
),
atomic
.
LoadInt32
(
&
repo
.
calls
))
}
func
TestDashboardService_CacheParseError_RepoFailure
(
t
*
testing
.
T
)
{
cache
:=
&
dashboardCacheStub
{
get
:
func
(
ctx
context
.
Context
)
(
string
,
error
)
{
return
"not-json"
,
nil
},
}
repo
:=
&
usageRepoStub
{
err
:
errors
.
New
(
"db down"
)}
cfg
:=
&
config
.
Config
{
Dashboard
:
config
.
DashboardCacheConfig
{
Enabled
:
true
}}
svc
:=
NewDashboardService
(
repo
,
cache
,
cfg
)
_
,
err
:=
svc
.
GetDashboardStats
(
context
.
Background
())
require
.
Error
(
t
,
err
)
require
.
Equal
(
t
,
int32
(
1
),
atomic
.
LoadInt32
(
&
cache
.
delCalls
))
}
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