Compare commits

...

1 Commits

Author SHA1 Message Date
truewhile 91c6d9dfd9 处理token刷新失败bug 2026-09-14 16:47:44 +08:00
4 changed files with 132 additions and 18 deletions
+20 -10
View File
@@ -138,25 +138,35 @@ func testStrmAccountHandler(svc *service.Container) gin.HandlerFunc {
return
}
now := time.Now()
acct.LastTestAt = &now
if acct.Provider == model.StrmProviderEmbyRemote && svc.EmbyRemote != nil {
result := ""
ok := false
if err := svc.EmbyRemote.TestConnection(c.Request.Context(), acct); err != nil {
acct.LastTestResult = err.Error()
acct.LastTestOK = false
result = err.Error()
} else {
acct.LastTestResult = "ok"
acct.LastTestOK = true
result = "ok"
ok = true
}
} else {
acct = svc.Strm.TestStrmAccount(c.Request.Context(), id)
if acct == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "网盘账号不存在"})
acct.LastTestAt = &now
acct.LastTestResult = result
acct.LastTestOK = ok
if err := svc.Repo.StrmAccount.UpdateTestResult(c.Request.Context(), acct.ID, now, result, ok); err != nil {
// 写库失败时仍返回本地测试结果;不要回退到整行 Update,
// 那会覆盖 TestConnection 期间可能刷新的账号配置。
c.JSON(http.StatusOK, strmAccountViews(svc, []model.StrmAccount{*acct})[0])
return
}
if fresh, err := svc.Repo.StrmAccount.FindByID(c.Request.Context(), acct.ID); err == nil && fresh != nil {
acct = fresh
}
c.JSON(http.StatusOK, strmAccountViews(svc, []model.StrmAccount{*acct})[0])
return
}
_ = svc.Repo.StrmAccount.Update(c.Request.Context(), acct)
acct = svc.Strm.TestStrmAccount(c.Request.Context(), id)
if acct == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "网盘账号不存在"})
return
}
c.JSON(http.StatusOK, strmAccountViews(svc, []model.StrmAccount{*acct})[0])
}
}
+15
View File
@@ -57,6 +57,21 @@ func (r *StrmAccountRepository) Update(ctx context.Context, a *model.StrmAccount
})
}
// UpdateTestResult updates only connectivity-test metadata. Callers that touch
// account credentials must not use Update with a snapshot read before Ping:
// a 115 token refresh can persist new tokens while Ping is running, and writing
// the stale snapshot back would revoke the freshly rotated credentials.
func (r *StrmAccountRepository) UpdateTestResult(ctx context.Context, id string, at time.Time, result string, ok bool) error {
return withSQLiteBusyRetry(ctx, func() error {
return r.db.WithContext(ctx).Model(&model.StrmAccount{}).Where("id = ?", id).Updates(map[string]any{
"last_test_at": at,
"last_test_result": result,
"last_test_ok": ok,
"updated_at": time.Now(),
}).Error
})
}
func (r *StrmAccountRepository) Delete(ctx context.Context, id string) error {
return withSQLiteBusyRetry(ctx, func() error {
return r.db.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.StrmAccount{}).Error
+71
View File
@@ -10,6 +10,7 @@ import (
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service/cloud"
"github.com/truewhile/MeBox/internal/service/cloud115"
)
@@ -156,6 +157,76 @@ func TestPersist115TokensKeepsSharedClient(t *testing.T) {
}
}
// TestTestStrmAccountKeepsTokensRefreshedDuringPing 回归测试:
// Ping 期间 115 客户端可能刷新并持久化 token。账号测试只能写 last_test_*
// 字段,不能再用请求开始时读取的旧 Config 整包覆盖,否则新 token 会被旧值
// 覆盖,最终导致账号在下次重启后失效。
func TestTestStrmAccountKeepsTokensRefreshedDuringPing(t *testing.T) {
svc := testStrmService(t)
ctx := context.Background()
acct, err := svc.CreateStrmAccount(ctx, "115", model.StrmProvider115, map[string]string{
"app_id": "100195129",
"access_token": "at-old",
"refresh_token": "rt-old",
})
if err != nil {
t.Fatalf("create account: %v", err)
}
svc.providerMu.Lock()
svc.provider115Cache[acct.ID] = &refreshOnPing115Provider{svc: svc, accountID: acct.ID}
svc.providerMu.Unlock()
got := svc.TestStrmAccount(ctx, acct.ID)
if got == nil {
t.Fatal("TestStrmAccount returned nil")
}
if !got.LastTestOK || got.LastTestResult != "ok" {
t.Fatalf("test result = (%v, %q), want ok", got.LastTestOK, got.LastTestResult)
}
gotCfg, err := svc.strmAccountConfig(got)
if err != nil {
t.Fatalf("decode returned config: %v", err)
}
if gotCfg["access_token"] != "at-new" || gotCfg["refresh_token"] != "rt-new" {
t.Fatalf("returned account lost refreshed tokens: %#v", gotCfg)
}
fresh, err := svc.repo.StrmAccount.FindByID(ctx, acct.ID)
if err != nil || fresh == nil {
t.Fatalf("reload account: %v", err)
}
freshCfg, err := svc.strmAccountConfig(fresh)
if err != nil {
t.Fatalf("decode persisted config: %v", err)
}
if freshCfg["access_token"] != "at-new" || freshCfg["refresh_token"] != "rt-new" {
t.Fatalf("persisted account lost refreshed tokens: %#v", freshCfg)
}
}
// refreshOnPing115Provider 模拟真实 115 客户端在 Ping 内完成 token 轮转并
// 通过持久化回调写回新 token 的行为。
type refreshOnPing115Provider struct {
svc *StrmService
accountID string
}
func (p *refreshOnPing115Provider) Type() string { return model.StrmProvider115 }
func (p *refreshOnPing115Provider) Ping(context.Context) error {
p.svc.persist115Tokens(p.accountID, "at-new", "rt-new")
return nil
}
func (p *refreshOnPing115Provider) List(context.Context, string) ([]cloud.FileEntry, error) {
return nil, nil
}
func (p *refreshOnPing115Provider) Resolve(context.Context, string) (*cloud.DirectLink, error) {
return nil, nil
}
func TestDeleteStrmAccountCascadesEmbyMounts(t *testing.T) {
ctx := context.Background()
db := newServiceTestDB(t, &model.StrmAccount{}, &model.EmbyMount{}, &model.StrmSyncPath{})
+26 -8
View File
@@ -449,19 +449,37 @@ func (s *StrmService) TestStrmAccount(ctx context.Context, id string) *model.Str
return nil
}
now := time.Now()
acct.LastTestAt = &now
result := ""
ok := false
provider, err := s.providerFor(ctx, acct)
if err != nil {
acct.LastTestResult = err.Error()
acct.LastTestOK = false
result = err.Error()
} else if err := provider.Ping(ctx); err != nil {
acct.LastTestResult = err.Error()
acct.LastTestOK = false
result = err.Error()
} else {
acct.LastTestResult = "ok"
acct.LastTestOK = true
result = "ok"
ok = true
}
_ = s.repo.StrmAccount.Update(ctx, acct)
// Ping 期间 115 客户端可能刷新 access/refresh token,并通过
// OnTokenRefreshed 持久化新配置。这里只写测试结果字段,不能把请求开始时
// 读取的旧 acct.Config 整包写回,否则会把刚轮转的 token 覆盖失效。
updateErr := s.repo.StrmAccount.UpdateTestResult(ctx, id, now, result, ok)
if updateErr != nil && s.log != nil {
s.log.Warn("update strm account test result failed", zap.String("account_id", id), zap.Error(updateErr))
}
// 重新读取,确保返回给前端的账号配置已经是 Ping 期间刷新后的版本。
if fresh, err := s.repo.StrmAccount.FindByID(ctx, id); err == nil && fresh != nil {
if updateErr != nil {
// 写库失败时仍让本次响应展示刚完成测试的结果。
fresh.LastTestAt = &now
fresh.LastTestResult = result
fresh.LastTestOK = ok
}
return fresh
}
acct.LastTestAt = &now
acct.LastTestResult = result
acct.LastTestOK = ok
return acct
}