AutoJob.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use App\Components\ServerChan;
  5. use App\Http\Models\Coupon;
  6. use App\Http\Models\CouponLog;
  7. use App\Http\Models\EmailLog;
  8. use App\Http\Models\Invite;
  9. use App\Http\Models\Order;
  10. use App\Http\Models\Payment;
  11. use App\Http\Models\Config;
  12. use App\Http\Models\SsNode;
  13. use App\Http\Models\SsNodeInfo;
  14. use App\Http\Models\User;
  15. use App\Http\Models\UserLabel;
  16. use App\Http\Models\UserBanLog;
  17. use App\Http\Models\UserSubscribe;
  18. use App\Http\Models\UserSubscribeLog;
  19. use App\Http\Models\UserTrafficHourly;
  20. use App\Mail\nodeCrashWarning;
  21. use Cache;
  22. use Mail;
  23. use Log;
  24. use DB;
  25. class AutoJob extends Command
  26. {
  27. protected $signature = 'autoJob';
  28. protected $description = '自动化任务';
  29. protected $ssrCheckCacheKey = 'ssr_check_warning_';
  30. protected $serverCheckCacheKey = 'server_check_warning_';
  31. protected static $config;
  32. public function __construct()
  33. {
  34. parent::__construct();
  35. self::$config = $this->systemConfig();
  36. }
  37. /*
  38. * 以下操作顺序如果随意挪动可能导致出现异常
  39. */
  40. public function handle()
  41. {
  42. $jobStartTime = microtime(true);
  43. // SSR(R)被启用说明用户购买了流量,需要重置ban_time,防止异常
  44. $this->resetBantime();
  45. // 优惠券到期自动置无效
  46. $this->expireCoupon();
  47. // 邀请码到期自动置无效
  48. $this->exipreInvite();
  49. // 封禁访问异常的订阅链接
  50. $this->blockSubscribe();
  51. // 封禁账号
  52. $this->blockUsers();
  53. // 自动移除被封禁账号的标签
  54. $this->removeUserLabels();
  55. // 解封被临时封禁的账号(ban_time > 0)
  56. $this->unblockUsers();
  57. // 端口回收与分配
  58. $this->dispatchPort();
  59. // 关闭超时未支付订单
  60. $this->closeOrder();
  61. // 监测节点状态
  62. $this->checkNode();
  63. $jobEndTime = microtime(true);
  64. $jobUsedTime = round(($jobEndTime - $jobStartTime), 4);
  65. Log::info('执行定时任务【' . $this->description . '】,耗时' . $jobUsedTime . '秒');
  66. }
  67. // 重置ban_time
  68. private function resetBantime()
  69. {
  70. User::query()->where('enable', 1)->where('ban_time', -1)->update(['ban_time' => 0]);
  71. }
  72. // 优惠券到期自动置无效
  73. private function expireCoupon()
  74. {
  75. $couponList = Coupon::query()->where('status', 0)->where('available_end', '<=', time())->get();
  76. if (!$couponList->isEmpty()) {
  77. foreach ($couponList as $coupon) {
  78. Coupon::query()->where('id', $coupon->id)->update(['status' => 2]);
  79. }
  80. }
  81. }
  82. // 邀请码到期自动置无效
  83. private function exipreInvite()
  84. {
  85. $inviteList = Invite::query()->where('status', 0)->where('dateline', '<=', date('Y-m-d H:i:s'))->get();
  86. if (!$inviteList->isEmpty()) {
  87. foreach ($inviteList as $invite) {
  88. Invite::query()->where('id', $invite->id)->update(['status' => 2]);
  89. }
  90. }
  91. }
  92. // 封禁访问异常的订阅链接
  93. private function blockSubscribe()
  94. {
  95. if (self::$config['is_subscribe_ban']) {
  96. $subscribeList = UserSubscribe::query()->where('status', 1)->get();
  97. if (!$subscribeList->isEmpty()) {
  98. foreach ($subscribeList as $subscribe) {
  99. // 24小时内不同IP的请求次数
  100. $request_times = UserSubscribeLog::query()->where('sid', $subscribe->id)->where('request_time', '>=', date("Y-m-d H:i:s", strtotime("-24 hours")))->distinct('request_ip')->count('request_ip');
  101. if ($request_times >= self::$config['subscribe_ban_times']) {
  102. UserSubscribe::query()->where('id', $subscribe->id)->update(['status' => 0, 'ban_time' => time(), 'ban_desc' => '存在异常,自动封禁']);
  103. // 记录封禁日志
  104. $this->addUserBanLog($subscribe->user_id, 0, '【完全封禁订阅】-订阅24小时内请求异常');
  105. }
  106. }
  107. }
  108. }
  109. }
  110. // 封禁账号
  111. private function blockUsers()
  112. {
  113. // 封禁24小时内流量异常账号
  114. if (self::$config['is_traffic_ban']) {
  115. $userList = User::query()->where('status', '>=', 0)->where('enable', 1)->where('ban_time', '>=', 0)->get();
  116. if (!$userList->isEmpty()) {
  117. foreach ($userList as $user) {
  118. $time = date('Y-m-d H:i:s', time() - 24 * 60 * 60);
  119. $totalTraffic = UserTrafficHourly::query()->where('user_id', $user->id)->where('node_id', 0)->where('created_at', '>=', $time)->sum('total');
  120. if ($totalTraffic >= (self::$config['traffic_ban_value'] * 1024 * 1024 * 1024)) {
  121. $ban_time = strtotime(date('Y-m-d H:i:s', strtotime("+" . self::$config['traffic_ban_time'] . " minutes")));
  122. User::query()->where('id', $user->id)->update(['enable' => 0, 'ban_time' => $ban_time]);
  123. // 写入日志
  124. $this->addUserBanLog($user->id, self::$config['traffic_ban_time'], '【临时封禁代理】-24小时内流量异常');
  125. }
  126. }
  127. }
  128. }
  129. // 禁用流量超限用户
  130. $userList = User::query()->where('enable', 1)->whereRaw("u + d >= transfer_enable")->get();
  131. if (!$userList->isEmpty()) {
  132. foreach ($userList as $user) {
  133. User::query()->where('id', $user->id)->update(['enable' => 0, 'ban_time' => -1]);
  134. // 写入日志
  135. $this->addUserBanLog($user->id, 0, '【完全封禁代理】-流量已用完');
  136. }
  137. }
  138. // 自动禁用过期用户
  139. $userList = User::query()->where('enable', 1)->where('expire_time', '<=', date('Y-m-d'))->get();
  140. if (!$userList->isEmpty()) {
  141. foreach ($userList as $user) {
  142. if (self::$config['is_ban_status']) {
  143. User::query()->where('id', $user->id)->update(['enable' => 0, 'status' => -1, 'ban_time' => -1]);
  144. $this->addUserBanLog($user->id, 0, '【完全封禁账号及代理】-账号已过期');
  145. } else {
  146. User::query()->where('id', $user->id)->update(['enable' => 0, 'ban_time' => -1]);
  147. $this->addUserBanLog($user->id, 0, '【完全封禁代理】-账号已过期');
  148. }
  149. }
  150. }
  151. }
  152. // 自动移除被封禁账号的标签
  153. private function removeUserLabels()
  154. {
  155. $userList = User::query()->where('enable', 0)->where('ban_time', -1)->get();
  156. if (!$userList->isEmpty()) {
  157. foreach ($userList as $user) {
  158. UserLabel::query()->where('user_id', $user->id)->delete();
  159. }
  160. }
  161. }
  162. // 解封被临时封禁的账号(ban_time > 0)
  163. private function unblockUsers()
  164. {
  165. $userList = User::query()->where('status', '>=', 0)->where('ban_time', '>', 0)->get();
  166. foreach ($userList as $user) {
  167. if ($user->ban_time < time()) {
  168. User::query()->where('id', $user->id)->update(['enable' => 1, 'ban_time' => 0]);
  169. // 写入操作日志
  170. $this->addUserBanLog($user->id, 0, '【自动解封】-封禁到期');
  171. }
  172. }
  173. // 用户购买了流量(可用大于已用)也解封
  174. $userList = User::query()->where('status', '>=', 0)->where('enable', 0)->where('ban_time', -1)->whereRaw("u + d < transfer_enable")->get();
  175. if (!$userList->isEmpty()) {
  176. foreach ($userList as $user) {
  177. User::query()->where('id', $user->id)->update(['enable' => 1, 'ban_time' => 0]);
  178. // 写入操作日志
  179. $this->addUserBanLog($user->id, 0, '【自动解封】-有流量解封');
  180. }
  181. }
  182. }
  183. // 端口回收与分配
  184. private function dispatchPort()
  185. {
  186. // 自动分配端口
  187. if (self::$config['auto_release_port']) {
  188. $userList = User::query()->where('status', '>=', 0)->where('enable', 1)->where('port', 0)->get();
  189. if (!$userList->isEmpty()) {
  190. foreach ($userList as $user) {
  191. $port = self::$config['is_rand_port'] ? $this->getRandPort() : $this->getOnlyPort();
  192. User::query()->where('id', $user->id)->update(['port' => $port]);
  193. }
  194. }
  195. }
  196. // 被封禁账号自动释放端口
  197. if (self::$config['auto_release_port']) {
  198. $userList = User::query()->where('enable', 0)->where('ban_time', -1)->get();
  199. if (!$userList->isEmpty()) {
  200. foreach ($userList as $user) {
  201. if ($user->port) {
  202. User::query()->where('id', $user->id)->update(['port' => 0]);
  203. }
  204. }
  205. }
  206. }
  207. }
  208. // 自动关闭超时未支付订单
  209. private function closeOrder()
  210. {
  211. // 自动关闭超时未支付的有赞云订单(有赞云收款二维码超过60分钟自动关闭,我们限制30分钟内必须付款)
  212. $paymentList = Payment::query()->with(['order', 'order.coupon'])->where('status', 0)->where('created_at', '<=', date("Y-m-d H:i:s", strtotime("-30 minutes")))->get();
  213. if (!$paymentList->isEmpty()) {
  214. DB::beginTransaction();
  215. try {
  216. foreach ($paymentList as $payment) {
  217. // 关闭支付单
  218. Payment::query()->where('id', $payment->id)->update(['status' => -1]);
  219. // 关闭订单
  220. Order::query()->where('oid', $payment->oid)->update(['status' => -1]);
  221. // 退回优惠券
  222. if ($payment->order->coupon_id) {
  223. Coupon::query()->where('id', $payment->order->coupon_id)->update(['status' => 0]);
  224. $this->addCouponLog($payment->order->coupon_id, $payment->order->goods_id, $payment->oid, '订单超时未支付,自动退回');
  225. }
  226. }
  227. DB::commit();
  228. } catch (\Exception $e) {
  229. Log::info('【异常】自动关闭超时未支付订单:' . $e->getMessage());
  230. DB::rollBack();
  231. }
  232. }
  233. }
  234. // 监测节点状态 TODO:需要改进,否则curl请求时间超长
  235. private function checkNode()
  236. {
  237. $title = "节点异常警告";
  238. $nodeList = SsNode::query()->where('status', 1)->get();
  239. foreach ($nodeList as $node) {
  240. // TCP检测
  241. $tcpCheck = $this->tcpCheck($node->ip);
  242. if (false !== $tcpCheck && $tcpCheck) {
  243. // 10分钟内已发警告,则不再发
  244. if (Cache::has($this->ssrCheckCacheKey . $node->id)) {
  245. continue;
  246. }
  247. $content = '节点无异常';
  248. if ($tcpCheck === 1) {
  249. $content = "节点**{$node->name}【{$node->ip}】**异常:**宕机**";
  250. } else if ($tcpCheck === 2) {
  251. $content = "节点**{$node->name}【{$node->ip}】**异常:**海外不通**";
  252. } else if ($tcpCheck === 3) {
  253. $content = "节点**{$node->name}【{$node->ip}】**异常:**TCP阻断**";
  254. }
  255. // 通知管理员
  256. $this->notifyMaster($title, $content, $node->name, $node->server);
  257. // 写入发信缓存
  258. Cache::put($this->ssrCheckCacheKey . $node->id, $node->name . '(' . $node->server . ')', 10);
  259. }
  260. // 10分钟内无节点负载信息且TCP检测认为不是宕机则认为是SSR(R)后端炸了
  261. $node_info = SsNodeInfo::query()->where('node_id', $node->id)->where('log_time', '>=', strtotime("-10 minutes"))->orderBy('id', 'desc')->first();
  262. if ($tcpCheck !== 1 && (empty($node_info) || empty($node_info->load))) {
  263. // 10分钟内已发警告,则不再发
  264. if (Cache::has($this->serverCheckCacheKey . $node->id)) {
  265. continue;
  266. }
  267. $content = "节点**{$node->name}【{$node->ip}】**异常:**心跳异常**";
  268. // 通知管理员
  269. $this->notifyMaster($title, $content, $node->name, $node->server);
  270. // 写入发信缓存
  271. Cache::put($this->serverCheckCacheKey . $node->id, $node->name . '(' . $node->server . ')', 10);
  272. }
  273. }
  274. }
  275. // TCP检测
  276. private function tcpCheck($ip)
  277. {
  278. try {
  279. $tcpCN = $this->curlRequest("https://ipcheck.need.sh/api.php?location=cn&ip={$ip}&type=tcp");
  280. $tcpUS = $this->curlRequest("https://ipcheck.need.sh/api.php?location=us&ip={$ip}&type=tcp");
  281. if ($tcpCN['result'] != 'success' && $tcpUS['result'] != 'success') {
  282. throw new \Exception("节点监测探测接口异常");
  283. }
  284. if (!$tcpCN['alive'] && !$tcpUS['alive']) {
  285. return 1; // 中美都不通,服务器宕机
  286. } else if ($tcpCN['alive'] && !$tcpUS['alive']) {
  287. return 2; // 中通美不通,无法出国,可能是安全组策略限制(例如:阿里云、腾讯云)
  288. } else if (!$tcpCN['alive'] && $tcpUS['alive']) {
  289. return 3; // 美通中不通,说明被墙进行TCP阻断
  290. } else {
  291. return 0; // 正常
  292. }
  293. } catch (\Exception $e) {
  294. Log::error('节点监测请求失败:' . $e);
  295. return false;
  296. }
  297. }
  298. // 检测服务
  299. // private function checkHost()
  300. // {
  301. // $data =
  302. // $header = [
  303. // 'Content-Type: application/json',
  304. // 'Content-Length: ' . strlen($data)
  305. // ];
  306. // }
  307. /**
  308. * 通知管理员
  309. *
  310. * @param string $title 消息标题
  311. * @param string $content 消息内容
  312. * @param string $nodeName 节点名称
  313. * @param string $nodeServer 节点域名
  314. */
  315. private function notifyMaster($title, $content, $nodeName, $nodeServer)
  316. {
  317. $this->notifyMasterByEmail($title, $content, $nodeName, $nodeServer);
  318. $this->notifyMasterByServerchan($title, $content);
  319. }
  320. /**
  321. * 发邮件通知管理员
  322. *
  323. * @param string $title 消息标题
  324. * @param string $content 消息内容
  325. * @param string $nodeName 节点名称
  326. * @param string $nodeServer 节点域名
  327. */
  328. private function notifyMasterByEmail($title, $content, $nodeName, $nodeServer)
  329. {
  330. if (self::$config['is_node_crash_warning'] && self::$config['crash_warning_email']) {
  331. try {
  332. Mail::to(self::$config['crash_warning_email'])->send(new nodeCrashWarning(self::$config['website_name'], $nodeName, $nodeServer));
  333. $this->addEmailLog(1, $title, $content);
  334. } catch (\Exception $e) {
  335. $this->addEmailLog(1, $title, $content, 0, $e->getMessage());
  336. }
  337. }
  338. }
  339. /**
  340. * 通过ServerChan发微信消息提醒管理员
  341. *
  342. * @param string $title 消息标题
  343. * @param string $content 消息内容
  344. */
  345. private function notifyMasterByServerchan($title, $content)
  346. {
  347. if (self::$config['is_server_chan'] && self::$config['server_chan_key']) {
  348. $serverChan = new ServerChan();
  349. $serverChan->send($title, $content);
  350. }
  351. }
  352. /**
  353. * 添加用户封禁日志
  354. *
  355. * @param int $userId 用户ID
  356. * @param int $minutes 封禁时长,单位分钟
  357. * @param string $desc 封禁理由
  358. */
  359. private function addUserBanLog($userId, $minutes, $desc)
  360. {
  361. $log = new UserBanLog();
  362. $log->user_id = $userId;
  363. $log->minutes = $minutes;
  364. $log->desc = $desc;
  365. $log->save();
  366. }
  367. /**
  368. * 添加邮件发送日志
  369. *
  370. * @param int $user_id 接收者用户ID
  371. * @param string $title 标题
  372. * @param string $content 内容
  373. * @param int $status 投递状态
  374. * @param string $error 投递失败时记录的异常信息
  375. */
  376. private function addEmailLog($userId, $title, $content, $status = 1, $error = '')
  377. {
  378. $emailLogObj = new EmailLog();
  379. $emailLogObj->user_id = $userId;
  380. $emailLogObj->title = $title;
  381. $emailLogObj->content = $content;
  382. $emailLogObj->status = $status;
  383. $emailLogObj->error = $error;
  384. $emailLogObj->created_at = date('Y-m-d H:i:s');
  385. $emailLogObj->save();
  386. }
  387. /**
  388. * 添加优惠券操作日志
  389. *
  390. * @param int $couponId 优惠券ID
  391. * @param int $goodsId 商品ID
  392. * @param int $orderId 订单ID
  393. * @param string $desc 备注
  394. */
  395. private function addCouponLog($couponId, $goodsId, $orderId, $desc = '')
  396. {
  397. $couponLog = new CouponLog();
  398. $couponLog->coupon_id = $couponId;
  399. $couponLog->goods_id = $goodsId;
  400. $couponLog->order_id = $orderId;
  401. $couponLog->desc = $desc;
  402. $couponLog->save();
  403. }
  404. // 系统配置
  405. private function systemConfig()
  406. {
  407. $config = Config::query()->get();
  408. $data = [];
  409. foreach ($config as $vo) {
  410. $data[$vo->name] = $vo->value;
  411. }
  412. return $data;
  413. }
  414. // 获取一个随机端口
  415. public function getRandPort()
  416. {
  417. $config = $this->systemConfig();
  418. $port = mt_rand($config['min_port'], $config['max_port']);
  419. $deny_port = [1068, 1109, 1434, 3127, 3128, 3129, 3130, 3332, 4444, 5554, 6669, 8080, 8081, 8082, 8181, 8282, 9996, 17185, 24554, 35601, 60177, 60179]; // 不生成的端口
  420. $exists_port = User::query()->pluck('port')->toArray();
  421. if (in_array($port, $exists_port) || in_array($port, $deny_port)) {
  422. $port = $this->getRandPort();
  423. }
  424. return $port;
  425. }
  426. // 获取一个端口
  427. public function getOnlyPort()
  428. {
  429. $config = $this->systemConfig();
  430. $port = $config['min_port'];
  431. $deny_port = [1068, 1109, 1434, 3127, 3128, 3129, 3130, 3332, 4444, 5554, 6669, 8080, 8081, 8082, 8181, 8282, 9996, 17185, 24554, 35601, 60177, 60179]; // 不生成的端口
  432. $exists_port = User::query()->where('port', '>=', $config['min_port'])->where('port', '<=', $config['max_port'])->pluck('port')->toArray();
  433. while (in_array($port, $exists_port) || in_array($port, $deny_port)) {
  434. $port = $port + 1;
  435. }
  436. return $port;
  437. }
  438. /**
  439. * 发起一个CURL请求
  440. *
  441. * @param string $url
  442. * @param array $data
  443. * @param array $header
  444. *
  445. * @return mixed
  446. */
  447. private function curlRequest($url, $data = [], $header = [])
  448. {
  449. $ch = curl_init();
  450. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  451. curl_setopt($ch, CURLOPT_TIMEOUT, 500);
  452. // 为保证第三方服务器与微信服务器之间数据传输的安全性,所有微信接口采用https方式调用,必须使用下面2行代码打开ssl安全校验。
  453. // 如果在部署过程中代码在此处验证失败,请到 http://curl.haxx.se/ca/cacert.pem 下载新的证书判别文件。
  454. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  455. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  456. curl_setopt($ch, CURLOPT_URL, $url);
  457. // 如果设置了header
  458. if ($header) {
  459. // $header = [
  460. // 'Content-Type: application/json',
  461. // 'Content-Length: ' . strlen($data)
  462. // ];
  463. // $header = [
  464. // 'Content-Type: text/xml; charset=utf-8',
  465. // 'Content-Length: ' . strlen($data)
  466. // ];
  467. // $header = [
  468. // 'Content-type: text/plain',
  469. // 'Content-Length: ' . strlen($data)
  470. // ];
  471. curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
  472. }
  473. // 如果data有数据,则用POST请求
  474. if ($data) {
  475. curl_setopt($ch, CURLOPT_POST, 1);
  476. curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  477. }
  478. $result = curl_exec($ch);
  479. curl_close($ch);
  480. return json_decode($result, JSON_OBJECT_AS_ARRAY);
  481. }
  482. }