236 lines
6.9 KiB
Go
236 lines
6.9 KiB
Go
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
|
|
}
|