Files
affiliate_dash/backend/internal/service/rock_token_auth.go
T

197 lines
5.7 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"log"
"strings"
"time"
"affiliate_dash/internal/model"
)
// RockLoginTestResult 是「测试登录」返回给管理端的结果,不含 token。
type RockLoginTestResult struct {
Username string `json:"username"`
Role string `json:"role"`
Points float64 `json:"points"`
}
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
}
password, err := s.decryptLoginPassword(latest)
if err != nil {
s.recordTokenError(ctx, latest.ID, err.Error())
return err
}
resp, err := s.client.Login(ctx, latest.LoginUsername, password)
if err != nil {
message := "上游登录失败: " + err.Error()
s.recordTokenError(ctx, latest.ID, message)
return 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 = ?", latest.ID).
Updates(updates).Error; err != nil {
return err
}
// 新 token 立即拉一次用户信息,顺带刷新积分与心跳。
if me, meErr := s.client.GetMe(ctx, resp.Token); meErr == nil && me != nil {
s.markTokenHealthy(ctx, latest.ID, me)
}
log.Printf("[RockService] 自动登录刷新 Token 成功: user=%s", resp.ResolveUsername())
return 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
}