package service import ( "context" "errors" "fmt" "log" "strings" "time" "affiliate_dash/internal/model" ) // RockLoginTestResult 是「测试登录 / 刷新 Token」返回给管理端的结果,不含 token。 type RockLoginTestResult struct { 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 { if cfg == nil { return false } return strings.TrimSpace(cfg.LoginUsername) != "" && strings.TrimSpace(cfg.LoginPasswordCiphertext) != "" } func (s *RockService) decryptLoginPassword(cfg *model.RockConfig) (string, error) { if cfg == nil { return "", errors.New("Rock 配置不存在") } if strings.TrimSpace(cfg.LoginPasswordCiphertext) == "" { return "", errors.New("尚未配置上游登录密码") } if s.codec == nil { return "", errors.New("未配置数据加密密钥,无法解密登录密码") } password, err := s.codec.Decrypt(cfg.LoginPasswordCiphertext) if err != nil { return "", fmt.Errorf("解密登录密码失败: %w", err) } if strings.TrimSpace(password) == "" { return "", errors.New("登录密码为空,请重新配置") } return password, nil } // EnsureTokenValid 检查当前 Bearer Token;失效时用已保存的账号密码自动重新登录。 // 未配置 token 且未配置账号密码时静默返回,避免全新环境每 5 分钟刷错误。 func (s *RockService) EnsureTokenValid(ctx context.Context) error { cfg, err := s.GetConfig(ctx) if err != nil { return err } token := strings.TrimSpace(cfg.BearerToken) if token == "" { if !s.hasLoginCredentials(cfg) { return nil } return s.loginAndSaveToken(ctx, cfg) } me, err := s.client.GetMe(ctx, token) if err == nil { s.markTokenHealthy(ctx, cfg.ID, me) return nil } if !errors.Is(err, ErrRockUnauthorized) { return err } if !s.hasLoginCredentials(cfg) { message := err.Error() + ";请配置上游登录账号密码或手动更新 Bearer Token" s.recordTokenError(ctx, cfg.ID, message) return errors.New(message) } return s.loginAndSaveToken(ctx, cfg) } // loginAndSaveToken 登录上游并把新 token 落库;并发调用时只允许一次真实登录。 func (s *RockService) loginAndSaveToken(ctx context.Context, cfg *model.RockConfig) error { s.tokenMu.Lock() defer s.tokenMu.Unlock() // 双重检查:拿到锁时可能已经有别的 goroutine 刷新过了。 latest, err := s.GetConfig(ctx) if err != nil { return err } if latest.LastTokenRefreshAt != nil && time.Since(*latest.LastTokenRefreshAt) < 30*time.Second { return nil } _, _, err = s.refreshTokenWithLogin(ctx, latest) return err } // 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 { return nil, err } 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, cfg.ID, message) return nil, nil, errors.New(message) } now := time.Now() updates := map[string]interface{}{ "bearer_token": resp.Token, "last_token_refresh_at": now, "last_token_error": "", "updated_at": now, } if username := resp.ResolveUsername(); username != "" { updates["username"] = username } if err := s.db.WithContext(ctx).Model(&model.RockConfig{}). Where("id = ?", cfg.ID). Updates(updates).Error; err != nil { return nil, nil, err } // 新 token 立即拉一次用户信息,顺带刷新积分与心跳。 var me *RockMeResponse if latest, meErr := s.client.GetMe(ctx, resp.Token); meErr == nil && latest != nil { me = latest s.markTokenHealthy(ctx, cfg.ID, me) } return resp, me, nil } func (s *RockService) markTokenHealthy(ctx context.Context, cfgID uint, me *RockMeResponse) { updates := map[string]interface{}{ "last_heartbeat_at": time.Now(), "last_token_error": "", } if me != nil { if username := strings.TrimSpace(me.User.Username); username != "" { updates["username"] = username } updates["points"] = me.User.Points.Float64() } if err := s.db.WithContext(ctx).Model(&model.RockConfig{}). Where("id = ?", cfgID). Updates(updates).Error; err != nil { log.Printf("[RockService] 写入 token 健康状态失败: %v", err) } } func (s *RockService) recordTokenError(ctx context.Context, cfgID uint, message string) { message = truncatePurchaseError(message) log.Printf("[RockService][WARN] Token 维护失败: %s", message) if err := s.db.WithContext(ctx).Model(&model.RockConfig{}). Where("id = ?", cfgID). Updates(map[string]interface{}{ "last_token_error": message, "updated_at": time.Now(), }).Error; err != nil { log.Printf("[RockService] 写入 token 错误失败: %v", err) } } // TestLogin 用当前(或传入的)账号密码试登录,不保存 token,供配置页验证。 func (s *RockService) TestLogin(ctx context.Context, username, password string) (*RockLoginTestResult, error) { cfg, err := s.GetConfig(ctx) if err != nil { return nil, err } username = strings.TrimSpace(username) if username == "" { username = cfg.LoginUsername } if username == "" { return nil, errors.New("请先填写上游登录账号") } if password == "" { password, err = s.decryptLoginPassword(cfg) if err != nil { return nil, errors.New("请先填写登录密码(或先在配置中保存密码)") } } resp, err := s.client.Login(ctx, username, password) if err != nil { return nil, err } result := &RockLoginTestResult{ Username: resp.ResolveUsername(), Role: resp.ResolveRole(), } if me, meErr := s.client.GetMe(ctx, resp.Token); meErr == nil && me != nil { result.Points = me.User.Points.Float64() if result.Username == "" { result.Username = me.User.Username } } return result, nil }