|
|
@@ -1,17 +1,30 @@
|
|
|
using dodohold.core;
|
|
|
-using Sayaka.Common;
|
|
|
-using System.Diagnostics;
|
|
|
-using System.Text.RegularExpressions;
|
|
|
-using System.Threading.Channels;
|
|
|
-using System.Web;
|
|
|
+using Sayaka.Common;
|
|
|
+using System.Diagnostics;
|
|
|
+using System.Text.RegularExpressions;
|
|
|
+using System.Threading.Channels;
|
|
|
+using System.Web;
|
|
|
|
|
|
namespace molilian.core
|
|
|
{
|
|
|
- public partial class PddUnionPlus
|
|
|
+ public enum RrecheckGoodsIdResult
|
|
|
{
|
|
|
- public static string _url_pattern = @"https?://(?:[\w-]+\.)*(?:yangkeduo\.com|pinduoduo\.com)(?:/[^\s]*)?";
|
|
|
- public static string GetLink(string content)
|
|
|
- {
|
|
|
+ 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;
|
|
|
//
|
|
|
@@ -27,16 +40,107 @@ namespace molilian.core
|
|
|
}
|
|
|
|
|
|
|
|
|
- public static string GetDeeplink(string url)
|
|
|
- {
|
|
|
- string result = $"pinduoduo://com.xunmeng.pinduoduo/{url}";
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- private string ExtractGoodsIdUrl(string url)
|
|
|
- {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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"];
|
|
|
@@ -145,6 +249,132 @@ namespace molilian.core
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ public static async Task<RrecheckGoodsIdResult> PrecheckGoodsIdAsync(string content, CancellationToken cancellationToken = default)
|
|
|
+ {
|
|
|
+ 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
|
|
|
+ {
|
|
|
+ const string precheckClientId = "c8823690b47842649e4fee054317e0c1";
|
|
|
+ const string precheckClientSecret = "0b5eeab761a6b61df59f11296387b95523a26b09";
|
|
|
+ const string precheckPid = "13585632_187155840";
|
|
|
+ var precheckAccount = new PddPoolDTO
|
|
|
+ {
|
|
|
+ app_key = precheckClientId,
|
|
|
+ app_secret = precheckClientSecret
|
|
|
+ };
|
|
|
+ var ts = DateTime.Now.Convert2UnixTimestamp(true);
|
|
|
+ var args = new Dictionary<string, string>
|
|
|
+ {
|
|
|
+ { "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<GoodsSearchDTO>();
|
|
|
+ if (root?.error_response != null)
|
|
|
+ {
|
|
|
+ _ = new LoggerLibrary("PddUnion", "PrecheckGoodsIdAsync.Empty").Info(urlWithQuery, body).SaveAsync();
|
|
|
+ ResetPrecheckExceptionState();
|
|
|
+ return RrecheckGoodsIdResult.Empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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<PddDataDTO> PddParseAsync(string content, int commerceType, PddDataDTO result, CancellationToken cancellationToken = default)
|
|
|
{
|
|
|
string message = "OK";
|
|
|
@@ -214,10 +444,13 @@ namespace molilian.core
|
|
|
stopwatch.Start();
|
|
|
var redirectedUrl = await GetRedirectedUrlAsync(url, cancellationToken);
|
|
|
stopwatch.Stop();
|
|
|
+ bool redirected = !string.IsNullOrWhiteSpace(redirectedUrl) &&
|
|
|
+ !string.Equals(redirectedUrl, url, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
- if (redirectedUrl != url)
|
|
|
+ if (redirected)
|
|
|
{
|
|
|
+ // 字符串ID转数字ID
|
|
|
result.rawContent2 = redirectedUrl;
|
|
|
url = redirectedUrl;
|
|
|
result.elapsedTime2 = (int)stopwatch.ElapsedMilliseconds;
|
|
|
@@ -253,6 +486,7 @@ namespace molilian.core
|
|
|
}
|
|
|
|
|
|
|
|
|
+
|
|
|
stopwatch2.Stop();
|
|
|
result.elapsedTime3 = (int)stopwatch2.ElapsedMilliseconds;
|
|
|
return result;
|