feat:rock增加手动刷新Token按钮
This commit is contained in:
@@ -85,6 +85,16 @@ func (h *RockHandler) TestLogin(c *gin.Context) {
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
// RefreshToken 强制登录上游并保存新的 Bearer Token
|
||||
func (h *RockHandler) RefreshToken(c *gin.Context) {
|
||||
result, err := h.rockSvc.RefreshTokenNow(c.Request.Context())
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
// GetStats 获取概览数据与库存统计
|
||||
func (h *RockHandler) GetStats(c *gin.Context) {
|
||||
stats, err := h.rockSvc.GetStats(c.Request.Context())
|
||||
|
||||
@@ -287,6 +287,7 @@ func Setup(h *Handlers) *gin.Engine {
|
||||
platform.PUT("/platform/rock/config", h.Rock.UpdateConfig)
|
||||
platform.POST("/platform/rock/test-conn", h.Rock.TestConnection)
|
||||
platform.POST("/platform/rock/login-test", h.Rock.TestLogin)
|
||||
platform.POST("/platform/rock/token/refresh", h.Rock.RefreshToken)
|
||||
platform.GET("/platform/rock/stats", h.Rock.GetStats)
|
||||
platform.POST("/platform/rock/dashboard/refresh", h.Rock.RefreshDashboard)
|
||||
platform.POST("/platform/rock/purchase", h.Rock.PurchaseTokens)
|
||||
|
||||
@@ -262,3 +262,67 @@ func TestEnsureTokenValid_RefreshesWhenBearerEmpty(t *testing.T) {
|
||||
t.Fatalf("token 未写入: %q", after.BearerToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshTokenNow_SavesNewToken(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
codec, _ := NewSecretCodec("test-master-key")
|
||||
var loginCalls int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/api/auth/login":
|
||||
atomic.AddInt32(&loginCalls, 1)
|
||||
_, _ = w.Write([]byte(`{"code":0,"token":"manual-fresh-token","user":{"username":"tester","role":"user"}}`))
|
||||
case "/api/me":
|
||||
if r.Header.Get("Authorization") != "Bearer manual-fresh-token" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"user":{"username":"tester","points":66.5}}`))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewRockService(db, NewRockClient(server.URL), codec)
|
||||
ctx := context.Background()
|
||||
cfg, _ := svc.GetConfig(ctx)
|
||||
ciphertext, _ := codec.Encrypt("secret-pass")
|
||||
_ = db.Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Updates(map[string]interface{}{
|
||||
"bearer_token": "still-valid-token",
|
||||
"login_username": "tester",
|
||||
"login_password_ciphertext": ciphertext,
|
||||
"last_token_refresh_at": nil,
|
||||
}).Error
|
||||
|
||||
result, err := svc.RefreshTokenNow(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("手动刷新 Token 失败: %v", err)
|
||||
}
|
||||
if result.Username != "tester" || result.Role != "user" || result.Points != 66.5 {
|
||||
t.Fatalf("返回信息不符: %+v", result)
|
||||
}
|
||||
if result.RefreshedAt == "" {
|
||||
t.Fatal("refreshed_at 未返回")
|
||||
}
|
||||
if calls := atomic.LoadInt32(&loginCalls); calls != 1 {
|
||||
t.Fatalf("应强制登录一次,实际 %d", calls)
|
||||
}
|
||||
after, _ := svc.GetConfig(ctx)
|
||||
if after.BearerToken != "manual-fresh-token" {
|
||||
t.Fatalf("token 未更新: %q", after.BearerToken)
|
||||
}
|
||||
if after.LastTokenRefreshAt == nil {
|
||||
t.Fatal("last_token_refresh_at 未更新")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshTokenNow_RequiresCredentials(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
svc := NewRockService(db, NewRockClient("https://example.com"))
|
||||
if _, err := svc.RefreshTokenNow(context.Background()); err == nil ||
|
||||
!strings.Contains(err.Error(), "登录账号") {
|
||||
t.Fatalf("未配置账号密码时应提示,实际 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import (
|
||||
"affiliate_dash/internal/model"
|
||||
)
|
||||
|
||||
// RockLoginTestResult 是「测试登录」返回给管理端的结果,不含 token。
|
||||
// RockLoginTestResult 是「测试登录 / 刷新 Token」返回给管理端的结果,不含 token。
|
||||
type RockLoginTestResult struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
Points float64 `json:"points"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
Points float64 `json:"points"`
|
||||
RefreshedAt string `json:"refreshed_at,omitempty"`
|
||||
}
|
||||
|
||||
func (s *RockService) hasLoginCredentials(cfg *model.RockConfig) bool {
|
||||
@@ -90,17 +91,54 @@ func (s *RockService) loginAndSaveToken(ctx context.Context, cfg *model.RockConf
|
||||
if latest.LastTokenRefreshAt != nil && time.Since(*latest.LastTokenRefreshAt) < 30*time.Second {
|
||||
return nil
|
||||
}
|
||||
_, _, err = s.refreshTokenWithLogin(ctx, latest)
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := s.decryptLoginPassword(latest)
|
||||
// RefreshTokenNow 手动强制登录刷新 Token,不检查当前 token 是否有效。
|
||||
func (s *RockService) RefreshTokenNow(ctx context.Context) (*RockLoginTestResult, error) {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
|
||||
cfg, err := s.GetConfig(ctx)
|
||||
if err != nil {
|
||||
s.recordTokenError(ctx, latest.ID, err.Error())
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
resp, err := s.client.Login(ctx, latest.LoginUsername, password)
|
||||
if !s.hasLoginCredentials(cfg) {
|
||||
return nil, errors.New("请先在配置中保存上游登录账号与密码")
|
||||
}
|
||||
|
||||
resp, me, err := s.refreshTokenWithLogin(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &RockLoginTestResult{
|
||||
Username: resp.ResolveUsername(),
|
||||
Role: resp.ResolveRole(),
|
||||
RefreshedAt: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
if me != nil {
|
||||
result.Points = me.User.Points.Float64()
|
||||
if result.Username == "" {
|
||||
result.Username = me.User.Username
|
||||
}
|
||||
}
|
||||
log.Printf("[RockService] 手动刷新 Token 成功: user=%s", result.Username)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// refreshTokenWithLogin 登录上游、保存新 token 并刷新用户快照;调用方需持有 tokenMu。
|
||||
func (s *RockService) refreshTokenWithLogin(ctx context.Context, cfg *model.RockConfig) (*RockLoginResponse, *RockMeResponse, error) {
|
||||
password, err := s.decryptLoginPassword(cfg)
|
||||
if err != nil {
|
||||
s.recordTokenError(ctx, cfg.ID, err.Error())
|
||||
return nil, nil, err
|
||||
}
|
||||
resp, err := s.client.Login(ctx, cfg.LoginUsername, password)
|
||||
if err != nil {
|
||||
message := "上游登录失败: " + err.Error()
|
||||
s.recordTokenError(ctx, latest.ID, message)
|
||||
return errors.New(message)
|
||||
s.recordTokenError(ctx, cfg.ID, message)
|
||||
return nil, nil, errors.New(message)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@@ -114,17 +152,18 @@ func (s *RockService) loginAndSaveToken(ctx context.Context, cfg *model.RockConf
|
||||
updates["username"] = username
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Model(&model.RockConfig{}).
|
||||
Where("id = ?", latest.ID).
|
||||
Where("id = ?", cfg.ID).
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 新 token 立即拉一次用户信息,顺带刷新积分与心跳。
|
||||
if me, meErr := s.client.GetMe(ctx, resp.Token); meErr == nil && me != nil {
|
||||
s.markTokenHealthy(ctx, latest.ID, me)
|
||||
var me *RockMeResponse
|
||||
if latest, meErr := s.client.GetMe(ctx, resp.Token); meErr == nil && latest != nil {
|
||||
me = latest
|
||||
s.markTokenHealthy(ctx, cfg.ID, me)
|
||||
}
|
||||
log.Printf("[RockService] 自动登录刷新 Token 成功: user=%s", resp.ResolveUsername())
|
||||
return nil
|
||||
return resp, me, nil
|
||||
}
|
||||
|
||||
func (s *RockService) markTokenHealthy(ctx context.Context, cfgID uint, me *RockMeResponse) {
|
||||
|
||||
Reference in New Issue
Block a user