PddPoolCore.cs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  1. using Microsoft.AspNetCore.Http;
  2. using Microsoft.AspNetCore.Mvc.Controllers;
  3. using Microsoft.AspNetCore.Mvc.Filters;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Linq;
  7. using System.Text;
  8. using dodohold.core;
  9. using Dataoke;
  10. using Google.Protobuf.WellKnownTypes;
  11. using System.Diagnostics;
  12. using System.Text.Json;
  13. using TencentCloud.Tcm.V20210413.Models;
  14. using System.Security.Cryptography;
  15. using System.Collections.Concurrent;
  16. using YunhuiKit;
  17. using System.Xml.Linq;
  18. namespace molilian.core
  19. {
  20. public partial class PddPoolCore
  21. {
  22. private static readonly object _lockObj = new();
  23. private static IEnumerable<PddPoolDTO> _cached;
  24. private static IEnumerable<PddPoolDTO> _all_cached;
  25. private static string _end_point;
  26. private static Dictionary<string, decimal> _incomeAmt = new();
  27. private static ConcurrentDictionary<int, DateTime> _suspend = new();
  28. // 移除本地内存统计,改用 Redis 存储
  29. private static SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
  30. static PddPoolCore()
  31. {
  32. _end_point = Environment.GetEnvironmentVariable("EndPoint");
  33. }
  34. /// <summary>
  35. /// 获取一个账号用于处理请求
  36. /// 使用平滑加权轮询算法(Smooth Weighted Round-Robin)实现流量均衡分配
  37. /// </summary>
  38. public static PddPoolDTO? GetOne(PddUnionWorkMode mode, int accountid = 0, string parse_type = "")
  39. {
  40. var list = List();
  41. if (!list.Any()) return null;
  42. // 按 parse_type 过滤
  43. if ("dp".Equals(parse_type))
  44. {
  45. list = list.Where(item => "dp".Equals(item.parse_type)).ToList();
  46. if (!list.Any()) return null;
  47. }
  48. else
  49. {
  50. list = list.Where(item => string.IsNullOrEmpty(item.parse_type)).ToList();
  51. if (!list.Any()) return null;
  52. }
  53. // 过滤出符合条件的候选账号
  54. var candidates = list.Where(e => IsMatch(e, mode, accountid)).ToList();
  55. if (!candidates.Any()) return null;
  56. // 使用平滑加权轮询算法选择账号
  57. return SmoothWeightedRoundRobin(candidates, parse_type);
  58. }
  59. /// <summary>
  60. /// 基于最后使用时间的调度策略(并发安全版本)
  61. /// 选择"距离上次使用时间最长"且"超过cis_limit间隔"的账号
  62. ///
  63. /// 优势:
  64. /// 1. 不区分新老账号,一视同仁(无历史记录的账号返回DateTime.MinValue,自然会被优先选中)
  65. /// 2. 自然满足cis_limit要求(距离时间 < limit 会被跳过)
  66. /// 3. 优先选择"休息最久"的账号,最大化每个账号的休息时间
  67. /// 4. 简单直观,易于调试和理解
  68. /// 5. 并发安全:使用Redis原子操作预留账号,避免多个请求竞争同一账号
  69. /// </summary>
  70. private static PddPoolDTO? SmoothWeightedRoundRobin(List<PddPoolDTO> candidates, string parse_type)
  71. {
  72. if (!candidates.Any()) return null;
  73. #if DEBUG
  74. var single = candidates[0];
  75. return single;
  76. #else
  77. if (candidates.Count == 1)
  78. {
  79. var single = candidates[0];
  80. // 尝试预留账号(并发安全)
  81. if (TryReserveAccount(single.id, parse_type, single.cis_limit))
  82. {
  83. return single;
  84. }
  85. return null; // 预留失败,说明账号已被其他请求占用
  86. }
  87. #endif
  88. try
  89. {
  90. string groupKey = string.IsNullOrEmpty(parse_type) ? "default" : parse_type;
  91. var now = DateTime.Now;
  92. // 候选账号列表:记录每个账号的空闲时长
  93. var accountsWithIdleTime = new List<(PddPoolDTO account, long idleMs)>();
  94. foreach (var account in candidates)
  95. {
  96. // 获取账号最后使用时间
  97. var lastUsedTime = GetAccountLastUsedTime(account.id, groupKey);
  98. // 计算距离现在的时间差(毫秒)
  99. long idleMs = (long)(now - lastUsedTime).TotalMilliseconds;
  100. // 如果设置了 cis_limit,必须满足间隔要求
  101. if (account.cis_limit > 0 && idleMs < account.cis_limit)
  102. {
  103. continue; // 跳过未满足间隔要求的账号
  104. }
  105. accountsWithIdleTime.Add((account, idleMs));
  106. }
  107. // 如果没有满足条件的账号,返回null
  108. if (!accountsWithIdleTime.Any()) return null;
  109. // 按照空闲时间降序排序,优先尝试"休息最久"的账号
  110. var sortedAccounts = accountsWithIdleTime
  111. .OrderByDescending(x => x.idleMs)
  112. .ToList();
  113. // 依次尝试预留账号(从空闲时间最长的开始)
  114. foreach (var (account, idleMs) in sortedAccounts)
  115. {
  116. // 尝试原子预留此账号
  117. if (TryReserveAccount(account.id, groupKey, account.cis_limit))
  118. {
  119. // 预留成功,更新最后使用时间
  120. SetAccountLastUsedTime(account.id, groupKey, now);
  121. Console.WriteLine($"[PDD] Selected account {account.id} ({account.name}), idle time: {Math.Round(idleMs / 1000.0, 2)}s");
  122. return account;
  123. }
  124. else
  125. {
  126. // 预留失败,说明此账号已被其他并发请求占用,尝试下一个
  127. Console.WriteLine($"[PDD] Account {account.id} ({account.name}) is reserved by another request, trying next...");
  128. }
  129. }
  130. // 所有满足条件的账号都已被占用
  131. Console.WriteLine($"[PDD] All available accounts are reserved, no account available");
  132. return null;
  133. }
  134. catch (Exception ex)
  135. {
  136. Console.WriteLine($"Idle time selection failed: {ex.Message}, fallback to simple strategy");
  137. // 降级策略:使用原有的最少使用次数算法
  138. return candidates
  139. .OrderBy(account => GetCurrentDayUsageFromRedis(account.id))
  140. .ThenBy(account => Guid.NewGuid())
  141. .FirstOrDefault();
  142. }
  143. }
  144. /// <summary>
  145. /// 尝试原子预留账号(并发安全)
  146. /// 使用 Redis SET NX EX 实现原子操作,避免多个请求竞争同一账号
  147. /// </summary>
  148. /// <param name="accountId">账号ID</param>
  149. /// <param name="groupKey">分组键</param>
  150. /// <param name="cisLimit">请求间隔限制(毫秒),0表示无限制</param>
  151. /// <returns>true=预留成功,false=账号已被占用</returns>
  152. private static bool TryReserveAccount(int accountId, string groupKey, int cisLimit)
  153. {
  154. try
  155. {
  156. string reserveKey = $"pdd_account_reserved:{groupKey}:{accountId}";
  157. // 如果没有设置 cis_limit,使用默认100ms的预留时间(防止极端并发)
  158. int expireSeconds = cisLimit > 0 ? (int)Math.Ceiling(cisLimit / 1000.0) : 1;
  159. // 使用 Redis SET NX EX 原子操作(一次性完成设置和过期时间)
  160. // 等价于 Redis 命令: SET key value NX EX seconds
  161. // 如果 key 不存在则设置成功(返回true),如果已存在则失败(返回false)
  162. // 注意:必须使用原子操作,SetNx + Expire 两步操作会有竞态条件!
  163. bool reserved = RedisHelper.Set(reserveKey, 1, expireSeconds, CSRedis.RedisExistence.Nx);
  164. return reserved;
  165. }
  166. catch (Exception ex)
  167. {
  168. Console.WriteLine($"Failed to reserve account {accountId}: {ex.Message}");
  169. // 预留失败时保守处理:假设账号不可用
  170. return false;
  171. }
  172. }
  173. /// <summary>
  174. /// 获取账号最后使用时间
  175. /// </summary>
  176. private static DateTime GetAccountLastUsedTime(int accountId, string groupKey)
  177. {
  178. try
  179. {
  180. string key = $"pdd_last_used_time:{groupKey}:{accountId}";
  181. var timestamp = RedisHelper.Get<long>(key);
  182. if (timestamp > 0)
  183. {
  184. return DateTimeOffset.FromUnixTimeMilliseconds(timestamp).LocalDateTime;
  185. }
  186. // 如果没有记录,返回很久以前的时间(确保新账号和长期未使用的账号能被优先选中)
  187. return DateTime.MinValue;
  188. }
  189. catch
  190. {
  191. return DateTime.MinValue;
  192. }
  193. }
  194. /// <summary>
  195. /// 设置账号最后使用时间
  196. /// </summary>
  197. private static void SetAccountLastUsedTime(int accountId, string groupKey, DateTime time)
  198. {
  199. try
  200. {
  201. string key = $"pdd_last_used_time:{groupKey}:{accountId}";
  202. long timestamp = new DateTimeOffset(time).ToUnixTimeMilliseconds();
  203. // 设置过期时间为24小时,避免数据积累
  204. RedisHelper.Set(key, timestamp, 86400);
  205. }
  206. catch (Exception ex)
  207. {
  208. Console.WriteLine($"Failed to set last used time for {accountId}: {ex.Message}");
  209. }
  210. }
  211. /// <summary>
  212. /// 获取账号的静态权重
  213. /// 可以根据账号的 daily_calls_limit 或其他因素动态计算
  214. /// </summary>
  215. private static int GetAccountStaticWeight(int accountId)
  216. {
  217. try
  218. {
  219. string key = $"pdd_account_weight:{accountId}";
  220. int weight = RedisHelper.Get<int>(key);
  221. // 如果没有设置,返回默认权重100
  222. if (weight <= 0)
  223. {
  224. // 可以基于账号的限额动态计算默认权重
  225. var account = List()?.FirstOrDefault(a => a.id == accountId);
  226. if (account != null && account.daily_calls_limit > 0)
  227. {
  228. // 将日限额映射到权重:每1000次调用对应权重10
  229. weight = Math.Max(10, Math.Min(1000, account.daily_calls_limit / 100));
  230. }
  231. else
  232. {
  233. weight = 100; // 默认权重
  234. }
  235. }
  236. return weight;
  237. }
  238. catch
  239. {
  240. return 100; // 异常时返回默认权重
  241. }
  242. }
  243. /// <summary>
  244. /// 设置账号的静态权重
  245. /// </summary>
  246. public static void SetAccountStaticWeight(int accountId, int weight)
  247. {
  248. try
  249. {
  250. if (weight < 1) weight = 1;
  251. if (weight > 1000) weight = 1000;
  252. string key = $"pdd_account_weight:{accountId}";
  253. RedisHelper.Set(key, weight, 30 * 86400); // 30天过期
  254. }
  255. catch (Exception ex)
  256. {
  257. Console.WriteLine($"Failed to set account weight for {accountId}: {ex.Message}");
  258. }
  259. }
  260. /// <summary>
  261. /// 获取账号的当前动态权重
  262. /// </summary>
  263. private static int GetAccountCurrentWeight(int accountId, string groupKey)
  264. {
  265. try
  266. {
  267. string key = $"pdd_current_weight:{groupKey}:{accountId}";
  268. return RedisHelper.Get<int>(key);
  269. }
  270. catch
  271. {
  272. return 0;
  273. }
  274. }
  275. /// <summary>
  276. /// 设置账号的当前动态权重
  277. /// </summary>
  278. private static void SetAccountCurrentWeight(int accountId, string groupKey, int weight)
  279. {
  280. try
  281. {
  282. string key = $"pdd_current_weight:{groupKey}:{accountId}";
  283. RedisHelper.Set(key, weight, 3600); // 1小时过期,自动重置
  284. }
  285. catch (Exception ex)
  286. {
  287. Console.WriteLine($"Failed to set current weight for {accountId}: {ex.Message}");
  288. }
  289. }
  290. /// <summary>
  291. /// 重置所有账号的动态权重(用于调试或重新初始化)
  292. /// </summary>
  293. public static void ResetAllWeights(string groupKey = "default")
  294. {
  295. try
  296. {
  297. var accounts = List()?.Where(a => a.status && a.enable_parse).ToList();
  298. if (accounts == null || !accounts.Any()) return;
  299. foreach (var account in accounts)
  300. {
  301. string key = $"pdd_current_weight:{groupKey}:{account.id}";
  302. RedisHelper.Del(key);
  303. }
  304. Console.WriteLine($"Reset weights for {accounts.Count} accounts in group '{groupKey}'");
  305. }
  306. catch (Exception ex)
  307. {
  308. Console.WriteLine($"Failed to reset weights: {ex.Message}");
  309. }
  310. }
  311. private static int GetCurrentDayUsageFromRedis(int accountId)
  312. {
  313. try
  314. {
  315. var today = DateTime.Now.ToString("yyyyMMdd");
  316. string key = $"pdd_daily_usage:{accountId}:{today}";
  317. return RedisHelper.Get<int>(key);
  318. }
  319. catch (Exception)
  320. {
  321. return 0;
  322. }
  323. }
  324. internal static void UpdateAccountUsage(int accountId)
  325. {
  326. try
  327. {
  328. var today = DateTime.Now.ToString("yyyyMMdd");
  329. string key = $"pdd_daily_usage:{accountId}:{today}";
  330. // 递增计数并设置24小时过期时间
  331. RedisHelper.IncrBy(key);
  332. RedisHelper.Expire(key, 86400); // 24小时后自动过期
  333. }
  334. catch (Exception ex)
  335. {
  336. // 记录错误但不影响主流程
  337. Console.WriteLine($"Failed to update account usage for {accountId}: {ex.Message}");
  338. }
  339. }
  340. /// <summary>
  341. /// 获取当前所有账号的平均使用次数
  342. /// </summary>
  343. private static int GetAverageUsageCount()
  344. {
  345. try
  346. {
  347. var activeAccounts = List()?.Where(a => a.enable_parse).ToList();
  348. if (activeAccounts == null || !activeAccounts.Any()) return 0;
  349. var today = DateTime.Now.ToString("yyyyMMdd");
  350. var totalUsage = 0;
  351. var validCount = 0;
  352. foreach (var account in activeAccounts)
  353. {
  354. var usage = GetCurrentDayUsageFromRedis(account.id);
  355. totalUsage += usage;
  356. validCount++;
  357. }
  358. return validCount > 0 ? totalUsage / validCount : 0;
  359. }
  360. catch (Exception ex)
  361. {
  362. Console.WriteLine($"Failed to get average usage count: {ex.Message}");
  363. return 0;
  364. }
  365. }
  366. /// <summary>
  367. /// 重置所有在线账号的当日调用次数Redis计数器
  368. /// 随机排序账号后,每个账号间隔1递增value
  369. /// </summary>
  370. public static int RechargeAllOnlineAccountUsage()
  371. {
  372. try
  373. {
  374. var onlineAccounts = List()?.Where(a => a.status && a.enable_parse).ToList();
  375. if (onlineAccounts == null || !onlineAccounts.Any())
  376. {
  377. return 0;
  378. }
  379. var today = DateTime.Now.ToString("yyyyMMdd");
  380. // 随机排序账号
  381. var random = new Random();
  382. var shuffledAccounts = onlineAccounts.OrderBy(x => random.Next()).ToList();
  383. int incrementValue = 1;
  384. int rechargedCount = 0;
  385. foreach (var account in shuffledAccounts)
  386. {
  387. string key = $"pdd_daily_usage:{account.id}:{today}";
  388. // 设置递增的value并设置24小时过期时间
  389. RedisHelper.Set(key, incrementValue, 86400);
  390. RedisHelper.Set(key, 0, 86400);
  391. incrementValue++;
  392. rechargedCount++;
  393. }
  394. return rechargedCount;
  395. }
  396. catch (Exception ex)
  397. {
  398. }
  399. return 0;
  400. }
  401. /// <summary>
  402. /// 为新上线的账号设置平均使用次数(仅用于降级策略的统计)
  403. /// 注意:基于时间间隔的调度策略不需要初始化权重,因为无历史记录的账号会自动返回DateTime.MinValue
  404. /// </summary>
  405. public static void InitializeNewAccountUsage(int accountId, string groupKey = "default")
  406. {
  407. try
  408. {
  409. var today = DateTime.Now.ToString("yyyyMMdd");
  410. string usageKey = $"pdd_daily_usage:{accountId}:{today}";
  411. // 检查该账号今天是否已有使用记录
  412. var currentUsage = RedisHelper.Get<int>(usageKey);
  413. if (currentUsage > 0)
  414. {
  415. Console.WriteLine($"Account {accountId} already has usage record: {currentUsage}, skip initialization");
  416. return; // 已有记录,不需要初始化
  417. }
  418. // 获取所有在线账号的平均值(仅用于降级策略)
  419. var activeAccounts = List()?.Where(a => a.status && a.enable_parse && a.id != accountId).ToList();
  420. if (activeAccounts == null || !activeAccounts.Any())
  421. {
  422. Console.WriteLine($"No other active accounts found, account {accountId} will use default value");
  423. return; // 没有其他账号,使用默认值0即可
  424. }
  425. // 初始化 daily_usage(用于降级策略的统计)
  426. var totalUsage = 0;
  427. foreach (var account in activeAccounts)
  428. {
  429. totalUsage += GetCurrentDayUsageFromRedis(account.id);
  430. }
  431. var averageUsage = totalUsage / activeAccounts.Count;
  432. if (averageUsage > 0)
  433. {
  434. RedisHelper.Set(usageKey, averageUsage, 86400);
  435. Console.WriteLine($"Initialized account {accountId} daily_usage with average: {averageUsage}");
  436. }
  437. Console.WriteLine($"Account {accountId} initialization complete. Time-based scheduling will use DateTime.MinValue for idle time calculation.");
  438. }
  439. catch (Exception ex)
  440. {
  441. Console.WriteLine($"Failed to initialize new account for {accountId}: {ex.Message}");
  442. }
  443. }
  444. /// <summary>
  445. /// 获取当前统计信息(用于监控和调试)- 改为从Redis获取
  446. /// </summary>
  447. internal static Dictionary<string, int> GetCurrentUsageSnapshot()
  448. {
  449. try
  450. {
  451. var result = new Dictionary<string, int>();
  452. var accounts = List()?.Where(a => a.status && a.enable_parse).ToList();
  453. if (accounts == null || !accounts.Any()) return result;
  454. var today = DateTime.Now.ToString("yyyyMMdd");
  455. foreach (var account in accounts)
  456. {
  457. string key = $"pdd_daily_usage:{account.id}:{today}";
  458. int usage = RedisHelper.Get<int>(key);
  459. result[$"Account_{account.id}_{account.name}"] = usage;
  460. }
  461. return result;
  462. }
  463. catch (Exception)
  464. {
  465. return new Dictionary<string, int>();
  466. }
  467. }
  468. /// <summary>
  469. /// 获取账号调度时间间隔信息(用于监控和调试)
  470. /// </summary>
  471. public static Dictionary<string, object> GetScheduleWeightSnapshot(string groupKey = "default")
  472. {
  473. try
  474. {
  475. var result = new Dictionary<string, object>();
  476. var accounts = List()?.Where(a => a.status && a.enable_parse).ToList();
  477. if (accounts == null || !accounts.Any()) return result;
  478. var now = DateTime.Now;
  479. var accountInfos = new List<Dictionary<string, object>>();
  480. foreach (var account in accounts)
  481. {
  482. var lastUsedTime = GetAccountLastUsedTime(account.id, groupKey);
  483. long idleMs = (long)(now - lastUsedTime).TotalMilliseconds;
  484. bool canUse = account.cis_limit <= 0 || idleMs >= account.cis_limit;
  485. var info = new Dictionary<string, object>
  486. {
  487. ["account_id"] = account.id,
  488. ["account_name"] = account.name,
  489. ["cis_limit_ms"] = account.cis_limit,
  490. ["last_used_time"] = lastUsedTime == DateTime.MinValue ? "从未使用" : lastUsedTime.ToString("yyyy-MM-dd HH:mm:ss.fff"),
  491. ["idle_time_ms"] = idleMs,
  492. ["idle_time_seconds"] = Math.Round(idleMs / 1000.0, 2),
  493. ["can_use"] = canUse,
  494. ["daily_usage"] = GetCurrentDayUsageFromRedis(account.id),
  495. ["daily_limit"] = account.daily_calls_limit
  496. };
  497. accountInfos.Add(info);
  498. }
  499. // 按照空闲时间降序排序(与选择逻辑一致)
  500. accountInfos = accountInfos.OrderByDescending(x => (long)x["idle_time_ms"]).ToList();
  501. result["accounts"] = accountInfos;
  502. result["group_key"] = groupKey;
  503. result["timestamp"] = now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  504. result["total_accounts"] = accountInfos.Count;
  505. result["available_accounts"] = accountInfos.Count(x => (bool)x["can_use"]);
  506. return result;
  507. }
  508. catch (Exception ex)
  509. {
  510. return new Dictionary<string, object> { ["error"] = ex.Message };
  511. }
  512. }
  513. /// <summary>
  514. /// 强制清理统计数据(仅用于测试或紧急情况)- Redis版本
  515. /// </summary>
  516. internal static void ForceClearStats()
  517. {
  518. try
  519. {
  520. var today = DateTime.Now.ToString("yyyyMMdd");
  521. var pattern = $"pdd_daily_usage:*:{today}";
  522. // TODO: 实现Redis批量删除相关keys
  523. // 生产环境中需要谨慎使用
  524. Console.WriteLine("Force clear stats - Redis keys will auto-expire in 24h");
  525. }
  526. catch (Exception ex)
  527. {
  528. Console.WriteLine($"Failed to force clear stats: {ex.Message}");
  529. }
  530. }
  531. internal static void TempSuspend(int accountId)
  532. {
  533. _suspend.AddOrUpdate(accountId, DateTime.Now, (key, oldValue) => DateTime.Now);
  534. }
  535. private static bool IsMatch(PddPoolDTO item, PddUnionWorkMode mode, int accountid)
  536. {
  537. if (accountid != 0 && accountid != item.id) return false;
  538. if (!item.enable_parse) return false;
  539. if (mode != PddUnionWorkMode.All && mode != item.work_mode) return false;
  540. if (item.work_mode == PddUnionWorkMode.Crawler && !item.cookie_status) return false;
  541. if (!string.IsNullOrEmpty(_end_point) && !string.IsNullOrEmpty(item.end_point))
  542. {
  543. if (item.end_point != _end_point) return false;
  544. }
  545. // 使用初始化参数创建工作时间表
  546. if (!new WorkSchedule(item.time_range).IsWorkHour()) return false;
  547. if (_suspend.TryGetValue(accountid, out DateTime suspendTime))
  548. {
  549. var ts = DateTime.Now - suspendTime;
  550. if (ts.TotalSeconds < 70) return false;
  551. }
  552. // cis_limit 检查已移至 SmoothWeightedRoundRobin 方法中基于时间间隔判断
  553. // 这里不再需要检查 Redis 锁
  554. #if DEBUG
  555. #else
  556. if (item.rpm_limit > 0)
  557. {
  558. int rpm_num = RiskControlCore.GetCalls(TkChannelEnum.pdd, item.id, DateTime.Now.ToString("yyyyMMddHHmm"));
  559. if (rpm_num >= item.rpm_limit) return false;
  560. }
  561. if (item.daily_calls_limit > 0)
  562. {
  563. int daily_num = RiskControlCore.GetCalls(TkChannelEnum.pdd, item.id, DateTime.Now.ToString("yyyyMMdd"));
  564. if (daily_num >= item.daily_calls_limit) return false;
  565. }
  566. if (item.hourly_calls_limit > 0)
  567. {
  568. int hourly_num = RiskControlCore.GetCalls(TkChannelEnum.pdd, item.id, DateTime.Now.ToString("yyyyMMddHH"));
  569. if (hourly_num >= item.hourly_calls_limit) return false;
  570. }
  571. #endif
  572. return true;
  573. }
  574. public static async Task<IEnumerable<PddPoolDTO>> AllListAsync(bool force = false)
  575. {
  576. if (!force && _all_cached != default) return _all_cached;
  577. try
  578. {
  579. await _semaphore.WaitAsync();
  580. string cache_key = $"cache:all_pdd_pool";
  581. var list = await RedisKit.GetAsync<IEnumerable<PddPoolDTO>>(cache_key);
  582. if (force || list == default)
  583. {
  584. list = new DBContext.Table("pdd_pool").Select<PddPoolDTO>();
  585. if (list == null) return default;
  586. // 等待 Redis 写入完成
  587. await RedisKit.SetAsync(cache_key, list, 30 * 86400);
  588. }
  589. _all_cached = list;
  590. return list;
  591. }
  592. catch (Exception)
  593. {
  594. // 发生异常时返回上一次的缓存,如果没有则返回默认值
  595. return _all_cached ?? default;
  596. }
  597. finally
  598. {
  599. _semaphore.Release();
  600. }
  601. }
  602. public static IEnumerable<PddPoolDTO> List(bool force = false)
  603. {
  604. if (!force && _cached != null) return _cached;
  605. string cache_key = $"cache:pdd_pool";
  606. var list = RedisHelper.Get<IEnumerable<PddPoolDTO>>(cache_key);
  607. if (force || list == null)
  608. {
  609. lock (_lockObj)
  610. {
  611. list = new DBContext.Table("pdd_pool")
  612. .Where("status=@status", new { status = 1 })
  613. .Select<PddPoolDTO>();
  614. if (list == null) return default;
  615. foreach (var item in list)
  616. {
  617. RiskControlCore.SetCallsAsync(TkChannelEnum.pdd, item.id, DateTime.Now.ToString("yyyyMMddHH"), item.current_hourly_calls);
  618. RiskControlCore.SetCallsAsync(TkChannelEnum.pdd, item.id, DateTime.Now.ToString("yyyyMMdd"), item.current_daily_calls);
  619. }
  620. RedisHelper.Set(cache_key, list, 30 * 86400);
  621. }
  622. }
  623. _cached = list;
  624. return list;
  625. }
  626. public static void Refresh()
  627. {
  628. _cached = null;
  629. _all_cached = null;
  630. _ = List(true);
  631. _ = AllListAsync(true);
  632. }
  633. public static void Disabled(string name)
  634. {
  635. string cache_key = $"cache:pdd_pool:{name}:disabled";
  636. long count = RedisHelper.IncrBy(cache_key);
  637. RedisHelper.Expire(cache_key, 10);
  638. if (count > 1) return;
  639. new DBContext.Table("pdd_pool")
  640. .Add("status", 0)
  641. .Where("name=@name", new { name })
  642. .Update();
  643. _ = List(true);
  644. NotifyCore.Notify(new NifyMessage
  645. {
  646. message = $"【多多:{name}】调用异常",
  647. priority = NifyMessagePriority.high,
  648. tags = ["red_circle"]
  649. });
  650. }
  651. public static void Disabled(int accountId, string name, string content)
  652. {
  653. string cache_key = $"cache:pdd_pool:{name}:disabled";
  654. long count = RedisHelper.IncrBy(cache_key);
  655. RedisHelper.Expire(cache_key, 10);
  656. if (count > 1) return;
  657. var update = new DBContext.Table("pdd_pool").Add("status", 0);
  658. if (accountId > 0)
  659. {
  660. update.Where("id=@accountId", new { accountId }).Update();
  661. }
  662. else
  663. {
  664. update.Where("name=@name", new { name }).Update();
  665. }
  666. _ = List(true);
  667. NotifyCore.Notify(new NifyMessage
  668. {
  669. message = $"【多多{accountId}:{name}】cookie 掉线\n\n{content}",
  670. priority = NifyMessagePriority.high,
  671. tags = ["red_circle"]
  672. });
  673. //NotifyCore.QYWeixinPushNotifyAsync("掉线", $"【多多{accountId}:{name}】cookie 掉线");
  674. EndPointCore.NotifyReload(true);
  675. }
  676. internal static void AccountExhausted()
  677. {
  678. string cache_key = $"cache:pdd_pool:account:exhausted";
  679. long count = RedisHelper.IncrBy(cache_key);
  680. if (count > 1) return;
  681. RedisHelper.Expire(cache_key, 3600);
  682. NotifyCore.Notify(new NifyMessage
  683. {
  684. message = $"【多多】没有匹配账号",
  685. priority = NifyMessagePriority.high,
  686. tags = ["red_circle"]
  687. });
  688. //_ = NotifyCore.QYWeixinPushNotifyAsync("没账号", $"【多多】没有匹配账号");
  689. }
  690. internal static async Task<JsonElement> GetUserInfo(string cookies)
  691. {
  692. JsonElement result = default;
  693. try
  694. {
  695. string url = "https://jinbao.pinduoduo.com/network/api/account/userInfo";
  696. WebClientUtility client = new WebClientUtility();
  697. client.Post("{}");
  698. client.SetCookies(cookies);
  699. client.SetContentType("application/json");
  700. var response = await client.RequestAsync(url, "POST");
  701. var body = response.Body();
  702. result = body.Convert2JsonElement();
  703. }
  704. catch (Exception ex)
  705. {
  706. }
  707. return result;
  708. }
  709. public static async Task<int> UpdateCookies(string cookies, string user_agent, int id = 0)
  710. {
  711. if (string.IsNullOrEmpty(cookies)) return 0;
  712. var userinfo = await GetUserInfo(cookies);
  713. if (userinfo.ValueKind != JsonValueKind.Object) return 0;
  714. int duoId = userinfo.PathRead<int>("result.duoId", 0);
  715. string company = userinfo.PathRead<string>("result.mobile", string.Empty);
  716. string lastPid = userinfo.PathRead<string>("result.lastPid", string.Empty);
  717. int accountId = 0;
  718. if (duoId == 0 && id == 0) return 0;
  719. string filter = id != 0 ? "id=@id" : "duoId=@duoId";
  720. var exist = new DBContext.Table("pdd_pool").Get<PddPoolDTO>(filter, new { id, duoId });
  721. if (exist != null)
  722. {
  723. var status = exist.status;
  724. var work_mode = exist.work_mode;
  725. accountId = exist.id;
  726. if (work_mode == PddUnionWorkMode.Crawler) status = true;
  727. new DBContext.Table("pdd_pool")
  728. .Add("cookies", cookies)
  729. .Add("user_agent", user_agent)
  730. .Add("status", status)
  731. .Add("cookie_status", 1)
  732. .Add("last_time", DateTime.Now)
  733. .Add("login_time", DateTime.Now)
  734. .Where("id=@id", new { exist.id })
  735. .Update();
  736. if (status)
  737. {
  738. _ = List(true);
  739. // 为重新上线的账号初始化使用次数和权重(针对所有分组)
  740. InitializeNewAccountUsage(exist.id, "default");
  741. InitializeNewAccountUsage(exist.id, "dp");
  742. }
  743. }
  744. else
  745. {
  746. accountId = new DBContext.Table("pdd_pool")
  747. .Add("duoId", duoId)
  748. .Add("name", company)
  749. .Add("company", company)
  750. .Add("description", "由cookies上报创建此记录")
  751. .Add("cookies", cookies)
  752. .Add("user_agent", user_agent)
  753. .Add("pid", lastPid)
  754. .Add("cookie_status", 1)
  755. .Add("create_time", DateTime.Now)
  756. .Add("last_time", DateTime.Now)
  757. .Add("login_time", DateTime.Now)
  758. .Add("status", 0)
  759. .Create();
  760. // 为新创建的账号初始化使用次数(如果将来会启用的话)
  761. // InitializeNewAccountUsage(accountId); // 暂不调用,因为新创建的账号status=0
  762. }
  763. NotifyCore.Notify(new NifyMessage
  764. {
  765. message = $"【拼多多{accountId}:{company}】cookie 上线",
  766. tags = ["green_circle"]
  767. });
  768. EndPointCore.NotifyReload(true);
  769. //NotifyCore.QYWeixinPushNotifyAsync("上线", $"【淘宝联盟:{dnk}】cookie 上报更新");
  770. return accountId;
  771. }
  772. public static int CookieDisabled(int id)
  773. {
  774. return new DBContext.Table("pdd_pool")
  775. .Add("cookie_status", 0)
  776. .Add("last_time", DateTime.Now)
  777. .Where("id=@id", new { id })
  778. .Update();
  779. }
  780. public static int Update(PddPoolDTO account)
  781. {
  782. return new DBContext.Table("pdd_pool")
  783. .Add("current_hourly_calls", account.current_hourly_calls)
  784. .Add("current_daily_calls", account.current_daily_calls)
  785. //.Add("today_clickNum", account.today_clickNum)
  786. //.Add("today_cosFee", account.today_cosFee)
  787. //.Add("today_cosPrice", account.today_cosPrice)
  788. //.Add("today_finishCosFee", account.today_finishCosFee)
  789. //.Add("today_finishCosPrice", account.today_finishCosPrice)
  790. //.Add("today_finishOrderNum", account.today_finishOrderNum)
  791. //.Add("today_orderNum", account.today_orderNum)
  792. .Add("last_time", DateTime.Now)
  793. .Where("id=@id", new { account.id })
  794. .Update();
  795. }
  796. public static void AccountRisk(PddPoolDTO account)
  797. {
  798. string cache_key = $"cache:pdd_pool:{account.name}:disabled";
  799. long count = RedisHelper.IncrBy(cache_key);
  800. RedisHelper.Expire(cache_key, 10);
  801. if (count > 1) return;
  802. // 发送通知
  803. string message = $"【PDD风控】【{account.id}:{account.name}】出现30007";
  804. NotifyCore.Notify(new NifyMessage
  805. {
  806. message = message,
  807. priority = NifyMessagePriority.high,
  808. tags = ["red_circle"]
  809. });
  810. if (!account.is_hide)
  811. {
  812. _ = NotifyCore.QYWeixinPushNotifyAsync("PDD风控", message);
  813. }
  814. // 设置风控标记
  815. RiskControlCore.SetRiskCookie($"pdd:{account.id}", "30007");
  816. // 更新数据库状态
  817. new DBContext.Table("pdd_pool")
  818. .Add("enable_parse", 0)
  819. .Where("id=@id", new { account.id })
  820. .Update();
  821. _ = List(true);
  822. _ = EndPointCore.NotifyReload(true);
  823. }
  824. }
  825. }