using System.Collections.Concurrent; using Microsoft.Playwright; namespace dy.net.service { public class DyQrCodeService { private static readonly ConcurrentDictionary _sessions = new(); private class QrSession { public IPlaywright Playwright { get; set; } public IBrowser Browser { get; set; } public IBrowserContext Context { get; set; } public IPage Page { get; set; } public DateTime CreatedAt { get; set; } = DateTime.Now; public string FinalCookies { get; set; } public string SecUserId { get; set; } } private const string LoginUrl = "https://creator.douyin.com/creator-micro/login"; // QR 码图片的 class 名含 qrcode_img(后缀随版本变化,用属性选择器匹配) private const string QrSelector = "img[class*='qrcode_img']"; public async Task<(string token, string qrBase64)> GetQrCodeAsync() { CleanExpiredSessions(); var playwright = await Playwright.CreateAsync(); var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true, Args = new[] { "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage" } }); var context = await browser.NewContextAsync(new BrowserNewContextOptions { UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" }); var page = await context.NewPageAsync(); await page.GotoAsync(LoginUrl, new PageGotoOptions { Timeout = 15000 }); await page.WaitForSelectorAsync(QrSelector, new PageWaitForSelectorOptions { Timeout = 10000 }); // img.src 已经是 data:image/png;base64,... 直接取用 var src = await page.EvaluateAsync($"() => document.querySelector(\"{QrSelector}\").src"); var qrBase64 = src.Replace("data:image/png;base64,", ""); var token = Guid.NewGuid().ToString("N"); _sessions[token] = new QrSession { Playwright = playwright, Browser = browser, Context = context, Page = page }; 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(5)) { await CloseSessionAsync(token, session); return (-1, null, null); } // 已经登录过,直接返回缓存 if (session.FinalCookies != null) return (3, session.FinalCookies, session.SecUserId); var url = session.Page.Url; // 还在登录页 if (url.Contains("/creator-micro/login") || url.Contains("sso.douyin.com")) { // 判断是否已扫码(页面出现"确认"或"已扫码"文字) var scanned = await session.Page.EvaluateAsync( "() => { const t = document.body.innerText; return t.includes('已扫码') || t.includes('确认登录'); }" ); return (scanned ? 2 : 1, null, null); } // URL 已离开登录页,说明登录成功 var browserCookies = await session.Context.CookiesAsync(new[] { "https://creator.douyin.com", "https://www.douyin.com", "https://sso.douyin.com" }); var cookieStr = string.Join("; ", browserCookies.Select(c => $"{c.Name}={c.Value}")); // 尝试从页面获取 sec_user_id var secUserId = await GetSecUserIdAsync(session.Page); session.FinalCookies = cookieStr; session.SecUserId = secUserId; // 登录完成后关闭浏览器释放资源 _ = Task.Run(async () => await CloseSessionAsync(token, session)); return (3, cookieStr, 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) { if (_sessions.TryRemove(token, out var session)) _ = Task.Run(async () => await CloseSessionAsync(token, session)); } private static async Task GetSecUserIdAsync(IPage page) { try { // 从页面 URL 或 API 提取 sec_user_id var resp = await page.EvaluateAsync( "async () => { const r = await fetch('/web/api/userinfo/'); const j = await r.json(); return j?.data?.sec_uid || j?.sec_uid || ''; }" ); return resp ?? ""; } catch { return ""; } } private static async Task CloseSessionAsync(string token, QrSession session) { _sessions.TryRemove(token, out _); try { await session.Browser?.CloseAsync(); } catch { } try { session.Playwright?.Dispose(); } catch { } } private static void CleanExpiredSessions() { var expired = _sessions .Where(kv => DateTime.Now - kv.Value.CreatedAt > TimeSpan.FromMinutes(6)) .ToList(); foreach (var kv in expired) _ = Task.Run(async () => await CloseSessionAsync(kv.Key, kv.Value)); } } }