using System.Collections.Concurrent; using System.Net; using Newtonsoft.Json.Linq; namespace dy.net.service { public class DyQrCodeService { private static readonly ConcurrentDictionary _sessions = new(); private class QrSession { public CookieContainer CookieJar { get; set; } = new(); public DateTime CreatedAt { get; set; } = DateTime.Now; public string? FinalCookies { get; set; } public string? SecUserId { get; set; } } private const string Fp = "kj5j6uhv_tvKYUFA0_qzgZ_4l9c_9Amt_DCywEfwbVFCJ"; private const string NextUrl = "https://creator.douyin.com/content/manage"; private const string Service = "https://creator.douyin.com"; private const string Aid = "2906"; private const string Ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"; private const string Referer = "https://creator.douyin.com/"; public async Task<(string token, string qrBase64)> GetQrCodeAsync() { var jar = new CookieContainer(); using var client = BuildClient(jar, autoRedirect: true); var url = $"https://sso.douyin.com/get_qrcode/?next={Uri.EscapeDataString(NextUrl)}&aid={Aid}&service={Uri.EscapeDataString(Service)}&is_vcd=1&fp={Fp}"; var response = await client.GetStringAsync(url); var json = JObject.Parse(response); var token = json["data"]?["token"]?.ToString() ?? throw new Exception("获取二维码失败:响应中无 token"); var qrBase64 = json["data"]?["qrcode"]?.ToString() ?? throw new Exception("获取二维码失败:响应中无图片"); _sessions[token] = new QrSession { CookieJar = jar }; CleanExpiredSessions(); return (token, qrBase64); } public async Task<(int status, string? cookies, string? secUserId)> CheckStatusAsync(string token) { if (!_sessions.TryGetValue(token, out var session)) return (-1, null, null); if (DateTime.Now - session.CreatedAt > TimeSpan.FromMinutes(10)) { _sessions.TryRemove(token, out _); return (-1, null, null); } if (session.FinalCookies != null) return (3, session.FinalCookies, session.SecUserId); using var client = BuildClient(session.CookieJar, autoRedirect: false); var serviceParam = Uri.EscapeDataString($"{Service}/?logintype=user&loginapp=douyin&jump={NextUrl}"); var url = $"https://sso.douyin.com/check_qrconnect/?next={Uri.EscapeDataString(NextUrl)}&token={token}&service={serviceParam}&loginapp=douyin&jump={Uri.EscapeDataString(NextUrl)}&correct_service={serviceParam}&aid={Aid}&is_vcd=1&fp={Fp}"; var response = await client.GetStringAsync(url); var json = JObject.Parse(response); var statusStr = json["data"]?["status"]?.ToString() ?? "1"; int.TryParse(statusStr, out int statusCode); if (statusCode == 3) { var redirectUrl = json["data"]?["redirect_url"]?.ToString(); if (!string.IsNullOrEmpty(redirectUrl)) { try { using var followClient = BuildClient(session.CookieJar, autoRedirect: true); await followClient.GetAsync(redirectUrl); session.FinalCookies = ExtractCookieString(session.CookieJar); session.SecUserId = await GetSecUserIdAsync(session.CookieJar); } catch (Exception ex) { Serilog.Log.Error($"跟进登录重定向失败: {ex.Message}"); } } } return (statusCode, session.FinalCookies, session.SecUserId); } public (string cookies, string secUserId) GetSessionResult(string token) { if (_sessions.TryGetValue(token, out var session)) return (session.FinalCookies ?? "", session.SecUserId ?? ""); return ("", ""); } public void RemoveSession(string token) => _sessions.TryRemove(token, out _); private static HttpClient BuildClient(CookieContainer jar, bool autoRedirect) { var handler = new HttpClientHandler { CookieContainer = jar, AllowAutoRedirect = autoRedirect, UseCookies = true }; var client = new HttpClient(handler); client.DefaultRequestHeaders.Add("User-Agent", Ua); client.DefaultRequestHeaders.Add("Referer", Referer); return client; } private static string ExtractCookieString(CookieContainer jar) { var domains = new[] { "https://www.douyin.com", "https://creator.douyin.com", "https://sso.douyin.com" }; var all = new Dictionary(); foreach (var domain in domains) { foreach (Cookie c in jar.GetCookies(new Uri(domain))) all[c.Name] = c.Value; } return string.Join("; ", all.Select(kv => $"{kv.Key}={kv.Value}")); } private static async Task GetSecUserIdAsync(CookieContainer jar) { try { using var client = BuildClient(jar, autoRedirect: true); var resp = await client.GetStringAsync("https://creator.douyin.com/web/api/userinfo/"); var json = JObject.Parse(resp); return json["data"]?["sec_uid"]?.ToString() ?? json["data"]?["sec_user_id"]?.ToString() ?? json["sec_uid"]?.ToString(); } catch (Exception ex) { Serilog.Log.Error($"获取 sec_user_id 失败: {ex.Message}"); return null; } } private static void CleanExpiredSessions() { var expired = _sessions .Where(kv => DateTime.Now - kv.Value.CreatedAt > TimeSpan.FromMinutes(10)) .Select(kv => kv.Key).ToList(); foreach (var key in expired) _sessions.TryRemove(key, out _); } } }