using dodohold.core; using Sayaka.Common; using System.Diagnostics; using System.Text.RegularExpressions; using System.Threading.Channels; using System.Web; namespace molilian.core { public enum RrecheckGoodsIdResult { Success, Empty, Exception, InvalidLink, OtherAff, } public partial class PddUnionPlus { private static readonly object _precheckCircuitLock = new(); private static bool _precheckDisabled = false; private static int _precheckConsecutiveExceptionCount = 0; private static int _precheckDisableErrorThreshold = 10; public static string _url_pattern = @"https?://(?:[\w-]+\.)*(?:yangkeduo\.com|pinduoduo\.com)(?:/[^\s]*)?"; public static string GetLink(string content) { if (string.IsNullOrEmpty(content)) return content; string result = content; // Regex regex = new(_url_pattern); MatchCollection matches = regex.Matches(content); foreach (Match m in matches.Cast()) { string url = m.Value; return url; } return string.Empty; } public static string GetDeeplink(string url) { string result = $"pinduoduo://com.xunmeng.pinduoduo/{url}"; return result; } private static bool IsGoodsDetailPath(string path) { if (string.IsNullOrWhiteSpace(path)) return false; return Regex.IsMatch(path, @"^/goods\d*\.html$", RegexOptions.IgnoreCase); } private static bool IsPddShortLinkHost(string host) { return "p.pinduoduo.com".Equals(host, StringComparison.OrdinalIgnoreCase); } private static bool TryGetPrecheckPsGoodsUrl(string url, out string normalizedUrl) { normalizedUrl = string.Empty; if (string.IsNullOrWhiteSpace(url)) return false; if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false; if (IsPddShortLinkHost(uri.Host) && !string.IsNullOrWhiteSpace(uri.AbsolutePath) && uri.AbsolutePath != "/") { normalizedUrl = $"{uri.Scheme}://{uri.Host}{uri.PathAndQuery}"; return true; } if (!IsGoodsDetailPath(uri.AbsolutePath)) return false; var queryParams = HttpUtility.ParseQueryString(uri.Query); string goodsId = queryParams["goods_id"]; if (!string.IsNullOrWhiteSpace(goodsId) && long.TryParse(goodsId, out _)) { return false; } string ps = queryParams["ps"]; if (string.IsNullOrWhiteSpace(ps)) return false; normalizedUrl = $"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}?ps={HttpUtility.UrlEncode(ps)}"; return true; } private static bool IsPrecheckDisabled() { lock (_precheckCircuitLock) { return _precheckDisabled; } } private static void ResetPrecheckExceptionState() { lock (_precheckCircuitLock) { _precheckConsecutiveExceptionCount = 0; } } public static void ResetPrecheckCircuitState() { lock (_precheckCircuitLock) { _precheckDisabled = false; _precheckConsecutiveExceptionCount = 0; } } private static (int count, bool disabledNow) RegisterPrecheckException() { lock (_precheckCircuitLock) { _precheckConsecutiveExceptionCount++; bool disabledNow = false; if (!_precheckDisabled && _precheckConsecutiveExceptionCount >= _precheckDisableErrorThreshold) { _precheckDisabled = true; disabledNow = true; } return (_precheckConsecutiveExceptionCount, disabledNow); } } private static void NotifyPrecheckException(string goodsUrl, string stage, string detail, int count, bool disabledNow) { string message = $"【PDD商品Precheck异常】{_end_point}\n" + $"url:{goodsUrl}\n" + $"stage:{stage}\n" + $"连续异常:{count}/{_precheckDisableErrorThreshold}\n" + $"{detail}"; if (disabledNow) { message += "\n状态: 已临时禁用Precheck,后续默认返回Success"; } NotifyCore.Notify(new NifyMessage { message = message, priority = NifyMessagePriority.high, tags = ["red_circle"] }); } private string ExtractGoodsIdUrl(string url) { var uri = new Uri(url); var queryParams = HttpUtility.ParseQueryString(uri.Query); string goodsId = queryParams["goods_id"]; if (!string.IsNullOrEmpty(goodsId) && uri.AbsolutePath.StartsWith("/goods")) { // 保持域名和路径不变,仅保留 goods_id 参数 var baseUrl = $"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}"; return $"{baseUrl}?goods_id={goodsId}"; } return url; // 如果没有找到 goods_id 参数,则返回原始 URL } private bool IsTrackUrl(string sourceUrl) { string pattern = @"^https?://mobile\.yangkeduo\.com/goods(\d*)\.html\?ps=.*$"; Regex regex = new Regex(pattern); return regex.IsMatch(sourceUrl); } /// /// bool:是否ps链接,string 处理后的链接 /// /// /// /// public async Task GetRedirectedUrlAsync(string sourceUrl, CancellationToken cancellationToken = default) { var isTrackUrl = IsTrackUrl(sourceUrl); if (!isTrackUrl) return ExtractGoodsIdUrl(sourceUrl); string redirectedUrl = sourceUrl; try { string useragent = ProviderFakeUserAgent.RandomMobile; WebClientUtility client = new() { AllowAutoRedirect = false, UserAgent = useragent, }; client.Proxy = _proxy; #if DEBUG client.Proxy = null; #endif var response = await client.RequestAsync(sourceUrl, "GET", cancellationToken); if (!response.Successed) return null; if (response.ResponseMessage.StatusCode == System.Net.HttpStatusCode.RedirectKeepVerb || response.ResponseMessage.StatusCode == System.Net.HttpStatusCode.TemporaryRedirect) { redirectedUrl = response.ResponseMessage.Headers.Location.ToString(); redirectedUrl = ExtractGoodsIdUrl(redirectedUrl); return redirectedUrl; } } catch (Exception ex) { } return sourceUrl; } public static PddDataDTO GetFormattedObject(UnionParseRequest request) { switch (request.RiskStrategy) { case "tbpush": if (AlimamaPlus.IsDigitsOnly(request.Content)) { request.Content = $"https://mobile.yangkeduo.com/goods.html?goods_id={request.Content}"; } break; } string shortLinkurl = GetLink(request.Content); string deeplink_url = GetDeeplink(shortLinkurl); var link_type = LinkTypeEnum.unknown; if (!string.IsNullOrEmpty(shortLinkurl) && shortLinkurl.StartsWith("https://p.pinduoduo.com/")) { link_type = LinkTypeEnum.other_aff; } return new PddDataDTO() { message = string.Empty, link_type = link_type, channel = TkChannelEnum.pdd, accountName = string.Empty, success = false, content = request.Content, ip = request.Ip, oaid = request.Oaid, elapsedTime = 0, itemName = "点击打开拼多多APP", shortLinkurl = shortLinkurl, deeplink_url = string.Empty, create_time = DateTime.Now, end_point = _end_point, parse_type = request.Type, riskStrategy = request.RiskStrategy, launchScene = request.LaunchScene, }; } public static async Task PrecheckGoodsIdAsync(string content, CancellationToken cancellationToken = default) { var config = TkConfigCore.Get(); if (config != null && !config.pdd_rrecheck_enabled) { return RrecheckGoodsIdResult.Success; } string url = GetLink(content); if (string.IsNullOrEmpty(url)) { return RrecheckGoodsIdResult.InvalidLink; } if (!TryGetPrecheckPsGoodsUrl(url, out string goods_url)) { return RrecheckGoodsIdResult.Success; } if (IsPrecheckDisabled()) return RrecheckGoodsIdResult.Success; if (string.IsNullOrWhiteSpace(goods_url)) return RrecheckGoodsIdResult.Empty; try { string precheckClientId = (config?.pdd_precheck_client_id ?? TkConfigDTO.DefaultPddPrecheckClientId).Trim(); string precheckClientSecret = (config?.pdd_precheck_client_secret ?? TkConfigDTO.DefaultPddPrecheckClientSecret).Trim(); string precheckPid = (config?.pdd_precheck_pid ?? TkConfigDTO.DefaultPddPrecheckPid).Trim(); if (string.IsNullOrWhiteSpace(precheckClientId) || string.IsNullOrWhiteSpace(precheckClientSecret) || string.IsNullOrWhiteSpace(precheckPid)) { _ = new LoggerLibrary("PddUnion", "PrecheckGoodsIdAsync.Config") .Info(goods_url, "precheck config missing") .SaveAsync(); var status = RegisterPrecheckException(); NotifyPrecheckException(goods_url, "Config", "precheck config missing", status.count, status.disabledNow); return status.disabledNow ? RrecheckGoodsIdResult.Success : RrecheckGoodsIdResult.Exception; } var precheckAccount = new PddPoolDTO { app_key = precheckClientId, app_secret = precheckClientSecret }; var ts = DateTime.Now.Convert2UnixTimestamp(true); var args = new Dictionary { { "type", "pdd.ddk.goods.search" }, { "data_type", "JSON" }, { "client_id", precheckClientId }, { "keyword", goods_url.UrlDecode() }, { "pid", precheckPid }, { "timestamp", ts.ToString() } }; args = GenerateSignature(precheckAccount, args); var query = HttpUtility.ParseQueryString(string.Empty); foreach (var kvp in args) query[kvp.Key] = kvp.Value; string urlWithQuery = $"{_baseUrl}?{query}"; WebClientUtility client = new(); client.SetContentType("application/json"); var response = await client.RequestAsync(urlWithQuery, "GET", cancellationToken); string body = response.Body(); if (!response.Successed) { _ = new LoggerLibrary("PddUnion", "PrecheckGoodsIdAsync").Info(urlWithQuery, body).SaveAsync(); var status = RegisterPrecheckException(); NotifyPrecheckException(goods_url, "RequestAsync", $"response.Successed=false\nbody:{body}", status.count, status.disabledNow); return status.disabledNow ? RrecheckGoodsIdResult.Success : RrecheckGoodsIdResult.Exception; } var root = body.Convert2Object(); if (root?.error_response != null) { _ = new LoggerLibrary("PddUnion", "PrecheckGoodsIdAsync").Info(urlWithQuery, body).SaveAsync(); var status = RegisterPrecheckException(); var error = root.error_response; NotifyPrecheckException( goods_url, "ErrorResponse", $"error_code:{error.error_code}\nsub_code:{error.sub_code}\nerror_msg:{error.error_msg}\nsub_msg:{error.sub_msg}\nbody:{body}", status.count, status.disabledNow); return status.disabledNow ? RrecheckGoodsIdResult.Success : RrecheckGoodsIdResult.Exception; } var goodsList = root?.goods_search_response?.goods_list; if (goodsList == null) { _ = new LoggerLibrary("PddUnion", "PrecheckGoodsIdAsync").Info(urlWithQuery, body).SaveAsync(); var status = RegisterPrecheckException(); NotifyPrecheckException(goods_url, "GoodsSearchResponse", $"goods_list is null\nbody:{body}", status.count, status.disabledNow); return status.disabledNow ? RrecheckGoodsIdResult.Success : RrecheckGoodsIdResult.Exception; } bool isValidGoods = goodsList.Any(item => item.goods_id > 0 && !string.IsNullOrWhiteSpace(item.goods_name)); if (!isValidGoods && goodsList.Count > 0) { _ = new LoggerLibrary("PddUnion", "PrecheckGoodsIdAsync.Empty").Info(urlWithQuery, body).SaveAsync(); } ResetPrecheckExceptionState(); return isValidGoods ? RrecheckGoodsIdResult.Success : RrecheckGoodsIdResult.Empty; } catch (Exception ex) { _ = new LoggerLibrary("PddUnion", "PrecheckGoodsIdAsync").Info(goods_url, ex.ToString()).SaveAsync(); var status = RegisterPrecheckException(); NotifyPrecheckException(goods_url, "Exception", ex.ToString(), status.count, status.disabledNow); return status.disabledNow ? RrecheckGoodsIdResult.Success : RrecheckGoodsIdResult.Exception; } // client_id:c8823690b47842649e4fee054317e0c1 //client_secret:0b5eeab761a6b61df59f11296387b95523a26b09 //https://gw-api.pinduoduo.com/api/router //{ // "type": "pdd.ddk.goods.search", // "data_type": "JSON", // "client_id": "c8823690b47842649e4fee054317e0c1", // "keyword": "https://mobile.yangkeduo.com/goods.html?ps=ROyjHRQJ7d", // "pid": "13585632_187155840", // "timestamp": 1779396871, // "sign": "3DE407843BECA8BA480E8BA4C51E6049" //} // 这样可以用字符串链接去查询是否多多商品。返回goods_list里面有内容就是可以去转链的。 //下面这个就是不能转链的 //{ // "goods_search_response": { // "goods_list": [{ // "subsidy_goods_type": 0, // "subsidy_list": [], // "platform_discount_list": [], // "goods_id": 922492775451, // "goods_sign": "E9L2dKKUUo9LeKDhwePAPXLcFjD6hB3MKw_JGaDyX4DX" // }], // "total_count": 1, // "request_id": "17793969978830062" // } // } } public async Task PddParseAsync(string content, int commerceType, PddDataDTO result, CancellationToken cancellationToken = default) { string message = "OK"; string url = GetLink(content); if (string.IsNullOrEmpty(url)) { result.success = false; result.link_type = LinkTypeEnum.unknown; result.channel_type = ChannelTypeEnum.pdd; result.message = "放弃转链"; result.reason = "无效链接"; result.accountId = 0; result.accountName = string.Empty; result.content = content; return result; } string shortLinkurl = url; if (result.link_type == LinkTypeEnum.other_aff) { result.success = false; result.link_type = LinkTypeEnum.unknown; result.channel_type = ChannelTypeEnum.pdd; result.deeplink_url = GetDeeplink(url); result.message = "放弃转链"; result.reason = "其他推广链接"; result.accountId = 0; result.accountName = string.Empty; result.content = content; return result; } // 移到外围了 //if (!TestParseCore.InWhitelist(result.ip, result.oaid) && FlowControlIgnoreRequest(_config, out string reason)) //{ // result.success = false; // result.link_type = LinkTypeEnum.unknown; // result.channel_type = ChannelTypeEnum.pdd; // result.message = "放弃转链"; // result.reason = reason; // result.accountId = 0; // result.accountName = string.Empty; // result.content = content; // result.deeplink_url = GetDeeplink(result.shortLinkurl); // return result; //} var isTrackUrl = IsTrackUrl(url); //不是跟踪链接 ps=*** 就返回 //2024-09-26 放开数字id转链 //if (!isTrackUrl) //{ // result.success = false; // result.link_type = LinkTypeEnum.unknown; // result.channel_type = ChannelTypeEnum.pdd; // result.message = "放弃转链"; // result.reason = "数字id链接"; // result.accountId = 0; // result.accountName = string.Empty; // result.content = content; // return result; //} //去追踪 Stopwatch stopwatch = Stopwatch.StartNew(); stopwatch.Start(); var redirectedUrl = await GetRedirectedUrlAsync(url, cancellationToken); stopwatch.Stop(); bool redirected = !string.IsNullOrWhiteSpace(redirectedUrl) && !string.Equals(redirectedUrl, url, StringComparison.OrdinalIgnoreCase); if (redirected) { // 字符串ID转数字ID result.rawContent2 = redirectedUrl; url = redirectedUrl; result.elapsedTime2 = (int)stopwatch.ElapsedMilliseconds; } switch (commerceType) { case 1: case 2: result.success = false; result.message = "放弃转链"; result.reason = $"厂商放弃{commerceType}"; return result; } Stopwatch stopwatch2 = Stopwatch.StartNew(); stopwatch2.Start(); result = _account.work_mode switch { PddUnionWorkMode.SiteApi => await UnionGeneratelink(result, url, cancellationToken), //case PddUnionWorkMode.AffApi: // result = await GetPromotionByAffAsync(result, url, cancellationToken); // break; _ => await transferUrl(result, url, cancellationToken), }; if (!result.success) { result.shortLinkurl = shortLinkurl; result.deeplink_url = GetDeeplink(shortLinkurl); } stopwatch2.Stop(); result.elapsedTime3 = (int)stopwatch2.ElapsedMilliseconds; return result; } } }