diff --git a/addons/crontab/.addonrc b/addons/crontab/.addonrc new file mode 100644 index 0000000..118f9cf --- /dev/null +++ b/addons/crontab/.addonrc @@ -0,0 +1 @@ +{"license":"regular","licenseto":"30453","licensekey":"Z5JMkd396LTbnaU4 T8aTPrtkRJNg8HH+v5RkHg==","menus":["general\/crontab","general\/crontab\/index","general\/crontab\/add","general\/crontab\/edit","general\/crontab\/del","general\/crontab\/multi"],"files":["application\\admin\\controller\\general\\Crontab.php","application\\admin\\controller\\general\\CrontabLog.php","application\\admin\\lang\\zh-cn\\general\\crontab.php","application\\admin\\lang\\zh-cn\\general\\crontab_log.php","application\\admin\\model\\Crontab.php","application\\admin\\model\\CrontabLog.php","application\\admin\\view\\general\\crontab\\add.html","application\\admin\\view\\general\\crontab\\edit.html","application\\admin\\view\\general\\crontab\\index.html","application\\admin\\view\\general\\crontab_log\\detail.html","application\\admin\\view\\general\\crontab_log\\index.html","public\\assets\\js\\backend\\general\\crontab.js","public\\assets\\js\\backend\\general\\crontab_log.js"]} \ No newline at end of file diff --git a/addons/crontab/Crontab.php b/addons/crontab/Crontab.php new file mode 100644 index 0000000..151d3b9 --- /dev/null +++ b/addons/crontab/Crontab.php @@ -0,0 +1,77 @@ +<?php + +namespace addons\crontab; + +use app\common\library\Menu; +use think\Addons; +use think\Loader; + +/** + * 定时任务 + */ +class Crontab extends Addons +{ + + /** + * 插件安装方法 + * @return bool + */ + public function install() + { + $menu = [ + [ + 'name' => 'general/crontab', + 'title' => '定时任务', + 'icon' => 'fa fa-tasks', + 'remark' => '类似于Linux的Crontab定时任务,可以按照设定的时间进行任务的执行,目前支持三种任务:请求URL、执行SQL、执行Shell', + 'sublist' => [ + ['name' => 'general/crontab/index', 'title' => '查看'], + ['name' => 'general/crontab/add', 'title' => '添加'], + ['name' => 'general/crontab/edit', 'title' => '编辑 '], + ['name' => 'general/crontab/del', 'title' => '删除'], + ['name' => 'general/crontab/multi', 'title' => '批量更新'], + ] + ] + ]; + Menu::create($menu, 'general'); + return true; + } + + /** + * 插件卸载方法 + * @return bool + */ + public function uninstall() + { + Menu::delete('general/crontab'); + return true; + } + + /** + * 插件启用方法 + */ + public function enable() + { + Menu::enable('general/crontab'); + } + + /** + * 插件禁用方法 + */ + public function disable() + { + Menu::disable('general/crontab'); + } + + /** + * 添加命名空间 + */ + public function appInit() + { + //添加命名空间 + if (!class_exists('\Cron\CronExpression')) { + Loader::addNamespace('Cron', ADDON_PATH . 'crontab' . DS . 'library' . DS . 'Cron' . DS); + } + } + +} diff --git a/addons/crontab/config.php b/addons/crontab/config.php new file mode 100644 index 0000000..b625128 --- /dev/null +++ b/addons/crontab/config.php @@ -0,0 +1,4 @@ +<?php + +return [ +]; diff --git a/addons/crontab/controller/Autotask.php b/addons/crontab/controller/Autotask.php new file mode 100644 index 0000000..d80b782 --- /dev/null +++ b/addons/crontab/controller/Autotask.php @@ -0,0 +1,201 @@ +<?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' . DS; + 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]; + + } +} diff --git a/addons/crontab/controller/Index.php b/addons/crontab/controller/Index.php new file mode 100644 index 0000000..0ba12a9 --- /dev/null +++ b/addons/crontab/controller/Index.php @@ -0,0 +1,16 @@ +<?php + +namespace addons\crontab\controller; + +use think\addons\Controller; + +class Index extends Controller +{ + + public function index() + { + $this->error("当前插件暂无前台页面"); + } + +} + diff --git a/addons/crontab/info.ini b/addons/crontab/info.ini new file mode 100644 index 0000000..cd161d7 --- /dev/null +++ b/addons/crontab/info.ini @@ -0,0 +1,10 @@ +name = crontab +title = 定时任务 +intro = 便捷的后台定时任务管理 +author = FastAdmin +website = https://www.fastadmin.net +version = 1.0.6 +state = 1 +url = /addons/crontab +license = regular +licenseto = 30453 diff --git a/addons/crontab/install.sql b/addons/crontab/install.sql new file mode 100644 index 0000000..d3456e7 --- /dev/null +++ b/addons/crontab/install.sql @@ -0,0 +1,35 @@ +CREATE TABLE IF NOT EXISTS `__PREFIX__crontab` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', + `type` varchar(10) NOT NULL DEFAULT '' COMMENT '事件类型', + `title` varchar(100) NOT NULL DEFAULT '' COMMENT '事件标题', + `content` text NOT NULL COMMENT '事件内容', + `schedule` varchar(100) NOT NULL DEFAULT '' COMMENT 'Crontab格式', + `sleep` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '延迟秒数执行', + `maximums` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大执行次数 0为不限', + `executes` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '已经执行的次数', + `createtime` int(10) DEFAULT NULL COMMENT '创建时间', + `updatetime` int(10) DEFAULT NULL COMMENT '更新时间', + `begintime` int(10) DEFAULT NULL COMMENT '开始时间', + `endtime` int(10) DEFAULT NULL COMMENT '结束时间', + `executetime` int(10) DEFAULT NULL COMMENT '最后执行时间', + `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重', + `status` enum('completed','expired','hidden','normal') NOT NULL DEFAULT 'normal' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='定时任务表'; + +BEGIN; +INSERT INTO `__PREFIX__crontab` (`id`, `type`, `title`, `content`, `schedule`, `sleep`, `maximums`, `executes`, `createtime`, `updatetime`, `begintime`, `endtime`, `executetime`, `weigh`, `status`) VALUES +(1, 'url', '请求百度', 'https://www.baidu.com', '* * * * *', 0, 0, 0, 1497070825, 1501253101, 1483200000, 1830268800, 1501253101, 1, 'normal'), +(2, 'sql', '查询一条SQL', 'SELECT 1;', '* * * * *', 0, 0, 0, 1497071095, 1501253101, 1483200000, 1830268800, 1501253101, 2, 'normal'); +COMMIT; + +CREATE TABLE IF NOT EXISTS `__PREFIX__crontab_log` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `crontab_id` int(10) DEFAULT NULL COMMENT '任务ID', + `executetime` int(10) DEFAULT NULL COMMENT '执行时间', + `completetime` int(10) DEFAULT NULL COMMENT '结束时间', + `content` text COMMENT '执行结果', + `status` enum('success','failure') DEFAULT 'failure' COMMENT '状态', + PRIMARY KEY (`id`), + KEY `crontab_id` (`crontab_id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='定时任务日志表'; \ No newline at end of file diff --git a/addons/crontab/library/Cron/AbstractField.php b/addons/crontab/library/Cron/AbstractField.php new file mode 100644 index 0000000..86db306 --- /dev/null +++ b/addons/crontab/library/Cron/AbstractField.php @@ -0,0 +1,252 @@ +<?php + +namespace Cron; + +/** + * Abstract CRON expression field + */ +abstract class AbstractField implements FieldInterface +{ + /** + * Full range of values that are allowed for this field type + * @var array + */ + protected $fullRange = []; + + /** + * Literal values we need to convert to integers + * @var array + */ + protected $literals = []; + + /** + * Start value of the full range + * @var integer + */ + protected $rangeStart; + + /** + * End value of the full range + * @var integer + */ + protected $rangeEnd; + + + public function __construct() + { + $this->fullRange = range($this->rangeStart, $this->rangeEnd); + } + + /** + * Check to see if a field is satisfied by a value + * + * @param string $dateValue Date value to check + * @param string $value Value to test + * + * @return bool + */ + public function isSatisfied($dateValue, $value) + { + if ($this->isIncrementsOfRanges($value)) { + return $this->isInIncrementsOfRanges($dateValue, $value); + } elseif ($this->isRange($value)) { + return $this->isInRange($dateValue, $value); + } + + return $value == '*' || $dateValue == $value; + } + + /** + * Check if a value is a range + * + * @param string $value Value to test + * + * @return bool + */ + public function isRange($value) + { + return strpos($value, '-') !== false; + } + + /** + * Check if a value is an increments of ranges + * + * @param string $value Value to test + * + * @return bool + */ + public function isIncrementsOfRanges($value) + { + return strpos($value, '/') !== false; + } + + /** + * Test if a value is within a range + * + * @param string $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInRange($dateValue, $value) + { + $parts = array_map('trim', explode('-', $value, 2)); + + return $dateValue >= $parts[0] && $dateValue <= $parts[1]; + } + + /** + * Test if a value is within an increments of ranges (offset[-to]/step size) + * + * @param string $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInIncrementsOfRanges($dateValue, $value) + { + $chunks = array_map('trim', explode('/', $value, 2)); + $range = $chunks[0]; + $step = isset($chunks[1]) ? $chunks[1] : 0; + + // No step or 0 steps aren't cool + if (is_null($step) || '0' === $step || 0 === $step) { + return false; + } + + // Expand the * to a full range + if ('*' == $range) { + $range = $this->rangeStart . '-' . $this->rangeEnd; + } + + // Generate the requested small range + $rangeChunks = explode('-', $range, 2); + $rangeStart = $rangeChunks[0]; + $rangeEnd = isset($rangeChunks[1]) ? $rangeChunks[1] : $rangeStart; + + if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) { + throw new \OutOfRangeException('Invalid range start requested'); + } + + if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) { + throw new \OutOfRangeException('Invalid range end requested'); + } + + if ($step > ($rangeEnd - $rangeStart) + 1) { + throw new \OutOfRangeException('Step cannot be greater than total range'); + } + + $thisRange = range($rangeStart, $rangeEnd, $step); + + return in_array($dateValue, $thisRange); + } + + /** + * Returns a range of values for the given cron expression + * + * @param string $expression The expression to evaluate + * @param int $max Maximum offset for range + * + * @return array + */ + public function getRangeForExpression($expression, $max) + { + $values = array(); + + if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) { + if (!$this->isIncrementsOfRanges($expression)) { + list ($offset, $to) = explode('-', $expression); + $stepSize = 1; + } + else { + $range = array_map('trim', explode('/', $expression, 2)); + $stepSize = isset($range[1]) ? $range[1] : 0; + $range = $range[0]; + $range = explode('-', $range, 2); + $offset = $range[0]; + $to = isset($range[1]) ? $range[1] : $max; + } + $offset = $offset == '*' ? 0 : $offset; + for ($i = $offset; $i <= $to; $i += $stepSize) { + $values[] = $i; + } + sort($values); + } + else { + $values = array($expression); + } + + return $values; + } + + protected function convertLiterals($value) + { + if (count($this->literals)) { + $key = array_search($value, $this->literals); + if ($key !== false) { + return $key; + } + } + + return $value; + } + + /** + * Checks to see if a value is valid for the field + * + * @param string $value + * @return bool + */ + public function validate($value) + { + $value = $this->convertLiterals($value); + + // All fields allow * as a valid value + if ('*' === $value) { + return true; + } + + // You cannot have a range and a list at the same time + if (strpos($value, ',') !== false && strpos($value, '-') !== false) { + return false; + } + + if (strpos($value, '/') !== false) { + list($range, $step) = explode('/', $value); + return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); + } + + if (strpos($value, '-') !== false) { + if (substr_count($value, '-') > 1) { + return false; + } + + $chunks = explode('-', $value); + $chunks[0] = $this->convertLiterals($chunks[0]); + $chunks[1] = $this->convertLiterals($chunks[1]); + + if ('*' == $chunks[0] || '*' == $chunks[1]) { + return false; + } + + return $this->validate($chunks[0]) && $this->validate($chunks[1]); + } + + // Validate each chunk of a list individually + if (strpos($value, ',') !== false) { + foreach (explode(',', $value) as $listItem) { + if (!$this->validate($listItem)) { + return false; + } + } + return true; + } + + // We should have a numeric by now, so coerce this into an integer + if (filter_var($value, FILTER_VALIDATE_INT) !== false) { + $value = (int) $value; + } + + return in_array($value, $this->fullRange, true); + } +} diff --git a/addons/crontab/library/Cron/CronExpression.php b/addons/crontab/library/Cron/CronExpression.php new file mode 100644 index 0000000..e809552 --- /dev/null +++ b/addons/crontab/library/Cron/CronExpression.php @@ -0,0 +1,402 @@ +<?php + +namespace Cron; + +use DateTime; +use DateTimeImmutable; +use DateTimeZone; +use Exception; +use InvalidArgumentException; +use RuntimeException; + +/** + * CRON expression parser that can determine whether or not a CRON expression is + * due to run, the next run date and previous run date of a CRON expression. + * The determinations made by this class are accurate if checked run once per + * minute (seconds are dropped from date time comparisons). + * + * Schedule parts must map to: + * minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week + * [1-7|MON-SUN], and an optional year. + * + * @link http://en.wikipedia.org/wiki/Cron + */ +class CronExpression +{ + const MINUTE = 0; + const HOUR = 1; + const DAY = 2; + const MONTH = 3; + const WEEKDAY = 4; + const YEAR = 5; + + /** + * @var array CRON expression parts + */ + private $cronParts; + + /** + * @var FieldFactory CRON field factory + */ + private $fieldFactory; + + /** + * @var int Max iteration count when searching for next run date + */ + private $maxIterationCount = 1000; + + /** + * @var array Order in which to test of cron parts + */ + private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE); + + /** + * Factory method to create a new CronExpression. + * + * @param string $expression The CRON expression to create. There are + * several special predefined values which can be used to substitute the + * CRON expression: + * + * `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 * + * `@monthly` - Run once a month, midnight, first of month - 0 0 1 * * + * `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0 + * `@daily` - Run once a day, midnight - 0 0 * * * + * `@hourly` - Run once an hour, first minute - 0 * * * * + * @param FieldFactory $fieldFactory Field factory to use + * + * @return CronExpression + */ + public static function factory($expression, FieldFactory $fieldFactory = null) + { + $mappings = array( + '@yearly' => '0 0 1 1 *', + '@annually' => '0 0 1 1 *', + '@monthly' => '0 0 1 * *', + '@weekly' => '0 0 * * 0', + '@daily' => '0 0 * * *', + '@hourly' => '0 * * * *' + ); + + if (isset($mappings[$expression])) { + $expression = $mappings[$expression]; + } + + return new static($expression, $fieldFactory ?: new FieldFactory()); + } + + /** + * Validate a CronExpression. + * + * @param string $expression The CRON expression to validate. + * + * @return bool True if a valid CRON expression was passed. False if not. + * @see \Cron\CronExpression::factory + */ + public static function isValidExpression($expression) + { + try { + self::factory($expression); + } catch (InvalidArgumentException $e) { + return false; + } + + return true; + } + + /** + * Parse a CRON expression + * + * @param string $expression CRON expression (e.g. '8 * * * *') + * @param FieldFactory $fieldFactory Factory to create cron fields + */ + public function __construct($expression, FieldFactory $fieldFactory) + { + $this->fieldFactory = $fieldFactory; + $this->setExpression($expression); + } + + /** + * Set or change the CRON expression + * + * @param string $value CRON expression (e.g. 8 * * * *) + * + * @return CronExpression + * @throws \InvalidArgumentException if not a valid CRON expression + */ + public function setExpression($value) + { + $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); + if (count($this->cronParts) < 5) { + throw new InvalidArgumentException( + $value . ' is not a valid CRON expression' + ); + } + + foreach ($this->cronParts as $position => $part) { + $this->setPart($position, $part); + } + + return $this; + } + + /** + * Set part of the CRON expression + * + * @param int $position The position of the CRON expression to set + * @param string $value The value to set + * + * @return CronExpression + * @throws \InvalidArgumentException if the value is not valid for the part + */ + public function setPart($position, $value) + { + if (!$this->fieldFactory->getField($position)->validate($value)) { + throw new InvalidArgumentException( + 'Invalid CRON field value ' . $value . ' at position ' . $position + ); + } + + $this->cronParts[$position] = $value; + + return $this; + } + + /** + * Set max iteration count for searching next run dates + * + * @param int $maxIterationCount Max iteration count when searching for next run date + * + * @return CronExpression + */ + public function setMaxIterationCount($maxIterationCount) + { + $this->maxIterationCount = $maxIterationCount; + + return $this; + } + + /** + * Get a next run date relative to the current date or a specific date + * + * @param string|\DateTime $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning a + * matching next run date. 0, the default, will return the current + * date and time if the next run date falls on the current date and + * time. Setting this value to 1 will skip the first match and go to + * the second match. Setting this value to 2 will skip the first 2 + * matches and so on. + * @param bool $allowCurrentDate Set to TRUE to return the current date if + * it matches the cron expression. + * @param null|string $timeZone Timezone to use instead of the system default + * + * @return \DateTime + * @throws \RuntimeException on too many iterations + */ + public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) + { + return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone); + } + + /** + * Get a previous run date relative to the current date or a specific date + * + * @param string|\DateTime $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone Timezone to use instead of the system default + * + * @return \DateTime + * @throws \RuntimeException on too many iterations + * @see \Cron\CronExpression::getNextRunDate + */ + public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) + { + return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone); + } + + /** + * Get multiple run dates starting at the current date or a specific date + * + * @param int $total Set the total number of dates to calculate + * @param string|\DateTime $currentTime Relative calculation date + * @param bool $invert Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone Timezone to use instead of the system default + * + * @return array Returns an array of run dates + */ + public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null) + { + $matches = array(); + for ($i = 0; $i < max(0, $total); $i++) { + try { + $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone); + } catch (RuntimeException $e) { + break; + } + } + + return $matches; + } + + /** + * Get all or part of the CRON expression + * + * @param string $part Specify the part to retrieve or NULL to get the full + * cron schedule string. + * + * @return string|null Returns the CRON expression, a part of the + * CRON expression, or NULL if the part was specified but not found + */ + public function getExpression($part = null) + { + if (null === $part) { + return implode(' ', $this->cronParts); + } elseif (array_key_exists($part, $this->cronParts)) { + return $this->cronParts[$part]; + } + + return null; + } + + /** + * Helper method to output the full expression. + * + * @return string Full CRON expression + */ + public function __toString() + { + return $this->getExpression(); + } + + /** + * Determine if the cron is due to run based on the current date or a + * specific date. This method assumes that the current number of + * seconds are irrelevant, and should be called once per minute. + * + * @param string|\DateTime $currentTime Relative calculation date + * @param null|string $timeZone Timezone to use instead of the system default + * + * @return bool Returns TRUE if the cron is due to run or FALSE if not + */ + public function isDue($currentTime = 'now', $timeZone = null) + { + if (is_null($timeZone)) { + $timeZone = date_default_timezone_get(); + } + + if ('now' === $currentTime) { + $currentDate = date('Y-m-d H:i'); + $currentTime = strtotime($currentDate); + } elseif ($currentTime instanceof DateTime) { + $currentDate = clone $currentTime; + // Ensure time in 'current' timezone is used + $currentDate->setTimezone(new DateTimeZone($timeZone)); + $currentDate = $currentDate->format('Y-m-d H:i'); + $currentTime = strtotime($currentDate); + } elseif ($currentTime instanceof DateTimeImmutable) { + $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); + $currentDate->setTimezone(new DateTimeZone($timeZone)); + $currentDate = $currentDate->format('Y-m-d H:i'); + $currentTime = strtotime($currentDate); + } else { + $currentTime = new DateTime($currentTime); + $currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0); + $currentDate = $currentTime->format('Y-m-d H:i'); + $currentTime = $currentTime->getTimeStamp(); + } + + try { + return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime; + } catch (Exception $e) { + return false; + } + } + + /** + * Get the next or previous run date of the expression relative to a date + * + * @param string|\DateTime $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $invert Set to TRUE to go backwards in time + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param string|null $timeZone Timezone to use instead of the system default + * + * @return \DateTime + * @throws \RuntimeException on too many iterations + */ + protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null) + { + if (is_null($timeZone)) { + $timeZone = date_default_timezone_get(); + } + + if ($currentTime instanceof DateTime) { + $currentDate = clone $currentTime; + } elseif ($currentTime instanceof DateTimeImmutable) { + $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); + $currentDate->setTimezone($currentTime->getTimezone()); + } else { + $currentDate = new DateTime($currentTime ?: 'now'); + $currentDate->setTimezone(new DateTimeZone($timeZone)); + } + + $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); + $nextRun = clone $currentDate; + $nth = (int) $nth; + + // We don't have to satisfy * or null fields + $parts = array(); + $fields = array(); + foreach (self::$order as $position) { + $part = $this->getExpression($position); + if (null === $part || '*' === $part) { + continue; + } + $parts[$position] = $part; + $fields[$position] = $this->fieldFactory->getField($position); + } + + // Set a hard limit to bail on an impossible date + for ($i = 0; $i < $this->maxIterationCount; $i++) { + + foreach ($parts as $position => $part) { + $satisfied = false; + // Get the field object used to validate this part + $field = $fields[$position]; + // Check if this is singular or a list + if (strpos($part, ',') === false) { + $satisfied = $field->isSatisfiedBy($nextRun, $part); + } else { + foreach (array_map('trim', explode(',', $part)) as $listPart) { + if ($field->isSatisfiedBy($nextRun, $listPart)) { + $satisfied = true; + break; + } + } + } + + // If the field is not satisfied, then start over + if (!$satisfied) { + $field->increment($nextRun, $invert, $part); + continue 2; + } + } + + // Skip this match if needed + if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { + $this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null); + continue; + } + + return $nextRun; + } + + // @codeCoverageIgnoreStart + throw new RuntimeException('Impossible CRON expression'); + // @codeCoverageIgnoreEnd + } +} diff --git a/addons/crontab/library/Cron/DayOfMonthField.php b/addons/crontab/library/Cron/DayOfMonthField.php new file mode 100644 index 0000000..abf5969 --- /dev/null +++ b/addons/crontab/library/Cron/DayOfMonthField.php @@ -0,0 +1,131 @@ +<?php + +namespace Cron; + +use DateTime; + +/** + * Day of month field. Allows: * , / - ? L W + * + * 'L' stands for "last" and specifies the last day of the month. + * + * The 'W' character is used to specify the weekday (Monday-Friday) nearest the + * given day. As an example, if you were to specify "15W" as the value for the + * day-of-month field, the meaning is: "the nearest weekday to the 15th of the + * month". So if the 15th is a Saturday, the trigger will fire on Friday the + * 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If + * the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you + * specify "1W" as the value for day-of-month, and the 1st is a Saturday, the + * trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary + * of a month's days. The 'W' character can only be specified when the + * day-of-month is a single day, not a range or list of days. + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +class DayOfMonthField extends AbstractField +{ + protected $rangeStart = 1; + protected $rangeEnd = 31; + + /** + * Get the nearest day of the week for a given day in a month + * + * @param int $currentYear Current year + * @param int $currentMonth Current month + * @param int $targetDay Target day of the month + * + * @return \DateTime Returns the nearest date + */ + private static function getNearestWeekday($currentYear, $currentMonth, $targetDay) + { + $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT); + $target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday"); + $currentWeekday = (int) $target->format('N'); + + if ($currentWeekday < 6) { + return $target; + } + + $lastDayOfMonth = $target->format('t'); + + foreach (array(-1, 1, -2, 2) as $i) { + $adjusted = $targetDay + $i; + if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { + $target->setDate($currentYear, $currentMonth, $adjusted); + if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { + return $target; + } + } + } + } + + public function isSatisfiedBy(DateTime $date, $value) + { + // ? states that the field value is to be skipped + if ($value == '?') { + return true; + } + + $fieldValue = $date->format('d'); + + // Check to see if this is the last day of the month + if ($value == 'L') { + return $fieldValue == $date->format('t'); + } + + // Check to see if this is the nearest weekday to a particular value + if (strpos($value, 'W')) { + // Parse the target day + $targetDay = substr($value, 0, strpos($value, 'W')); + // Find out if the current day is the nearest day of the week + return $date->format('j') == self::getNearestWeekday( + $date->format('Y'), + $date->format('m'), + $targetDay + )->format('j'); + } + + return $this->isSatisfied($date->format('d'), $value); + } + + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('previous day'); + $date->setTime(23, 59); + } else { + $date->modify('next day'); + $date->setTime(0, 0); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function validate($value) + { + $basicChecks = parent::validate($value); + + // Validate that a list don't have W or L + if (strpos($value, ',') !== false && (strpos($value, 'W') !== false || strpos($value, 'L') !== false)) { + return false; + } + + if (!$basicChecks) { + + if ($value === 'L') { + return true; + } + + if (preg_match('/^(.*)W$/', $value, $matches)) { + return $this->validate($matches[1]); + } + + return false; + } + + return $basicChecks; + } +} diff --git a/addons/crontab/library/Cron/DayOfWeekField.php b/addons/crontab/library/Cron/DayOfWeekField.php new file mode 100644 index 0000000..e178013 --- /dev/null +++ b/addons/crontab/library/Cron/DayOfWeekField.php @@ -0,0 +1,170 @@ +<?php + +namespace Cron; + +use DateTime; +use InvalidArgumentException; + + +/** + * Day of week field. Allows: * / , - ? L # + * + * Days of the week can be represented as a number 0-7 (0|7 = Sunday) + * or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT. + * + * 'L' stands for "last". It allows you to specify constructs such as + * "the last Friday" of a given month. + * + * '#' is allowed for the day-of-week field, and must be followed by a + * number between one and five. It allows you to specify constructs such as + * "the second Friday" of a given month. + */ +class DayOfWeekField extends AbstractField +{ + protected $rangeStart = 0; + protected $rangeEnd = 7; + + protected $nthRange; + + protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; + + public function __construct() + { + $this->nthRange = range(1, 5); + parent::__construct(); + } + + public function isSatisfiedBy(DateTime $date, $value) + { + if ($value == '?') { + return true; + } + + // Convert text day of the week values to integers + $value = $this->convertLiterals($value); + + $currentYear = $date->format('Y'); + $currentMonth = $date->format('m'); + $lastDayOfMonth = $date->format('t'); + + // Find out if this is the last specific weekday of the month + if (strpos($value, 'L')) { + $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L'))); + $tdate = clone $date; + $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); + while ($tdate->format('w') != $weekday) { + $tdateClone = new DateTime(); + $tdate = $tdateClone + ->setTimezone($tdate->getTimezone()) + ->setDate($currentYear, $currentMonth, --$lastDayOfMonth); + } + + return $date->format('j') == $lastDayOfMonth; + } + + // Handle # hash tokens + if (strpos($value, '#')) { + list($weekday, $nth) = explode('#', $value); + + if (!is_numeric($nth)) { + throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given"); + } else { + $nth = (int) $nth; + } + + // 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601 + if ($weekday === '0') { + $weekday = 7; + } + + $weekday = $this->convertLiterals($weekday); + + // Validate the hash fields + if ($weekday < 0 || $weekday > 7) { + throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given"); + } + + if (!in_array($nth, $this->nthRange)) { + throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given"); + } + + // The current weekday must match the targeted weekday to proceed + if ($date->format('N') != $weekday) { + return false; + } + + $tdate = clone $date; + $tdate->setDate($currentYear, $currentMonth, 1); + $dayCount = 0; + $currentDay = 1; + while ($currentDay < $lastDayOfMonth + 1) { + if ($tdate->format('N') == $weekday) { + if (++$dayCount >= $nth) { + break; + } + } + $tdate->setDate($currentYear, $currentMonth, ++$currentDay); + } + + return $date->format('j') == $currentDay; + } + + // Handle day of the week values + if (strpos($value, '-')) { + $parts = explode('-', $value); + if ($parts[0] == '7') { + $parts[0] = '0'; + } elseif ($parts[1] == '0') { + $parts[1] = '7'; + } + $value = implode('-', $parts); + } + + // Test to see which Sunday to use -- 0 == 7 == Sunday + $format = in_array(7, str_split($value)) ? 'N' : 'w'; + $fieldValue = $date->format($format); + + return $this->isSatisfied($fieldValue, $value); + } + + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('-1 day'); + $date->setTime(23, 59, 0); + } else { + $date->modify('+1 day'); + $date->setTime(0, 0, 0); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function validate($value) + { + $basicChecks = parent::validate($value); + + if (!$basicChecks) { + // Handle the # value + if (strpos($value, '#') !== false) { + $chunks = explode('#', $value); + $chunks[0] = $this->convertLiterals($chunks[0]); + + if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && in_array($chunks[1], $this->nthRange)) { + return true; + } + } + + if (preg_match('/^(.*)L$/', $value, $matches)) { + return $this->validate($matches[1]); + } + + return false; + } + + return $basicChecks; + } +} diff --git a/addons/crontab/library/Cron/FieldFactory.php b/addons/crontab/library/Cron/FieldFactory.php new file mode 100644 index 0000000..fd27352 --- /dev/null +++ b/addons/crontab/library/Cron/FieldFactory.php @@ -0,0 +1,54 @@ +<?php + +namespace Cron; + +use InvalidArgumentException; + +/** + * CRON field factory implementing a flyweight factory + * @link http://en.wikipedia.org/wiki/Cron + */ +class FieldFactory +{ + /** + * @var array Cache of instantiated fields + */ + private $fields = array(); + + /** + * Get an instance of a field object for a cron expression position + * + * @param int $position CRON expression position value to retrieve + * + * @return FieldInterface + * @throws InvalidArgumentException if a position is not valid + */ + public function getField($position) + { + if (!isset($this->fields[$position])) { + switch ($position) { + case 0: + $this->fields[$position] = new MinutesField(); + break; + case 1: + $this->fields[$position] = new HoursField(); + break; + case 2: + $this->fields[$position] = new DayOfMonthField(); + break; + case 3: + $this->fields[$position] = new MonthField(); + break; + case 4: + $this->fields[$position] = new DayOfWeekField(); + break; + default: + throw new InvalidArgumentException( + $position . ' is not a valid position' + ); + } + } + + return $this->fields[$position]; + } +} diff --git a/addons/crontab/library/Cron/FieldInterface.php b/addons/crontab/library/Cron/FieldInterface.php new file mode 100644 index 0000000..be37b93 --- /dev/null +++ b/addons/crontab/library/Cron/FieldInterface.php @@ -0,0 +1,40 @@ +<?php + +namespace Cron; +use DateTime; + +/** + * CRON field interface + */ +interface FieldInterface +{ + /** + * Check if the respective value of a DateTime field satisfies a CRON exp + * + * @param DateTime $date DateTime object to check + * @param string $value CRON expression to test against + * + * @return bool Returns TRUE if satisfied, FALSE otherwise + */ + public function isSatisfiedBy(DateTime $date, $value); + + /** + * When a CRON expression is not satisfied, this method is used to increment + * or decrement a DateTime object by the unit of the cron field + * + * @param DateTime $date DateTime object to change + * @param bool $invert (optional) Set to TRUE to decrement + * + * @return FieldInterface + */ + public function increment(DateTime $date, $invert = false); + + /** + * Validates a CRON expression for a given field + * + * @param string $value CRON expression value to validate + * + * @return bool Returns TRUE if valid, FALSE otherwise + */ + public function validate($value); +} diff --git a/addons/crontab/library/Cron/HoursField.php b/addons/crontab/library/Cron/HoursField.php new file mode 100644 index 0000000..4def9ca --- /dev/null +++ b/addons/crontab/library/Cron/HoursField.php @@ -0,0 +1,69 @@ +<?php + +namespace Cron; +use DateTime; +use DateTimeZone; + + +/** + * Hours field. Allows: * , / - + */ +class HoursField extends AbstractField +{ + protected $rangeStart = 0; + protected $rangeEnd = 23; + + public function isSatisfiedBy(DateTime $date, $value) + { + return $this->isSatisfied($date->format('H'), $value); + } + + public function increment(DateTime $date, $invert = false, $parts = null) + { + // Change timezone to UTC temporarily. This will + // allow us to go back or forwards and hour even + // if DST will be changed between the hours. + if (is_null($parts) || $parts == '*') { + $timezone = $date->getTimezone(); + $date->setTimezone(new DateTimeZone('UTC')); + if ($invert) { + $date->modify('-1 hour'); + } else { + $date->modify('+1 hour'); + } + $date->setTimezone($timezone); + + $date->setTime($date->format('H'), $invert ? 59 : 0); + return $this; + } + + $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts); + $hours = array(); + foreach ($parts as $part) { + $hours = array_merge($hours, $this->getRangeForExpression($part, 23)); + } + + $current_hour = $date->format('H'); + $position = $invert ? count($hours) - 1 : 0; + if (count($hours) > 1) { + for ($i = 0; $i < count($hours) - 1; $i++) { + if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) || + ($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) { + $position = $invert ? $i : $i + 1; + break; + } + } + } + + $hour = $hours[$position]; + if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) { + $date->modify(($invert ? '-' : '+') . '1 day'); + $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); + } + else { + $date->setTime($hour, $invert ? 59 : 0); + } + + return $this; + } +} diff --git a/addons/crontab/library/Cron/MinutesField.php b/addons/crontab/library/Cron/MinutesField.php new file mode 100644 index 0000000..59bb386 --- /dev/null +++ b/addons/crontab/library/Cron/MinutesField.php @@ -0,0 +1,60 @@ +<?php + +namespace Cron; + +use DateTime; + + +/** + * Minutes field. Allows: * , / - + */ +class MinutesField extends AbstractField +{ + protected $rangeStart = 0; + protected $rangeEnd = 59; + + public function isSatisfiedBy(DateTime $date, $value) + { + return $this->isSatisfied($date->format('i'), $value); + } + + public function increment(DateTime $date, $invert = false, $parts = null) + { + if (is_null($parts)) { + if ($invert) { + $date->modify('-1 minute'); + } else { + $date->modify('+1 minute'); + } + return $this; + } + + $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts); + $minutes = array(); + foreach ($parts as $part) { + $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59)); + } + + $current_minute = $date->format('i'); + $position = $invert ? count($minutes) - 1 : 0; + if (count($minutes) > 1) { + for ($i = 0; $i < count($minutes) - 1; $i++) { + if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) || + ($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) { + $position = $invert ? $i : $i + 1; + break; + } + } + } + + if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) { + $date->modify(($invert ? '-' : '+') . '1 hour'); + $date->setTime($date->format('H'), $invert ? 59 : 0); + } + else { + $date->setTime($date->format('H'), $minutes[$position]); + } + + return $this; + } +} diff --git a/addons/crontab/library/Cron/MonthField.php b/addons/crontab/library/Cron/MonthField.php new file mode 100644 index 0000000..79fdf3c --- /dev/null +++ b/addons/crontab/library/Cron/MonthField.php @@ -0,0 +1,38 @@ +<?php + +namespace Cron; + +use DateTime; + +/** + * Month field. Allows: * , / - + */ +class MonthField extends AbstractField +{ + protected $rangeStart = 1; + protected $rangeEnd = 12; + protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', + 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC']; + + public function isSatisfiedBy(DateTime $date, $value) + { + $value = $this->convertLiterals($value); + + return $this->isSatisfied($date->format('m'), $value); + } + + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('last day of previous month'); + $date->setTime(23, 59); + } else { + $date->modify('first day of next month'); + $date->setTime(0, 0); + } + + return $this; + } + + +} diff --git a/addons/crontab/model/Crontab.php b/addons/crontab/model/Crontab.php new file mode 100644 index 0000000..cfd0424 --- /dev/null +++ b/addons/crontab/model/Crontab.php @@ -0,0 +1,54 @@ +<?php + +namespace addons\crontab\model; + +use think\Model; + +class Crontab extends Model +{ + + // 开启自动写入时间戳字段 + protected $autoWriteTimestamp = 'int'; + // 定义时间戳字段名 + protected $createTime = 'createtime'; + protected $updateTime = 'updatetime'; + // 定义字段类型 + protected $type = [ + ]; + // 追加属性 + protected $append = [ + 'type_text' + ]; + + public static function getTypeList() + { + return [ + 'url' => __('Request Url'), + 'sql' => __('Execute Sql Script'), + 'shell' => __('Execute Shell'), + ]; + } + + public function getTypeTextAttr($value, $data) + { + $typelist = self::getTypeList(); + $value = $value ? $value : $data['type']; + return $value && isset($typelist[$value]) ? $typelist[$value] : $value; + } + + protected function setBegintimeAttr($value) + { + return $value && !is_numeric($value) ? strtotime($value) : $value; + } + + protected function setEndtimeAttr($value) + { + return $value && !is_numeric($value) ? strtotime($value) : $value; + } + + protected function setExecutetimeAttr($value) + { + return $value && !is_numeric($value) ? strtotime($value) : $value; + } + +} diff --git a/application/admin/controller/general/Crontab.php b/application/admin/controller/general/Crontab.php new file mode 100644 index 0000000..066ab29 --- /dev/null +++ b/application/admin/controller/general/Crontab.php @@ -0,0 +1,93 @@ +<?php + +namespace app\admin\controller\general; + +use app\common\controller\Backend; +use Cron\CronExpression; + +/** + * 定时任务 + * + * @icon fa fa-tasks + * @remark 类似于Linux的Crontab定时任务,可以按照设定的时间进行任务的执行 + */ +class Crontab extends Backend +{ + + protected $model = null; + protected $noNeedRight = ['check_schedule', 'get_schedule_future']; + + public function _initialize() + { + parent::_initialize(); + $this->model = model('Crontab'); + $this->view->assign('typeList', \app\admin\model\Crontab::getTypeList()); + $this->assignconfig('typeList', \app\admin\model\Crontab::getTypeList()); + } + + /** + * 查看 + */ + public function index() + { + if ($this->request->isAjax()) { + list($where, $sort, $order, $offset, $limit) = $this->buildparams(); + $total = $this->model + ->where($where) + ->order($sort, $order) + ->count(); + + $list = $this->model + ->where($where) + ->order($sort, $order) + ->limit($offset, $limit) + ->select(); + $time = time(); + foreach ($list as $k => &$v) { + $cron = CronExpression::factory($v['schedule']); + $v['nexttime'] = $time > $v['endtime'] ? __('None') : $cron->getNextRunDate()->getTimestamp(); + } + $result = array("total" => $total, "rows" => $list); + + return json($result); + } + return $this->view->fetch(); + } + + /** + * 判断Crontab格式是否正确 + * @internal + */ + public function check_schedule() + { + $row = $this->request->post("row/a"); + $schedule = isset($row['schedule']) ? $row['schedule'] : ''; + if (CronExpression::isValidExpression($schedule)) { + $this->success(); + } else { + $this->error(__('Crontab format invalid')); + } + } + + /** + * 根据Crontab表达式读取未来七次的时间 + * @internal + */ + public function get_schedule_future() + { + $time = []; + $schedule = $this->request->post('schedule'); + $days = (int)$this->request->post('days'); + try { + $cron = CronExpression::factory($schedule); + for ($i = 0; $i < $days; $i++) { + $time[] = $cron->getNextRunDate(null, $i)->format('Y-m-d H:i:s'); + } + } catch (\Exception $e) { + + } + + $this->success("", null, ['futuretime' => $time]); + } + +} diff --git a/application/admin/controller/general/CrontabLog.php b/application/admin/controller/general/CrontabLog.php new file mode 100644 index 0000000..ad17087 --- /dev/null +++ b/application/admin/controller/general/CrontabLog.php @@ -0,0 +1,61 @@ +<?php + +namespace app\admin\controller\general; + +use app\common\controller\Backend; + +/** + * 定时任务 + * + * @icon fa fa-tasks + * @remark 类似于Linux的Crontab定时任务,可以按照设定的时间进行任务的执行 + */ +class CrontabLog extends Backend +{ + + protected $model = null; + + public function _initialize() + { + parent::_initialize(); + $this->model = model('CrontabLog'); + $this->view->assign('statusList', $this->model->getStatusList()); + $this->assignconfig('statusList', $this->model->getStatusList()); + } + + /** + * 查看 + */ + public function index() + { + if ($this->request->isAjax()) { + list($where, $sort, $order, $offset, $limit) = $this->buildparams(); + $total = $this->model + ->where($where) + ->order($sort, $order) + ->count(); + + $list = $this->model + ->where($where) + ->order($sort, $order) + ->limit($offset, $limit) + ->select(); + $list = collection($list)->toArray(); + $result = array("total" => $total, "rows" => $list); + + return json($result); + } + return $this->view->fetch(); + } + + public function detail($ids = null) + { + $row = $this->model->get($ids); + if (!$row) { + $this->error(__('No Results were found')); + } + $this->view->assign("row", $row); + return $this->view->fetch(); + } + +} diff --git a/application/admin/lang/zh-cn/general/crontab.php b/application/admin/lang/zh-cn/general/crontab.php new file mode 100644 index 0000000..9bdf31b --- /dev/null +++ b/application/admin/lang/zh-cn/general/crontab.php @@ -0,0 +1,22 @@ +<?php + +return [ + 'Title' => '任务标题', + 'Maximums' => '最多执行', + 'Sleep' => '延迟秒数', + 'Schedule' => '执行周期', + 'Executes' => '执行次数', + 'Completed' => '已完成', + 'Expired' => '已过期', + 'Hidden' => '已禁用', + 'Logs' => '日志信息', + 'Crontab rules' => 'Crontab规则', + 'No limit' => '无限制', + 'Execute time' => '最后执行时间', + 'Request Url' => '请求URL', + 'Execute Sql Script' => '执行SQL', + 'Execute Shell' => '执行Shell', + 'Crontab format invalid' => 'Crontab格式错误', + 'Next execute time' => '下次预计时间', + 'The next %s times the execution time' => '接下来 %s 次的执行时间', +]; diff --git a/application/admin/lang/zh-cn/general/crontab_log.php b/application/admin/lang/zh-cn/general/crontab_log.php new file mode 100644 index 0000000..7ea3804 --- /dev/null +++ b/application/admin/lang/zh-cn/general/crontab_log.php @@ -0,0 +1,12 @@ +<?php + +return [ + 'Title' => '任务标题', + 'Crontab_id' => '定时任务ID', + 'Success' => '成功', + 'Failure' => '失败', + 'Content' => '返回结果', + 'Result' => '执行结果', + 'Complete time' => '完成时间', + 'Execute time' => '最后执行时间', +]; diff --git a/application/admin/model/Crontab.php b/application/admin/model/Crontab.php new file mode 100644 index 0000000..34be871 --- /dev/null +++ b/application/admin/model/Crontab.php @@ -0,0 +1,54 @@ +<?php + +namespace app\admin\model; + +use think\Model; + +class Crontab extends Model +{ + + // 开启自动写入时间戳字段 + protected $autoWriteTimestamp = 'int'; + // 定义时间戳字段名 + protected $createTime = 'createtime'; + protected $updateTime = 'updatetime'; + // 定义字段类型 + protected $type = [ + ]; + // 追加属性 + protected $append = [ + 'type_text' + ]; + + public static function getTypeList() + { + return [ + 'url' => __('Request Url'), + 'sql' => __('Execute Sql Script'), + 'shell' => __('Execute Shell'), + ]; + } + + public function getTypeTextAttr($value, $data) + { + $typelist = self::getTypeList(); + $value = $value ? $value : $data['type']; + return $value && isset($typelist[$value]) ? $typelist[$value] : $value; + } + + protected function setBegintimeAttr($value) + { + return $value && !is_numeric($value) ? strtotime($value) : $value; + } + + protected function setEndtimeAttr($value) + { + return $value && !is_numeric($value) ? strtotime($value) : $value; + } + + protected function setExecutetimeAttr($value) + { + return $value && !is_numeric($value) ? strtotime($value) : $value; + } + +} diff --git a/application/admin/model/CrontabLog.php b/application/admin/model/CrontabLog.php new file mode 100644 index 0000000..26e5318 --- /dev/null +++ b/application/admin/model/CrontabLog.php @@ -0,0 +1,34 @@ +<?php + +namespace app\admin\model; + +use think\Model; + +class CrontabLog extends Model +{ + + // 开启自动写入时间戳字段 + protected $autoWriteTimestamp = 'int'; + // 定义时间戳字段名 + protected $createTime = false; + protected $updateTime = false; + // 定义字段类型 + protected $type = [ + ]; + // 追加属性 + protected $append = [ + ]; + + public function getStatusList() + { + return ['normal' => __('Normal'), 'hidden' => __('Hidden')]; + } + + public function getStatusTextAttr($value, $data) + { + $value = $value ? $value : (isset($data['status']) ? $data['status'] : ''); + $list = $this->getStatusList(); + return isset($list[$value]) ? $list[$value] : ''; + } + +} diff --git a/application/admin/view/general/crontab/add.html b/application/admin/view/general/crontab/add.html new file mode 100644 index 0000000..11dc49f --- /dev/null +++ b/application/admin/view/general/crontab/add.html @@ -0,0 +1,87 @@ +<style type="text/css"> + #schedulepicker { + padding-top:7px; + } +</style> +<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action=""> + <div class="form-group"> + <label for="name" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label> + <div class="col-xs-12 col-sm-8"> + <input type="text" class="form-control" id="title" name="row[title]" value="" data-rule="required" /> + </div> + </div> + <div class="form-group"> + <label for="name" class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label> + <div class="col-xs-12 col-sm-8"> + {:build_select('row[type]', $typeList, null, ['class'=>'form-control', 'data-rule'=>'required'])} + </div> + </div> + <div class="form-group"> + <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label> + <div class="col-xs-12 col-sm-8"> + <textarea name="row[content]" id="conent" cols="30" rows="5" class="form-control" data-rule="required"></textarea> + </div> + </div> + <div class="form-group"> + <label for="schedule" class="control-label col-xs-12 col-sm-2">{:__('Schedule')}:</label> + <div class="col-xs-12 col-sm-8"> + <div class="input-group margin-bottom-sm"> + <input type="text" class="form-control" id="schedule" style="font-size:12px;font-family: Verdana;word-spacing:23px;" name="row[schedule]" value="* * * * *" data-rule="required; remote(general/crontab/check_schedule)"/> + <span class="input-group-btn"> + <a href="https://www.fastadmin.net/store/crontab.html" target="_blank" class="btn btn-default"><i class="fa fa-info-circle"></i> {:__('Crontab rules')}</a> + </span> + <span class="msg-box n-right"></span> + </div> + <div id="schedulepicker"> + <pre><code>* * * * * +- - - - - +| | | | +--- day of week (0 - 7) (Sunday=0 or 7) +| | | +-------- month (1 - 12) +| | +------------- day of month (1 - 31) +| +------------------ hour (0 - 23) ++----------------------- min (0 - 59)</code></pre> + <h5>{:__('The next %s times the execution time', '<input type="number" id="pickdays" class="form-control text-center" value="7" style="display: inline-block;width:80px;">')}</h5> + <ol id="scheduleresult" class="list-group"> + </ol> + </div> + </div> + </div> + <div class="form-group"> + <label for="maximums" class="control-label col-xs-12 col-sm-2">{:__('Maximums')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="number" class="form-control" id="maximums" name="row[maximums]" value="0" data-rule="required" size="6" /> + </div> + </div> + <div class="form-group"> + <label for="begintime" class="control-label col-xs-12 col-sm-2">{:__('Begin time')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="text" class="form-control datetimepicker" id="begintime" name="row[begintime]" value="" data-rule="{:__('Begin time')}:required" size="6" /> + </div> + </div> + <div class="form-group"> + <label for="endtime" class="control-label col-xs-12 col-sm-2">{:__('End time')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="text" class="form-control datetimepicker" id="endtime" name="row[endtime]" value="" data-rule="{:__('End time')}:required;match(gte, row[begintime], datetime)" size="6" /> + </div> + </div> + <div class="form-group"> + <label for="weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="text" class="form-control" id="weigh" name="row[weigh]" value="0" data-rule="required" size="6" /> + </div> + </div> + <div class="form-group"> + <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label> + <div class="col-xs-12 col-sm-8"> + {:build_radios('row[status]', ['normal'=>__('Normal'), 'hidden'=>__('Hidden')])} + </div> + </div> + <div class="form-group hide layer-footer"> + <label class="control-label col-xs-12 col-sm-2"></label> + <div class="col-xs-12 col-sm-8"> + <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button> + <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button> + </div> + </div> + +</form> diff --git a/application/admin/view/general/crontab/edit.html b/application/admin/view/general/crontab/edit.html new file mode 100644 index 0000000..7bc5b62 --- /dev/null +++ b/application/admin/view/general/crontab/edit.html @@ -0,0 +1,88 @@ +<style type="text/css"> + #schedulepicker { + padding-top: 7px; + } +</style> +<form id="edit-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action=""> + <div class="form-group"> + <label for="name" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label> + <div class="col-xs-12 col-sm-8"> + <input type="text" class="form-control" id="title" name="row[title]" value="{$row.title}" data-rule="required"/> + </div> + </div> + <div class="form-group"> + <label for="name" class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label> + <div class="col-xs-12 col-sm-8"> + {:build_select('row[type]', $typeList, $row['type'], ['class'=>'form-control', 'data-rule'=>'required'])} + </div> + </div> + <div class="form-group"> + <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label> + <div class="col-xs-12 col-sm-8"> + <textarea name="row[content]" id="conent" cols="30" rows="5" class="form-control" data-rule="required">{$row.content}</textarea> + </div> + </div> + <div class="form-group"> + <label for="schedule" class="control-label col-xs-12 col-sm-2">{:__('Schedule')}:</label> + <div class="col-xs-12 col-sm-8"> + <div class="input-group margin-bottom-sm"> + <input type="text" class="form-control" id="schedule" style="font-size:12px;font-family: Verdana;word-spacing:23px;" name="row[schedule]" value="{$row.schedule}" data-rule="required; remote(general/crontab/check_schedule)"/> + <span class="input-group-btn"> + <a href="https://www.fastadmin.net/store/crontab.html" target="_blank" class="btn btn-default"><i class="fa fa-info-circle"></i> {:__('Crontab rules')}</a> + </span> + <span class="msg-box n-right"></span> + </div> + + <div id="schedulepicker"> + <pre><code>* * * * * +- - - - - +| | | | +--- day of week (0 - 7) (Sunday=0 or 7) +| | | +-------- month (1 - 12) +| | +------------- day of month (1 - 31) +| +------------------ hour (0 - 23) ++----------------------- min (0 - 59)</code></pre> + <h5>{:__('The next %s times the execution time', '<input type="number" id="pickdays" class="form-control text-center" value="7" style="display: inline-block;width:80px;">')}</h5> + <ol id="scheduleresult" class="list-group"> + </ol> + </div> + </div> + </div> + <div class="form-group"> + <label for="maximums" class="control-label col-xs-12 col-sm-2">{:__('Maximums')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="number" class="form-control" id="maximums" name="row[maximums]" value="{$row.maximums}" data-rule="required" size="6"/> + </div> + </div> + <div class="form-group"> + <label for="begintime" class="control-label col-xs-12 col-sm-2">{:__('Begin time')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="text" class="form-control datetimepicker" id="begintime" name="row[begintime]" value="{$row.begintime|datetime}" data-rule="{:__('Begin time')}:required" size="6"/> + </div> + </div> + <div class="form-group"> + <label for="endtime" class="control-label col-xs-12 col-sm-2">{:__('End time')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="text" class="form-control datetimepicker" id="endtime" name="row[endtime]" value="{$row.endtime|datetime}" data-rule="{:__('End time')}:required;match(gte, row[begintime], datetime)" size="6"/> + </div> + </div> + <div class="form-group"> + <label for="weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="text" class="form-control" id="weigh" name="row[weigh]" value="{$row.weigh}" data-rule="required" size="6"/> + </div> + </div> + <div class="form-group"> + <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label> + <div class="col-xs-12 col-sm-8"> + {:build_radios('row[status]', ['normal'=>__('Normal'), 'completed'=>__('Completed'), 'expired'=>__('Expired'), 'hidden'=>__('Hidden')], $row['status'])} + </div> + </div> + <div class="form-group hide layer-footer"> + <label class="control-label col-xs-12 col-sm-2"></label> + <div class="col-xs-12 col-sm-8"> + <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button> + <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button> + </div> + </div> + +</form> diff --git a/application/admin/view/general/crontab/index.html b/application/admin/view/general/crontab/index.html new file mode 100644 index 0000000..89dcfa9 --- /dev/null +++ b/application/admin/view/general/crontab/index.html @@ -0,0 +1,30 @@ +<div class="panel panel-default panel-intro"> + + <div class="panel-heading"> + {:build_heading(null,FALSE)} + <ul class="nav nav-tabs" data-field="type"> + <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li> + {foreach name="typeList" item="vo"} + <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li> + {/foreach} + </ul> + </div> + + <div class="panel-body"> + <div id="myTabContent" class="tab-content"> + <div class="tab-pane fade active in" id="one"> + <div class="widget-body no-padding"> + <div id="toolbar" class="toolbar"> + {:build_toolbar('refresh,add,edit,del')} + </div> + <table id="table" class="table table-striped table-bordered table-hover" + data-operate-edit="{:$auth->check('general/crontab/edit')}" + data-operate-del="{:$auth->check('general/crontab/del')}" + width="100%"> + </table> + </div> + </div> + + </div> + </div> +</div> diff --git a/application/admin/view/general/crontab_log/detail.html b/application/admin/view/general/crontab_log/detail.html new file mode 100644 index 0000000..5758424 --- /dev/null +++ b/application/admin/view/general/crontab_log/detail.html @@ -0,0 +1,40 @@ +<style type="text/css"> + #schedulepicker { + padding-top:7px; + } +</style> +<form id="edit-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action=""> + <div class="form-group"> + <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label> + <div class="col-xs-12 col-sm-10"> + <textarea name="row[content]" id="conent" cols="30" style="width:100%;" rows="20" class="form-control" data-rule="required" readonly>{$row.content|htmlentities}</textarea> + </div> + </div> + <div class="form-group"> + <label for="executetime" class="control-label col-xs-12 col-sm-2">{:__('End time')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="text" class="form-control datetimepicker" id="executetime" name="row[executetime]" value="{$row.executetime|datetime}" data-rule="{:__('End time')}:required;match(gte, row[begintime], datetime)" size="6" disabled /> + </div> + </div> + <div class="form-group"> + <label for="completetime" class="control-label col-xs-12 col-sm-2">{:__('End time')}:</label> + <div class="col-xs-12 col-sm-4"> + <input type="text" class="form-control datetimepicker" id="completetime" name="row[completetime]" value="{$row.completetime|datetime}" data-rule="{:__('End time')}:required;match(gte, row[begintime], datetime)" size="6" disabled /> + </div> + </div> + <div class="form-group"> + <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label> + <div class="col-xs-12 col-sm-8"> + <div style="padding-top:8px;"> + {if $row['status']=='success'}<span class="label label-success">{:__('Success')}</span>{else/}<span class="label label-danger">{:__('Failure')}</span>{/if} + </div> + </div> + </div> + <div class="form-group hide layer-footer"> + <label class="control-label col-xs-12 col-sm-2"></label> + <div class="col-xs-12 col-sm-8"> + <button type="button" class="btn btn-success btn-embossed" onclick="parent.Layer.close(parent.Layer.getFrameIndex(window.name))">{:__('Close')}</button> + </div> + </div> + +</form> diff --git a/application/admin/view/general/crontab_log/index.html b/application/admin/view/general/crontab_log/index.html new file mode 100644 index 0000000..2feae15 --- /dev/null +++ b/application/admin/view/general/crontab_log/index.html @@ -0,0 +1,30 @@ +<div class="panel panel-default panel-intro"> + + <div class="panel-heading"> + {:build_heading(null,FALSE)} + <ul class="nav nav-tabs" data-field="status"> + <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li> + {foreach name="statusList" item="vo"} + <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li> + {/foreach} + </ul> + </div> + + <div class="panel-body"> + <div id="myTabContent" class="tab-content"> + <div class="tab-pane fade active in" id="one"> + <div class="widget-body no-padding"> + <div id="toolbar" class="toolbar"> + {:build_toolbar('refresh,del')} + </div> + <table id="table" class="table table-striped table-bordered table-hover" + data-operate-detail="{:$auth->check('general/crontab/detail')}" + data-operate-del="{:$auth->check('general/crontab/del')}" + width="100%"> + </table> + </div> + </div> + + </div> + </div> +</div> diff --git a/application/extra/addons.php b/application/extra/addons.php index 75c218e..d0905dd 100644 --- a/application/extra/addons.php +++ b/application/extra/addons.php @@ -3,6 +3,9 @@ return [ 'autoload' => false, 'hooks' => [ + 'app_init' => [ + 'crontab', + ], 'config_init' => [ 'third', ], diff --git a/application/index/controller/Crontab.php b/application/index/controller/Crontab.php new file mode 100644 index 0000000..1204f6a --- /dev/null +++ b/application/index/controller/Crontab.php @@ -0,0 +1,30 @@ +<?php + +namespace app\index\controller; + +use app\common\controller\Frontend; +use think\Db; + +/** + * 定时任务 + */ +class Crontab extends Frontend +{ + protected $layout = ''; + protected $noNeedLogin = ['*']; + protected $noNeedRight = ['*']; + + public function _initialize() + { + parent::_initialize(); + } + + /** + * 每月1号凌晨清空用户暂存的上月的打卡总工时 + */ + public function clear() + { + Db::name('user')->where('id',10)->update(['work_hours_month'=>0,'work_salary_month'=>0,'work_subsidy_month'=>0,'recruit_subsidy_month'=>0]); + } + +} diff --git a/public/assets/js/backend/general/crontab.js b/public/assets/js/backend/general/crontab.js new file mode 100644 index 0000000..c12a897 --- /dev/null +++ b/public/assets/js/backend/general/crontab.js @@ -0,0 +1,102 @@ +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) { + + var Controller = { + index: function () { + // 初始化表格参数配置 + Table.api.init({ + extend: { + index_url: 'general/crontab/index', + add_url: 'general/crontab/add', + edit_url: 'general/crontab/edit', + del_url: 'general/crontab/del', + multi_url: 'general/crontab/multi', + table: 'crontab' + } + }); + + var table = $("#table"); + + // 初始化表格 + table.bootstrapTable({ + url: $.fn.bootstrapTable.defaults.extend.index_url, + sortName: 'weigh', + columns: [ + [ + {field: 'state', checkbox: true,}, + {field: 'id', title: 'ID'}, + {field: 'type', title: __('Type'), searchList: Config.typeList, formatter: Table.api.formatter.label, custom: {sql: 'warning', url: 'info', shell: 'success'}}, + {field: 'title', title: __('Title')}, + {field: 'maximums', title: __('Maximums'), formatter: Controller.api.formatter.maximums}, + {field: 'executes', title: __('Executes')}, + {field: 'begintime', title: __('Begin time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange'}, + {field: 'endtime', title: __('End time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange'}, + {field: 'nexttime', title: __('Next execute time'), formatter: Controller.api.formatter.nexttime, operate: false, addclass: 'datetimerange', sortable: true}, + {field: 'executetime', title: __('Execute time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true}, + {field: 'weigh', title: __('Weigh')}, + {field: 'status', title: __('Status'), formatter: Table.api.formatter.status}, + { + field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate, + buttons: [ + { + name: "detail", + icon: "fa fa-list", + title: function (row, index) { + return __('Logs') + "[" + row['title'] + "]"; + }, + text: __('Logs'), + classname: "btn btn-xs btn-info btn-dialog", + url: "general/crontab_log/index?crontab_id={ids}", + } + ] + } + ] + ] + }); + + // 为表格绑定事件 + Table.api.bindevent(table); + }, + add: function () { + Controller.api.bindevent(); + }, + edit: function () { + Controller.api.bindevent(); + }, + api: { + bindevent: function () { + $('#schedule').on('valid.field', function (e, result) { + $("#pickdays").trigger("change"); + }); + Form.api.bindevent($("form[role=form]")); + $(document).on("change", "#pickdays", function () { + Fast.api.ajax({url: "general/crontab/get_schedule_future", data: {schedule: $("#schedule").val(), days: $(this).val()}}, function (data, ret) { + if (typeof data.futuretime !== 'undefined' && $.isArray(data.futuretime)) { + var result = []; + $.each(data.futuretime, function (i, j) { + result.push("<li class='list-group-item'>" + j + "<span class='badge'>" + (i + 1) + "</span></li>"); + }); + $("#scheduleresult").html(result.join("")); + } else { + $("#scheduleresult").html(""); + } + return false; + }); + }); + $("#pickdays").trigger("change"); + }, + formatter: { + nexttime: function (value, row, index) { + if (isNaN(value)) { + return value; + } else { + return Table.api.formatter.datetime.call(this, value, row, index); + } + }, + maximums: function (value, row, index) { + return value === 0 ? __('No limit') : value; + } + } + } + }; + return Controller; +}); diff --git a/public/assets/js/backend/general/crontab_log.js b/public/assets/js/backend/general/crontab_log.js new file mode 100644 index 0000000..f881e3f --- /dev/null +++ b/public/assets/js/backend/general/crontab_log.js @@ -0,0 +1,65 @@ +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) { + + var Controller = { + index: function () { + // 初始化表格参数配置 + Table.api.init({ + extend: { + index_url: 'general/crontab_log/index', + add_url: 'general/crontab_log/add', + edit_url: '', + del_url: 'general/crontab_log/del', + multi_url: 'general/crontab_log/multi', + table: 'crontab' + } + }); + + var table = $("#table"); + + // 初始化表格 + table.bootstrapTable({ + url: $.fn.bootstrapTable.defaults.extend.index_url, + sortName: 'id', + columns: [ + [ + {field: 'state', checkbox: true,}, + {field: 'id', title: 'ID'}, + {field: 'crontab_id', title: __('Crontab_id'), formatter: Table.api.formatter.search}, + {field: 'executetime', title: __('Execute time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true}, + {field: 'completetime', title: __('Complete time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true}, + {field: 'status', title: __('Status'), searchList: Config.statusList, custom: {success: 'success', failure: 'danger'}, formatter: Table.api.formatter.status}, + { + field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate, + buttons: [ + { + name: "detail", + text: __("Result"), + classname: "btn btn-xs btn-info btn-dialog", + icon: "fa fa-file", + url: "general/crontab_log/detail", + extend: "data-window='parent'" + } + ] + } + ] + ] + }); + + // 为表格绑定事件 + Table.api.bindevent(table); + }, + add: function () { + Controller.api.bindevent(); + }, + edit: function () { + Controller.api.bindevent(); + }, + api: { + bindevent: function () { + Form.api.bindevent($("form[role=form]")); + + }, + } + }; + return Controller; +}); \ No newline at end of file