<?php namespace addons\crontab\controller; use addons\crontab\model\Crontab; use Cron\CronExpression; use fast\Http; use think\Controller; use think\Db; use think\Exception; use think\Log; /** * 定时任务接口 * * 以Crontab方式每分钟定时执行,且只可以Cli方式运行 * @internal */ class Autotask extends Controller { /** * 初始化方法,最前且始终执行 */ public function _initialize() { // 只可以以cli方式执行 if (!$this->request->isCli()) { $this->error('Autotask script only work at client!'); } parent::_initialize(); // 清除错误 error_reporting(0); // 设置永不超时 set_time_limit(0); } /** * 执行定时任务 */ public function index() { $time = time(); $logDir = LOG_PATH . 'crontab/'; if (!is_dir($logDir)) { mkdir($logDir, 0755); } //筛选未过期且未完成的任务 $crontabList = Crontab::where('status', '=', 'normal')->order('weigh desc,id desc')->select(); $execTime = time(); foreach ($crontabList as $crontab) { $update = []; $execute = false; if ($time < $crontab['begintime']) { //任务未开始 continue; } if ($crontab['maximums'] && $crontab['executes'] > $crontab['maximums']) { //任务已超过最大执行次数 $update['status'] = 'completed'; } else { if ($crontab['endtime'] > 0 && $time > $crontab['endtime']) { //任务已过期 $update['status'] = 'expired'; } else { //重复执行 //如果未到执行时间则继续循环 $cron = CronExpression::factory($crontab['schedule']); if (!$cron->isDue(date("YmdHi", $execTime)) || date("YmdHi", $execTime) === date("YmdHi", $crontab['executetime'])) { continue; } $execute = true; } } // 如果允许执行 if ($execute) { $update['executetime'] = $time; $update['executes'] = $crontab['executes'] + 1; $update['status'] = ($crontab['maximums'] > 0 && $update['executes'] >= $crontab['maximums']) ? 'completed' : 'normal'; } // 如果需要更新状态 if (!$update) { continue; } // 更新状态 $crontab->save($update); // 将执行放在后面是为了避免超时导致多次执行 if (!$execute) { continue; } $result = false; $message = ''; try { if ($crontab['type'] == 'url') { if (substr($crontab['content'], 0, 1) == "/") { // 本地项目URL $message = shell_exec('php ' . ROOT_PATH . 'public/index.php ' . $crontab['content']); $result = $message ? true : false; } else { $arr = explode(" ", $crontab['content']); $url = $arr[0]; $params = isset($arr[1]) ? $arr[1] : ''; $method = isset($arr[2]) ? $arr[2] : 'POST'; try { // 远程异步调用URL $ret = Http::sendRequest($url, $params, $method); $result = $ret['ret']; $message = $ret['msg']; } catch (\Exception $e) { $message = $e->getMessage(); } } } elseif ($crontab['type'] == 'sql') { $ret = $this->sql($crontab['content']); $result = $ret['ret']; $message = $ret['msg']; } elseif ($crontab['type'] == 'shell') { // 执行Shell $message = shell_exec($crontab['content']); $result = $message ? true : false; } } catch (\Exception $e) { $message = $e->getMessage(); } $log = [ 'crontab_id' => $crontab['id'], 'executetime' => $time, 'completetime' => time(), 'content' => $message, 'status' => $result ? 'success' : 'failure', ]; Db::name("crontab_log")->insert($log); } return "Execute completed!\n"; } /** * 执行SQL语句 */ protected function sql($sql) { //这里需要强制重连数据库,使用已有的连接会报2014错误 $connect = Db::connect([], true); $connect->execute("select 1"); // 执行SQL $sqlquery = str_replace('__PREFIX__', config('database.prefix'), $sql); $sqls = preg_split("/;[ \t]{0,}\n/i", $sqlquery); $result = false; $message = ''; $connect->startTrans(); try { foreach ($sqls as $key => $val) { if (trim($val) == '' || substr($val, 0, 2) == '--' || substr($val, 0, 2) == '/*') { continue; } $message .= "\nSQL:{$val}\n"; $val = rtrim($val, ';'); if (preg_match("/^(select|explain)(.*)/i ", $val)) { $count = $connect->execute($val); if ($count > 0) { $resultlist = Db::query($val); } else { $resultlist = []; } $message .= "Total:{$count}\n"; $j = 1; foreach ($resultlist as $m => $n) { $message .= "\n"; $message .= "Row:{$j}\n"; foreach ($n as $k => $v) { $message .= "{$k}:{$v}\n"; } $j++; } } else { $count = $connect->getPdo()->exec($val); $message = "Affected rows:{$count}"; } } $connect->commit(); $result = true; } catch (\PDOException $e) { $message = $e->getMessage(); $connect->rollback(); $result = false; } return ['ret' => $result, 'msg' => $message]; } }