PddPoolCore.cs 35 KB

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