package sync import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "sync" "time" consts "dataengine/consts/public" "dataengine/utils" "gitea.redpowerfuture.com/red-future/common/db/gfdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/sirupsen/logrus" ) // refreshTokenMu 防止并发刷新 token(多个接口同时检测到过期时串行化) var refreshTokenMu sync.Mutex // tencentTokenResponse 腾讯广告 OAuth 刷新 token 响应 type tencentTokenResponse struct { Code int `json:"code"` Message string `json:"message"` Data *struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"access_token_expires_in"` RefreshExpiresIn int `json:"refresh_token_expires_in"` } `json:"data"` } // RefreshTencentToken 刷新腾讯广告 OAuth2 token // 由 ApiClient 在检测到 401/token 过期时自动调用 func RefreshTencentToken(ctx context.Context, platform *PlatformConfig) error { refreshTokenMu.Lock() defer refreshTokenMu.Unlock() if platform.AuthConfig == nil { return fmt.Errorf("平台 [%s] 未配置 auth_config", platform.PlatformCode) } clientID := platform.ClientId clientSecret := platform.ClientSecret refreshToken, _ := platform.AuthConfig["refresh_token"].(string) if clientID == "" || clientSecret == "" || refreshToken == "" { return fmt.Errorf("平台 [%s] OAuth2 配置不完整: client_id / client_secret / refresh_token 缺失", platform.PlatformCode) } logrus.Infof("正在刷新腾讯广告 token [platform=%s]", platform.PlatformCode) // 调用腾讯 OAuth 刷新端点(使用短超时防止阻塞) refreshCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // 腾讯 OAuth refresh_token 接口使用 GET 方法,参数在查询字符串中 refreshURL := fmt.Sprintf("https://api.e.qq.com/oauth/refresh_token?client_id=%s&client_secret=%s&refresh_token=%s", url.QueryEscape(clientID), url.QueryEscape(clientSecret), url.QueryEscape(refreshToken)) req, err := http.NewRequestWithContext(refreshCtx, "GET", refreshURL, nil) if err != nil { return fmt.Errorf("创建刷新请求失败: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("请求刷新 token 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("读取刷新响应失败: %w", err) } var tokenResp tencentTokenResponse if err := json.Unmarshal(body, &tokenResp); err != nil { return fmt.Errorf("解析刷新响应失败: %w", err) } if tokenResp.Code != 0 { return fmt.Errorf("腾讯 OAuth 刷新失败: code=%d, msg=%s", tokenResp.Code, tokenResp.Message) } if tokenResp.Data == nil { return fmt.Errorf("腾讯 OAuth 刷新响应缺少 data 字段") } newAccessToken := tokenResp.Data.AccessToken newRefreshToken := tokenResp.Data.RefreshToken if newAccessToken == "" { return fmt.Errorf("腾讯 OAuth 刷新返回的 access_token 为空") } // --- 更新内存中的配置 --- // token 字段(存明文 token) platform.Token = newAccessToken // AccessToken 字段(运行时使用的 token) platform.AccessToken = newAccessToken // auth_config 中的 refresh_token platform.AuthConfig["refresh_token"] = newRefreshToken // --- 更新数据库 --- tenantID := utils.GetCurrentTenantId(ctx) if tenantID == 0 { tenantID = 1 // 保底默认租户 } // 使用 gfdb 直接更新 token 和 auth_config(避免 DAO 层的 OmitEmpty 吞掉空值) _, err = gfdb.DB(ctx).Model(ctx, consts.DatasourcePlatformTable). Data(g.Map{ "token": newAccessToken, "auth_config": platform.AuthConfig, "updated_at": gtime.Now(), }). Where("platform_code", platform.PlatformCode). Where("tenant_id", tenantID). Update() if err != nil { return fmt.Errorf("更新数据库 token 失败: %w", err) } logrus.Infof("腾讯广告 Token 刷新成功 [platform=%s] (access_token有效期:%ds, refresh_token有效期:%ds)", platform.PlatformCode, tokenResp.Data.ExpiresIn, tokenResp.Data.RefreshExpiresIn) return nil }