正在显示
30 个修改的文件
包含
2345 行增加
和
0 行删除
addons/crontab/.addonrc
0 → 100644
1 | +{"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"],"license":"regular","licenseto":"10789","licensekey":"9IdXhjGlZeSQqsLR xRsMDd9LMf4z2yXTT4HomQ==","domains":["campus.cn"],"licensecodes":[],"validations":["c51a74160ee0aa6a19729b33d4a9fcdc"],"menus":["general\/crontab","general\/crontab\/index","general\/crontab\/add","general\/crontab\/edit","general\/crontab\/del","general\/crontab\/multi"]} |
addons/crontab/Crontab.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace addons\crontab; | ||
4 | + | ||
5 | +use app\common\library\Menu; | ||
6 | +use think\Addons; | ||
7 | +use think\Loader; | ||
8 | + | ||
9 | +/** | ||
10 | + * 定时任务 | ||
11 | + */ | ||
12 | +class Crontab extends Addons | ||
13 | +{ | ||
14 | + | ||
15 | + /** | ||
16 | + * 插件安装方法 | ||
17 | + * @return bool | ||
18 | + */ | ||
19 | + public function install() | ||
20 | + { | ||
21 | + $menu = [ | ||
22 | + [ | ||
23 | + 'name' => 'general/crontab', | ||
24 | + 'title' => '定时任务', | ||
25 | + 'icon' => 'fa fa-tasks', | ||
26 | + 'remark' => '按照设定的时间进行任务的执行,目前支持三种任务:请求URL、执行SQL、执行Shell。', | ||
27 | + 'sublist' => [ | ||
28 | + ['name' => 'general/crontab/index', 'title' => '查看'], | ||
29 | + ['name' => 'general/crontab/add', 'title' => '添加'], | ||
30 | + ['name' => 'general/crontab/edit', 'title' => '编辑 '], | ||
31 | + ['name' => 'general/crontab/del', 'title' => '删除'], | ||
32 | + ['name' => 'general/crontab/multi', 'title' => '批量更新'], | ||
33 | + ] | ||
34 | + ] | ||
35 | + ]; | ||
36 | + Menu::create($menu, 'general'); | ||
37 | + return true; | ||
38 | + } | ||
39 | + | ||
40 | + /** | ||
41 | + * 插件卸载方法 | ||
42 | + * @return bool | ||
43 | + */ | ||
44 | + public function uninstall() | ||
45 | + { | ||
46 | + Menu::delete('general/crontab'); | ||
47 | + return true; | ||
48 | + } | ||
49 | + | ||
50 | + /** | ||
51 | + * 插件启用方法 | ||
52 | + */ | ||
53 | + public function enable() | ||
54 | + { | ||
55 | + Menu::enable('general/crontab'); | ||
56 | + } | ||
57 | + | ||
58 | + /** | ||
59 | + * 插件禁用方法 | ||
60 | + */ | ||
61 | + public function disable() | ||
62 | + { | ||
63 | + Menu::disable('general/crontab'); | ||
64 | + } | ||
65 | + | ||
66 | + /** | ||
67 | + * 添加命名空间 | ||
68 | + */ | ||
69 | + public function appInit() | ||
70 | + { | ||
71 | + //添加命名空间 | ||
72 | + if (!class_exists('\Cron\CronExpression')) { | ||
73 | + Loader::addNamespace('Cron', ADDON_PATH . 'crontab' . DS . 'library' . DS . 'Cron' . DS); | ||
74 | + } | ||
75 | + } | ||
76 | + | ||
77 | +} |
addons/crontab/config.php
0 → 100644
addons/crontab/controller/Autotask.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace addons\crontab\controller; | ||
4 | + | ||
5 | +use addons\crontab\model\Crontab; | ||
6 | +use Cron\CronExpression; | ||
7 | +use fast\Http; | ||
8 | +use think\Controller; | ||
9 | +use think\Db; | ||
10 | +use think\Exception; | ||
11 | +use think\Log; | ||
12 | + | ||
13 | +/** | ||
14 | + * 定时任务接口 | ||
15 | + * | ||
16 | + * 以Crontab方式每分钟定时执行,且只可以Cli方式运行 | ||
17 | + * @internal | ||
18 | + */ | ||
19 | +class Autotask extends Controller | ||
20 | +{ | ||
21 | + | ||
22 | + /** | ||
23 | + * 初始化方法,最前且始终执行 | ||
24 | + */ | ||
25 | + public function _initialize() | ||
26 | + { | ||
27 | + // 只可以以cli方式执行 | ||
28 | + if (!$this->request->isCli()) { | ||
29 | + $this->error('Autotask script only work at client!'); | ||
30 | + } | ||
31 | + | ||
32 | + parent::_initialize(); | ||
33 | + | ||
34 | + // 清除错误 | ||
35 | + error_reporting(0); | ||
36 | + | ||
37 | + // 设置永不超时 | ||
38 | + set_time_limit(0); | ||
39 | + } | ||
40 | + | ||
41 | + /** | ||
42 | + * 执行定时任务 | ||
43 | + */ | ||
44 | + public function index() | ||
45 | + { | ||
46 | + $time = time(); | ||
47 | + $logDir = LOG_PATH . 'crontab' . DS; | ||
48 | + if (!is_dir($logDir)) { | ||
49 | + mkdir($logDir, 0755); | ||
50 | + } | ||
51 | + //筛选未过期且未完成的任务 | ||
52 | + $crontabList = Crontab::where('status', '=', 'normal')->order('weigh DESC,id DESC')->select(); | ||
53 | + $execTime = time(); | ||
54 | + foreach ($crontabList as $crontab) { | ||
55 | + $update = []; | ||
56 | + $execute = false; | ||
57 | + if ($time < $crontab['begintime']) { | ||
58 | + //任务未开始 | ||
59 | + continue; | ||
60 | + } | ||
61 | + if ($crontab['maximums'] && $crontab['executes'] > $crontab['maximums']) { | ||
62 | + //任务已超过最大执行次数 | ||
63 | + $update['status'] = 'completed'; | ||
64 | + } else { | ||
65 | + if ($crontab['endtime'] > 0 && $time > $crontab['endtime']) { | ||
66 | + //任务已过期 | ||
67 | + $update['status'] = 'expired'; | ||
68 | + } else { | ||
69 | + //重复执行 | ||
70 | + //如果未到执行时间则继续循环 | ||
71 | + $cron = CronExpression::factory($crontab['schedule']); | ||
72 | + if (!$cron->isDue(date("YmdHi", $execTime)) || date("YmdHi", $execTime) === date("YmdHi", $crontab['executetime'])) { | ||
73 | + continue; | ||
74 | + } | ||
75 | + $execute = true; | ||
76 | + } | ||
77 | + } | ||
78 | + | ||
79 | + // 如果允许执行 | ||
80 | + if ($execute) { | ||
81 | + $update['executetime'] = $time; | ||
82 | + $update['executes'] = $crontab['executes'] + 1; | ||
83 | + $update['status'] = ($crontab['maximums'] > 0 && $update['executes'] >= $crontab['maximums']) ? 'completed' : 'normal'; | ||
84 | + } | ||
85 | + | ||
86 | + // 如果需要更新状态 | ||
87 | + if (!$update) { | ||
88 | + continue; | ||
89 | + } | ||
90 | + // 更新状态 | ||
91 | + $crontab->save($update); | ||
92 | + | ||
93 | + // 将执行放在后面是为了避免超时导致多次执行 | ||
94 | + if (!$execute) { | ||
95 | + continue; | ||
96 | + } | ||
97 | + $result = false; | ||
98 | + $message = ''; | ||
99 | + | ||
100 | + try { | ||
101 | + if ($crontab['type'] == 'url') { | ||
102 | + if (substr($crontab['content'], 0, 1) == "/") { | ||
103 | + // 本地项目URL | ||
104 | + $message = shell_exec('php ' . ROOT_PATH . 'public/index.php ' . $crontab['content']); | ||
105 | + $result = $message ? true : false; | ||
106 | + } else { | ||
107 | + $arr = explode(" ", $crontab['content']); | ||
108 | + $url = $arr[0]; | ||
109 | + $params = isset($arr[1]) ? $arr[1] : ''; | ||
110 | + $method = isset($arr[2]) ? $arr[2] : 'POST'; | ||
111 | + try { | ||
112 | + // 远程异步调用URL | ||
113 | + $ret = Http::sendRequest($url, $params, $method); | ||
114 | + $result = $ret['ret']; | ||
115 | + $message = $ret['msg']; | ||
116 | + } catch (\Exception $e) { | ||
117 | + $message = $e->getMessage(); | ||
118 | + } | ||
119 | + } | ||
120 | + | ||
121 | + } elseif ($crontab['type'] == 'sql') { | ||
122 | + $ret = $this->sql($crontab['content']); | ||
123 | + $result = $ret['ret']; | ||
124 | + $message = $ret['msg']; | ||
125 | + } elseif ($crontab['type'] == 'shell') { | ||
126 | + // 执行Shell | ||
127 | + $message = shell_exec($crontab['content']); | ||
128 | + $result = $message ? true : false; | ||
129 | + } | ||
130 | + } catch (\Exception $e) { | ||
131 | + $message = $e->getMessage(); | ||
132 | + } | ||
133 | + $log = [ | ||
134 | + 'crontab_id' => $crontab['id'], | ||
135 | + 'executetime' => $time, | ||
136 | + 'completetime' => time(), | ||
137 | + 'content' => $message, | ||
138 | + 'status' => $result ? 'success' : 'failure', | ||
139 | + ]; | ||
140 | + Db::name("crontab_log")->insert($log); | ||
141 | + } | ||
142 | + return "Execute completed!\n"; | ||
143 | + } | ||
144 | + | ||
145 | + /** | ||
146 | + * 执行SQL语句 | ||
147 | + */ | ||
148 | + protected function sql($sql) | ||
149 | + { | ||
150 | + //这里需要强制重连数据库,使用已有的连接会报2014错误 | ||
151 | + $connect = Db::connect([], true); | ||
152 | + $connect->execute("select 1"); | ||
153 | + | ||
154 | + // 执行SQL | ||
155 | + $sqlquery = str_replace('__PREFIX__', config('database.prefix'), $sql); | ||
156 | + $sqls = preg_split("/;[ \t]{0,}\n/i", $sqlquery); | ||
157 | + | ||
158 | + $result = false; | ||
159 | + $message = ''; | ||
160 | + $connect->startTrans(); | ||
161 | + try { | ||
162 | + foreach ($sqls as $key => $val) { | ||
163 | + if (trim($val) == '' || substr($val, 0, 2) == '--' || substr($val, 0, 2) == '/*') { | ||
164 | + continue; | ||
165 | + } | ||
166 | + $message .= "\nSQL:{$val}\n"; | ||
167 | + $val = rtrim($val, ';'); | ||
168 | + if (preg_match("/^(select|explain)(.*)/i ", $val)) { | ||
169 | + $count = $connect->execute($val); | ||
170 | + if ($count > 0) { | ||
171 | + $resultlist = Db::query($val); | ||
172 | + } else { | ||
173 | + $resultlist = []; | ||
174 | + } | ||
175 | + | ||
176 | + $message .= "Total:{$count}\n"; | ||
177 | + $j = 1; | ||
178 | + foreach ($resultlist as $m => $n) { | ||
179 | + $message .= "\n"; | ||
180 | + $message .= "Row:{$j}\n"; | ||
181 | + foreach ($n as $k => $v) { | ||
182 | + $message .= "{$k}:{$v}\n"; | ||
183 | + } | ||
184 | + $j++; | ||
185 | + } | ||
186 | + } else { | ||
187 | + $count = $connect->getPdo()->exec($val); | ||
188 | + $message = "Affected rows:{$count}"; | ||
189 | + } | ||
190 | + } | ||
191 | + $connect->commit(); | ||
192 | + $result = true; | ||
193 | + } catch (\PDOException $e) { | ||
194 | + $message = $e->getMessage(); | ||
195 | + $connect->rollback(); | ||
196 | + $result = false; | ||
197 | + } | ||
198 | + return ['ret' => $result, 'msg' => $message]; | ||
199 | + } | ||
200 | +} |
addons/crontab/controller/Index.php
0 → 100644
addons/crontab/info.ini
0 → 100644
addons/crontab/install.sql
0 → 100644
1 | +CREATE TABLE IF NOT EXISTS `__PREFIX__crontab` ( | ||
2 | + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', | ||
3 | + `type` varchar(10) NOT NULL DEFAULT '' COMMENT '事件类型', | ||
4 | + `title` varchar(100) NOT NULL DEFAULT '' COMMENT '事件标题', | ||
5 | + `content` text NOT NULL COMMENT '事件内容', | ||
6 | + `schedule` varchar(100) NOT NULL DEFAULT '' COMMENT 'Crontab格式', | ||
7 | + `sleep` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '延迟秒数执行', | ||
8 | + `maximums` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大执行次数 0为不限', | ||
9 | + `executes` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '已经执行的次数', | ||
10 | + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', | ||
11 | + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', | ||
12 | + `begintime` bigint(16) DEFAULT NULL COMMENT '开始时间', | ||
13 | + `endtime` bigint(16) DEFAULT NULL COMMENT '结束时间', | ||
14 | + `executetime` bigint(16) DEFAULT NULL COMMENT '最后执行时间', | ||
15 | + `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重', | ||
16 | + `status` enum('completed','expired','hidden','normal') NOT NULL DEFAULT 'normal' COMMENT '状态', | ||
17 | + PRIMARY KEY (`id`) | ||
18 | +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='定时任务表'; | ||
19 | + | ||
20 | +BEGIN; | ||
21 | +INSERT INTO `__PREFIX__crontab` (`id`, `type`, `title`, `content`, `schedule`, `sleep`, `maximums`, `executes`, `createtime`, `updatetime`, `begintime`, `endtime`, `executetime`, `weigh`, `status`) VALUES | ||
22 | +(1, 'url', '请求百度', 'https://www.baidu.com', '* * * * *', 0, 0, 0, 1497070825, 1501253101, 1483200000, 1830268800, 1501253101, 1, 'normal'), | ||
23 | +(2, 'sql', '查询一条SQL', 'SELECT 1;', '* * * * *', 0, 0, 0, 1497071095, 1501253101, 1483200000, 1830268800, 1501253101, 2, 'normal'); | ||
24 | +COMMIT; | ||
25 | + | ||
26 | +CREATE TABLE IF NOT EXISTS `__PREFIX__crontab_log` ( | ||
27 | + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, | ||
28 | + `crontab_id` int(10) DEFAULT NULL COMMENT '任务ID', | ||
29 | + `executetime` bigint(16) DEFAULT NULL COMMENT '执行时间', | ||
30 | + `completetime` bigint(16) DEFAULT NULL COMMENT '结束时间', | ||
31 | + `content` text COMMENT '执行结果', | ||
32 | + `status` enum('success','failure') DEFAULT 'failure' COMMENT '状态', | ||
33 | + PRIMARY KEY (`id`), | ||
34 | + KEY `crontab_id` (`crontab_id`) | ||
35 | +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='定时任务日志表'; |
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | + | ||
5 | +/** | ||
6 | + * Abstract CRON expression field | ||
7 | + */ | ||
8 | +abstract class AbstractField implements FieldInterface | ||
9 | +{ | ||
10 | + /** | ||
11 | + * Full range of values that are allowed for this field type | ||
12 | + * @var array | ||
13 | + */ | ||
14 | + protected $fullRange = []; | ||
15 | + | ||
16 | + /** | ||
17 | + * Literal values we need to convert to integers | ||
18 | + * @var array | ||
19 | + */ | ||
20 | + protected $literals = []; | ||
21 | + | ||
22 | + /** | ||
23 | + * Start value of the full range | ||
24 | + * @var integer | ||
25 | + */ | ||
26 | + protected $rangeStart; | ||
27 | + | ||
28 | + /** | ||
29 | + * End value of the full range | ||
30 | + * @var integer | ||
31 | + */ | ||
32 | + protected $rangeEnd; | ||
33 | + | ||
34 | + | ||
35 | + public function __construct() | ||
36 | + { | ||
37 | + $this->fullRange = range($this->rangeStart, $this->rangeEnd); | ||
38 | + } | ||
39 | + | ||
40 | + /** | ||
41 | + * Check to see if a field is satisfied by a value | ||
42 | + * | ||
43 | + * @param string $dateValue Date value to check | ||
44 | + * @param string $value Value to test | ||
45 | + * | ||
46 | + * @return bool | ||
47 | + */ | ||
48 | + public function isSatisfied($dateValue, $value) | ||
49 | + { | ||
50 | + if ($this->isIncrementsOfRanges($value)) { | ||
51 | + return $this->isInIncrementsOfRanges($dateValue, $value); | ||
52 | + } elseif ($this->isRange($value)) { | ||
53 | + return $this->isInRange($dateValue, $value); | ||
54 | + } | ||
55 | + | ||
56 | + return $value == '*' || $dateValue == $value; | ||
57 | + } | ||
58 | + | ||
59 | + /** | ||
60 | + * Check if a value is a range | ||
61 | + * | ||
62 | + * @param string $value Value to test | ||
63 | + * | ||
64 | + * @return bool | ||
65 | + */ | ||
66 | + public function isRange($value) | ||
67 | + { | ||
68 | + return strpos($value, '-') !== false; | ||
69 | + } | ||
70 | + | ||
71 | + /** | ||
72 | + * Check if a value is an increments of ranges | ||
73 | + * | ||
74 | + * @param string $value Value to test | ||
75 | + * | ||
76 | + * @return bool | ||
77 | + */ | ||
78 | + public function isIncrementsOfRanges($value) | ||
79 | + { | ||
80 | + return strpos($value, '/') !== false; | ||
81 | + } | ||
82 | + | ||
83 | + /** | ||
84 | + * Test if a value is within a range | ||
85 | + * | ||
86 | + * @param string $dateValue Set date value | ||
87 | + * @param string $value Value to test | ||
88 | + * | ||
89 | + * @return bool | ||
90 | + */ | ||
91 | + public function isInRange($dateValue, $value) | ||
92 | + { | ||
93 | + $parts = array_map('trim', explode('-', $value, 2)); | ||
94 | + | ||
95 | + return $dateValue >= $parts[0] && $dateValue <= $parts[1]; | ||
96 | + } | ||
97 | + | ||
98 | + /** | ||
99 | + * Test if a value is within an increments of ranges (offset[-to]/step size) | ||
100 | + * | ||
101 | + * @param string $dateValue Set date value | ||
102 | + * @param string $value Value to test | ||
103 | + * | ||
104 | + * @return bool | ||
105 | + */ | ||
106 | + public function isInIncrementsOfRanges($dateValue, $value) | ||
107 | + { | ||
108 | + $chunks = array_map('trim', explode('/', $value, 2)); | ||
109 | + $range = $chunks[0]; | ||
110 | + $step = isset($chunks[1]) ? $chunks[1] : 0; | ||
111 | + | ||
112 | + // No step or 0 steps aren't cool | ||
113 | + if (is_null($step) || '0' === $step || 0 === $step) { | ||
114 | + return false; | ||
115 | + } | ||
116 | + | ||
117 | + // Expand the * to a full range | ||
118 | + if ('*' == $range) { | ||
119 | + $range = $this->rangeStart . '-' . $this->rangeEnd; | ||
120 | + } | ||
121 | + | ||
122 | + // Generate the requested small range | ||
123 | + $rangeChunks = explode('-', $range, 2); | ||
124 | + $rangeStart = $rangeChunks[0]; | ||
125 | + $rangeEnd = isset($rangeChunks[1]) ? $rangeChunks[1] : $rangeStart; | ||
126 | + | ||
127 | + if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) { | ||
128 | + throw new \OutOfRangeException('Invalid range start requested'); | ||
129 | + } | ||
130 | + | ||
131 | + if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) { | ||
132 | + throw new \OutOfRangeException('Invalid range end requested'); | ||
133 | + } | ||
134 | + | ||
135 | + if ($step > ($rangeEnd - $rangeStart) + 1) { | ||
136 | + throw new \OutOfRangeException('Step cannot be greater than total range'); | ||
137 | + } | ||
138 | + | ||
139 | + $thisRange = range($rangeStart, $rangeEnd, $step); | ||
140 | + | ||
141 | + return in_array($dateValue, $thisRange); | ||
142 | + } | ||
143 | + | ||
144 | + /** | ||
145 | + * Returns a range of values for the given cron expression | ||
146 | + * | ||
147 | + * @param string $expression The expression to evaluate | ||
148 | + * @param int $max Maximum offset for range | ||
149 | + * | ||
150 | + * @return array | ||
151 | + */ | ||
152 | + public function getRangeForExpression($expression, $max) | ||
153 | + { | ||
154 | + $values = array(); | ||
155 | + | ||
156 | + if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) { | ||
157 | + if (!$this->isIncrementsOfRanges($expression)) { | ||
158 | + list ($offset, $to) = explode('-', $expression); | ||
159 | + $stepSize = 1; | ||
160 | + } | ||
161 | + else { | ||
162 | + $range = array_map('trim', explode('/', $expression, 2)); | ||
163 | + $stepSize = isset($range[1]) ? $range[1] : 0; | ||
164 | + $range = $range[0]; | ||
165 | + $range = explode('-', $range, 2); | ||
166 | + $offset = $range[0]; | ||
167 | + $to = isset($range[1]) ? $range[1] : $max; | ||
168 | + } | ||
169 | + $offset = $offset == '*' ? 0 : $offset; | ||
170 | + for ($i = $offset; $i <= $to; $i += $stepSize) { | ||
171 | + $values[] = $i; | ||
172 | + } | ||
173 | + sort($values); | ||
174 | + } | ||
175 | + else { | ||
176 | + $values = array($expression); | ||
177 | + } | ||
178 | + | ||
179 | + return $values; | ||
180 | + } | ||
181 | + | ||
182 | + protected function convertLiterals($value) | ||
183 | + { | ||
184 | + if (count($this->literals)) { | ||
185 | + $key = array_search($value, $this->literals); | ||
186 | + if ($key !== false) { | ||
187 | + return $key; | ||
188 | + } | ||
189 | + } | ||
190 | + | ||
191 | + return $value; | ||
192 | + } | ||
193 | + | ||
194 | + /** | ||
195 | + * Checks to see if a value is valid for the field | ||
196 | + * | ||
197 | + * @param string $value | ||
198 | + * @return bool | ||
199 | + */ | ||
200 | + public function validate($value) | ||
201 | + { | ||
202 | + $value = $this->convertLiterals($value); | ||
203 | + | ||
204 | + // All fields allow * as a valid value | ||
205 | + if ('*' === $value) { | ||
206 | + return true; | ||
207 | + } | ||
208 | + | ||
209 | + // You cannot have a range and a list at the same time | ||
210 | + if (strpos($value, ',') !== false && strpos($value, '-') !== false) { | ||
211 | + return false; | ||
212 | + } | ||
213 | + | ||
214 | + if (strpos($value, '/') !== false) { | ||
215 | + list($range, $step) = explode('/', $value); | ||
216 | + return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); | ||
217 | + } | ||
218 | + | ||
219 | + if (strpos($value, '-') !== false) { | ||
220 | + if (substr_count($value, '-') > 1) { | ||
221 | + return false; | ||
222 | + } | ||
223 | + | ||
224 | + $chunks = explode('-', $value); | ||
225 | + $chunks[0] = $this->convertLiterals($chunks[0]); | ||
226 | + $chunks[1] = $this->convertLiterals($chunks[1]); | ||
227 | + | ||
228 | + if ('*' == $chunks[0] || '*' == $chunks[1]) { | ||
229 | + return false; | ||
230 | + } | ||
231 | + | ||
232 | + return $this->validate($chunks[0]) && $this->validate($chunks[1]); | ||
233 | + } | ||
234 | + | ||
235 | + // Validate each chunk of a list individually | ||
236 | + if (strpos($value, ',') !== false) { | ||
237 | + foreach (explode(',', $value) as $listItem) { | ||
238 | + if (!$this->validate($listItem)) { | ||
239 | + return false; | ||
240 | + } | ||
241 | + } | ||
242 | + return true; | ||
243 | + } | ||
244 | + | ||
245 | + // We should have a numeric by now, so coerce this into an integer | ||
246 | + if (filter_var($value, FILTER_VALIDATE_INT) !== false) { | ||
247 | + $value = (int) $value; | ||
248 | + } | ||
249 | + | ||
250 | + return in_array($value, $this->fullRange, true); | ||
251 | + } | ||
252 | +} |
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | + | ||
5 | +use DateTime; | ||
6 | +use DateTimeImmutable; | ||
7 | +use DateTimeZone; | ||
8 | +use Exception; | ||
9 | +use InvalidArgumentException; | ||
10 | +use RuntimeException; | ||
11 | + | ||
12 | +/** | ||
13 | + * CRON expression parser that can determine whether or not a CRON expression is | ||
14 | + * due to run, the next run date and previous run date of a CRON expression. | ||
15 | + * The determinations made by this class are accurate if checked run once per | ||
16 | + * minute (seconds are dropped from date time comparisons). | ||
17 | + * | ||
18 | + * Schedule parts must map to: | ||
19 | + * minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week | ||
20 | + * [1-7|MON-SUN], and an optional year. | ||
21 | + * | ||
22 | + * @link http://en.wikipedia.org/wiki/Cron | ||
23 | + */ | ||
24 | +class CronExpression | ||
25 | +{ | ||
26 | + const MINUTE = 0; | ||
27 | + const HOUR = 1; | ||
28 | + const DAY = 2; | ||
29 | + const MONTH = 3; | ||
30 | + const WEEKDAY = 4; | ||
31 | + const YEAR = 5; | ||
32 | + | ||
33 | + /** | ||
34 | + * @var array CRON expression parts | ||
35 | + */ | ||
36 | + private $cronParts; | ||
37 | + | ||
38 | + /** | ||
39 | + * @var FieldFactory CRON field factory | ||
40 | + */ | ||
41 | + private $fieldFactory; | ||
42 | + | ||
43 | + /** | ||
44 | + * @var int Max iteration count when searching for next run date | ||
45 | + */ | ||
46 | + private $maxIterationCount = 1000; | ||
47 | + | ||
48 | + /** | ||
49 | + * @var array Order in which to test of cron parts | ||
50 | + */ | ||
51 | + private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE); | ||
52 | + | ||
53 | + /** | ||
54 | + * Factory method to create a new CronExpression. | ||
55 | + * | ||
56 | + * @param string $expression The CRON expression to create. There are | ||
57 | + * several special predefined values which can be used to substitute the | ||
58 | + * CRON expression: | ||
59 | + * | ||
60 | + * `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 * | ||
61 | + * `@monthly` - Run once a month, midnight, first of month - 0 0 1 * * | ||
62 | + * `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0 | ||
63 | + * `@daily` - Run once a day, midnight - 0 0 * * * | ||
64 | + * `@hourly` - Run once an hour, first minute - 0 * * * * | ||
65 | + * @param FieldFactory $fieldFactory Field factory to use | ||
66 | + * | ||
67 | + * @return CronExpression | ||
68 | + */ | ||
69 | + public static function factory($expression, FieldFactory $fieldFactory = null) | ||
70 | + { | ||
71 | + $mappings = array( | ||
72 | + '@yearly' => '0 0 1 1 *', | ||
73 | + '@annually' => '0 0 1 1 *', | ||
74 | + '@monthly' => '0 0 1 * *', | ||
75 | + '@weekly' => '0 0 * * 0', | ||
76 | + '@daily' => '0 0 * * *', | ||
77 | + '@hourly' => '0 * * * *' | ||
78 | + ); | ||
79 | + | ||
80 | + if (isset($mappings[$expression])) { | ||
81 | + $expression = $mappings[$expression]; | ||
82 | + } | ||
83 | + | ||
84 | + return new static($expression, $fieldFactory ?: new FieldFactory()); | ||
85 | + } | ||
86 | + | ||
87 | + /** | ||
88 | + * Validate a CronExpression. | ||
89 | + * | ||
90 | + * @param string $expression The CRON expression to validate. | ||
91 | + * | ||
92 | + * @return bool True if a valid CRON expression was passed. False if not. | ||
93 | + * @see \Cron\CronExpression::factory | ||
94 | + */ | ||
95 | + public static function isValidExpression($expression) | ||
96 | + { | ||
97 | + try { | ||
98 | + self::factory($expression); | ||
99 | + } catch (InvalidArgumentException $e) { | ||
100 | + return false; | ||
101 | + } | ||
102 | + | ||
103 | + return true; | ||
104 | + } | ||
105 | + | ||
106 | + /** | ||
107 | + * Parse a CRON expression | ||
108 | + * | ||
109 | + * @param string $expression CRON expression (e.g. '8 * * * *') | ||
110 | + * @param FieldFactory $fieldFactory Factory to create cron fields | ||
111 | + */ | ||
112 | + public function __construct($expression, FieldFactory $fieldFactory) | ||
113 | + { | ||
114 | + $this->fieldFactory = $fieldFactory; | ||
115 | + $this->setExpression($expression); | ||
116 | + } | ||
117 | + | ||
118 | + /** | ||
119 | + * Set or change the CRON expression | ||
120 | + * | ||
121 | + * @param string $value CRON expression (e.g. 8 * * * *) | ||
122 | + * | ||
123 | + * @return CronExpression | ||
124 | + * @throws \InvalidArgumentException if not a valid CRON expression | ||
125 | + */ | ||
126 | + public function setExpression($value) | ||
127 | + { | ||
128 | + $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); | ||
129 | + if (count($this->cronParts) < 5) { | ||
130 | + throw new InvalidArgumentException( | ||
131 | + $value . ' is not a valid CRON expression' | ||
132 | + ); | ||
133 | + } | ||
134 | + | ||
135 | + foreach ($this->cronParts as $position => $part) { | ||
136 | + $this->setPart($position, $part); | ||
137 | + } | ||
138 | + | ||
139 | + return $this; | ||
140 | + } | ||
141 | + | ||
142 | + /** | ||
143 | + * Set part of the CRON expression | ||
144 | + * | ||
145 | + * @param int $position The position of the CRON expression to set | ||
146 | + * @param string $value The value to set | ||
147 | + * | ||
148 | + * @return CronExpression | ||
149 | + * @throws \InvalidArgumentException if the value is not valid for the part | ||
150 | + */ | ||
151 | + public function setPart($position, $value) | ||
152 | + { | ||
153 | + if (!$this->fieldFactory->getField($position)->validate($value)) { | ||
154 | + throw new InvalidArgumentException( | ||
155 | + 'Invalid CRON field value ' . $value . ' at position ' . $position | ||
156 | + ); | ||
157 | + } | ||
158 | + | ||
159 | + $this->cronParts[$position] = $value; | ||
160 | + | ||
161 | + return $this; | ||
162 | + } | ||
163 | + | ||
164 | + /** | ||
165 | + * Set max iteration count for searching next run dates | ||
166 | + * | ||
167 | + * @param int $maxIterationCount Max iteration count when searching for next run date | ||
168 | + * | ||
169 | + * @return CronExpression | ||
170 | + */ | ||
171 | + public function setMaxIterationCount($maxIterationCount) | ||
172 | + { | ||
173 | + $this->maxIterationCount = $maxIterationCount; | ||
174 | + | ||
175 | + return $this; | ||
176 | + } | ||
177 | + | ||
178 | + /** | ||
179 | + * Get a next run date relative to the current date or a specific date | ||
180 | + * | ||
181 | + * @param string|\DateTime $currentTime Relative calculation date | ||
182 | + * @param int $nth Number of matches to skip before returning a | ||
183 | + * matching next run date. 0, the default, will return the current | ||
184 | + * date and time if the next run date falls on the current date and | ||
185 | + * time. Setting this value to 1 will skip the first match and go to | ||
186 | + * the second match. Setting this value to 2 will skip the first 2 | ||
187 | + * matches and so on. | ||
188 | + * @param bool $allowCurrentDate Set to TRUE to return the current date if | ||
189 | + * it matches the cron expression. | ||
190 | + * @param null|string $timeZone Timezone to use instead of the system default | ||
191 | + * | ||
192 | + * @return \DateTime | ||
193 | + * @throws \RuntimeException on too many iterations | ||
194 | + */ | ||
195 | + public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) | ||
196 | + { | ||
197 | + return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone); | ||
198 | + } | ||
199 | + | ||
200 | + /** | ||
201 | + * Get a previous run date relative to the current date or a specific date | ||
202 | + * | ||
203 | + * @param string|\DateTime $currentTime Relative calculation date | ||
204 | + * @param int $nth Number of matches to skip before returning | ||
205 | + * @param bool $allowCurrentDate Set to TRUE to return the | ||
206 | + * current date if it matches the cron expression | ||
207 | + * @param null|string $timeZone Timezone to use instead of the system default | ||
208 | + * | ||
209 | + * @return \DateTime | ||
210 | + * @throws \RuntimeException on too many iterations | ||
211 | + * @see \Cron\CronExpression::getNextRunDate | ||
212 | + */ | ||
213 | + public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) | ||
214 | + { | ||
215 | + return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone); | ||
216 | + } | ||
217 | + | ||
218 | + /** | ||
219 | + * Get multiple run dates starting at the current date or a specific date | ||
220 | + * | ||
221 | + * @param int $total Set the total number of dates to calculate | ||
222 | + * @param string|\DateTime $currentTime Relative calculation date | ||
223 | + * @param bool $invert Set to TRUE to retrieve previous dates | ||
224 | + * @param bool $allowCurrentDate Set to TRUE to return the | ||
225 | + * current date if it matches the cron expression | ||
226 | + * @param null|string $timeZone Timezone to use instead of the system default | ||
227 | + * | ||
228 | + * @return array Returns an array of run dates | ||
229 | + */ | ||
230 | + public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null) | ||
231 | + { | ||
232 | + $matches = array(); | ||
233 | + for ($i = 0; $i < max(0, $total); $i++) { | ||
234 | + try { | ||
235 | + $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone); | ||
236 | + } catch (RuntimeException $e) { | ||
237 | + break; | ||
238 | + } | ||
239 | + } | ||
240 | + | ||
241 | + return $matches; | ||
242 | + } | ||
243 | + | ||
244 | + /** | ||
245 | + * Get all or part of the CRON expression | ||
246 | + * | ||
247 | + * @param string $part Specify the part to retrieve or NULL to get the full | ||
248 | + * cron schedule string. | ||
249 | + * | ||
250 | + * @return string|null Returns the CRON expression, a part of the | ||
251 | + * CRON expression, or NULL if the part was specified but not found | ||
252 | + */ | ||
253 | + public function getExpression($part = null) | ||
254 | + { | ||
255 | + if (null === $part) { | ||
256 | + return implode(' ', $this->cronParts); | ||
257 | + } elseif (array_key_exists($part, $this->cronParts)) { | ||
258 | + return $this->cronParts[$part]; | ||
259 | + } | ||
260 | + | ||
261 | + return null; | ||
262 | + } | ||
263 | + | ||
264 | + /** | ||
265 | + * Helper method to output the full expression. | ||
266 | + * | ||
267 | + * @return string Full CRON expression | ||
268 | + */ | ||
269 | + public function __toString() | ||
270 | + { | ||
271 | + return $this->getExpression(); | ||
272 | + } | ||
273 | + | ||
274 | + /** | ||
275 | + * Determine if the cron is due to run based on the current date or a | ||
276 | + * specific date. This method assumes that the current number of | ||
277 | + * seconds are irrelevant, and should be called once per minute. | ||
278 | + * | ||
279 | + * @param string|\DateTime $currentTime Relative calculation date | ||
280 | + * @param null|string $timeZone Timezone to use instead of the system default | ||
281 | + * | ||
282 | + * @return bool Returns TRUE if the cron is due to run or FALSE if not | ||
283 | + */ | ||
284 | + public function isDue($currentTime = 'now', $timeZone = null) | ||
285 | + { | ||
286 | + if (is_null($timeZone)) { | ||
287 | + $timeZone = date_default_timezone_get(); | ||
288 | + } | ||
289 | + | ||
290 | + if ('now' === $currentTime) { | ||
291 | + $currentDate = date('Y-m-d H:i'); | ||
292 | + $currentTime = strtotime($currentDate); | ||
293 | + } elseif ($currentTime instanceof DateTime) { | ||
294 | + $currentDate = clone $currentTime; | ||
295 | + // Ensure time in 'current' timezone is used | ||
296 | + $currentDate->setTimezone(new DateTimeZone($timeZone)); | ||
297 | + $currentDate = $currentDate->format('Y-m-d H:i'); | ||
298 | + $currentTime = strtotime($currentDate); | ||
299 | + } elseif ($currentTime instanceof DateTimeImmutable) { | ||
300 | + $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); | ||
301 | + $currentDate->setTimezone(new DateTimeZone($timeZone)); | ||
302 | + $currentDate = $currentDate->format('Y-m-d H:i'); | ||
303 | + $currentTime = strtotime($currentDate); | ||
304 | + } else { | ||
305 | + $currentTime = new DateTime($currentTime); | ||
306 | + $currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0); | ||
307 | + $currentDate = $currentTime->format('Y-m-d H:i'); | ||
308 | + $currentTime = $currentTime->getTimeStamp(); | ||
309 | + } | ||
310 | + | ||
311 | + try { | ||
312 | + return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime; | ||
313 | + } catch (Exception $e) { | ||
314 | + return false; | ||
315 | + } | ||
316 | + } | ||
317 | + | ||
318 | + /** | ||
319 | + * Get the next or previous run date of the expression relative to a date | ||
320 | + * | ||
321 | + * @param string|\DateTime $currentTime Relative calculation date | ||
322 | + * @param int $nth Number of matches to skip before returning | ||
323 | + * @param bool $invert Set to TRUE to go backwards in time | ||
324 | + * @param bool $allowCurrentDate Set to TRUE to return the | ||
325 | + * current date if it matches the cron expression | ||
326 | + * @param string|null $timeZone Timezone to use instead of the system default | ||
327 | + * | ||
328 | + * @return \DateTime | ||
329 | + * @throws \RuntimeException on too many iterations | ||
330 | + */ | ||
331 | + protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null) | ||
332 | + { | ||
333 | + if (is_null($timeZone)) { | ||
334 | + $timeZone = date_default_timezone_get(); | ||
335 | + } | ||
336 | + | ||
337 | + if ($currentTime instanceof DateTime) { | ||
338 | + $currentDate = clone $currentTime; | ||
339 | + } elseif ($currentTime instanceof DateTimeImmutable) { | ||
340 | + $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); | ||
341 | + $currentDate->setTimezone($currentTime->getTimezone()); | ||
342 | + } else { | ||
343 | + $currentDate = new DateTime($currentTime ?: 'now'); | ||
344 | + $currentDate->setTimezone(new DateTimeZone($timeZone)); | ||
345 | + } | ||
346 | + | ||
347 | + $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); | ||
348 | + $nextRun = clone $currentDate; | ||
349 | + $nth = (int) $nth; | ||
350 | + | ||
351 | + // We don't have to satisfy * or null fields | ||
352 | + $parts = array(); | ||
353 | + $fields = array(); | ||
354 | + foreach (self::$order as $position) { | ||
355 | + $part = $this->getExpression($position); | ||
356 | + if (null === $part || '*' === $part) { | ||
357 | + continue; | ||
358 | + } | ||
359 | + $parts[$position] = $part; | ||
360 | + $fields[$position] = $this->fieldFactory->getField($position); | ||
361 | + } | ||
362 | + | ||
363 | + // Set a hard limit to bail on an impossible date | ||
364 | + for ($i = 0; $i < $this->maxIterationCount; $i++) { | ||
365 | + | ||
366 | + foreach ($parts as $position => $part) { | ||
367 | + $satisfied = false; | ||
368 | + // Get the field object used to validate this part | ||
369 | + $field = $fields[$position]; | ||
370 | + // Check if this is singular or a list | ||
371 | + if (strpos($part, ',') === false) { | ||
372 | + $satisfied = $field->isSatisfiedBy($nextRun, $part); | ||
373 | + } else { | ||
374 | + foreach (array_map('trim', explode(',', $part)) as $listPart) { | ||
375 | + if ($field->isSatisfiedBy($nextRun, $listPart)) { | ||
376 | + $satisfied = true; | ||
377 | + break; | ||
378 | + } | ||
379 | + } | ||
380 | + } | ||
381 | + | ||
382 | + // If the field is not satisfied, then start over | ||
383 | + if (!$satisfied) { | ||
384 | + $field->increment($nextRun, $invert, $part); | ||
385 | + continue 2; | ||
386 | + } | ||
387 | + } | ||
388 | + | ||
389 | + // Skip this match if needed | ||
390 | + if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { | ||
391 | + $this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null); | ||
392 | + continue; | ||
393 | + } | ||
394 | + | ||
395 | + return $nextRun; | ||
396 | + } | ||
397 | + | ||
398 | + // @codeCoverageIgnoreStart | ||
399 | + throw new RuntimeException('Impossible CRON expression'); | ||
400 | + // @codeCoverageIgnoreEnd | ||
401 | + } | ||
402 | +} |
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | + | ||
5 | +use DateTime; | ||
6 | + | ||
7 | +/** | ||
8 | + * Day of month field. Allows: * , / - ? L W | ||
9 | + * | ||
10 | + * 'L' stands for "last" and specifies the last day of the month. | ||
11 | + * | ||
12 | + * The 'W' character is used to specify the weekday (Monday-Friday) nearest the | ||
13 | + * given day. As an example, if you were to specify "15W" as the value for the | ||
14 | + * day-of-month field, the meaning is: "the nearest weekday to the 15th of the | ||
15 | + * month". So if the 15th is a Saturday, the trigger will fire on Friday the | ||
16 | + * 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If | ||
17 | + * the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you | ||
18 | + * specify "1W" as the value for day-of-month, and the 1st is a Saturday, the | ||
19 | + * trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary | ||
20 | + * of a month's days. The 'W' character can only be specified when the | ||
21 | + * day-of-month is a single day, not a range or list of days. | ||
22 | + * | ||
23 | + * @author Michael Dowling <mtdowling@gmail.com> | ||
24 | + */ | ||
25 | +class DayOfMonthField extends AbstractField | ||
26 | +{ | ||
27 | + protected $rangeStart = 1; | ||
28 | + protected $rangeEnd = 31; | ||
29 | + | ||
30 | + /** | ||
31 | + * Get the nearest day of the week for a given day in a month | ||
32 | + * | ||
33 | + * @param int $currentYear Current year | ||
34 | + * @param int $currentMonth Current month | ||
35 | + * @param int $targetDay Target day of the month | ||
36 | + * | ||
37 | + * @return \DateTime Returns the nearest date | ||
38 | + */ | ||
39 | + private static function getNearestWeekday($currentYear, $currentMonth, $targetDay) | ||
40 | + { | ||
41 | + $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT); | ||
42 | + $target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday"); | ||
43 | + $currentWeekday = (int) $target->format('N'); | ||
44 | + | ||
45 | + if ($currentWeekday < 6) { | ||
46 | + return $target; | ||
47 | + } | ||
48 | + | ||
49 | + $lastDayOfMonth = $target->format('t'); | ||
50 | + | ||
51 | + foreach (array(-1, 1, -2, 2) as $i) { | ||
52 | + $adjusted = $targetDay + $i; | ||
53 | + if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { | ||
54 | + $target->setDate($currentYear, $currentMonth, $adjusted); | ||
55 | + if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { | ||
56 | + return $target; | ||
57 | + } | ||
58 | + } | ||
59 | + } | ||
60 | + } | ||
61 | + | ||
62 | + public function isSatisfiedBy(DateTime $date, $value) | ||
63 | + { | ||
64 | + // ? states that the field value is to be skipped | ||
65 | + if ($value == '?') { | ||
66 | + return true; | ||
67 | + } | ||
68 | + | ||
69 | + $fieldValue = $date->format('d'); | ||
70 | + | ||
71 | + // Check to see if this is the last day of the month | ||
72 | + if ($value == 'L') { | ||
73 | + return $fieldValue == $date->format('t'); | ||
74 | + } | ||
75 | + | ||
76 | + // Check to see if this is the nearest weekday to a particular value | ||
77 | + if (strpos($value, 'W')) { | ||
78 | + // Parse the target day | ||
79 | + $targetDay = substr($value, 0, strpos($value, 'W')); | ||
80 | + // Find out if the current day is the nearest day of the week | ||
81 | + return $date->format('j') == self::getNearestWeekday( | ||
82 | + $date->format('Y'), | ||
83 | + $date->format('m'), | ||
84 | + $targetDay | ||
85 | + )->format('j'); | ||
86 | + } | ||
87 | + | ||
88 | + return $this->isSatisfied($date->format('d'), $value); | ||
89 | + } | ||
90 | + | ||
91 | + public function increment(DateTime $date, $invert = false) | ||
92 | + { | ||
93 | + if ($invert) { | ||
94 | + $date->modify('previous day'); | ||
95 | + $date->setTime(23, 59); | ||
96 | + } else { | ||
97 | + $date->modify('next day'); | ||
98 | + $date->setTime(0, 0); | ||
99 | + } | ||
100 | + | ||
101 | + return $this; | ||
102 | + } | ||
103 | + | ||
104 | + /** | ||
105 | + * @inheritDoc | ||
106 | + */ | ||
107 | + public function validate($value) | ||
108 | + { | ||
109 | + $basicChecks = parent::validate($value); | ||
110 | + | ||
111 | + // Validate that a list don't have W or L | ||
112 | + if (strpos($value, ',') !== false && (strpos($value, 'W') !== false || strpos($value, 'L') !== false)) { | ||
113 | + return false; | ||
114 | + } | ||
115 | + | ||
116 | + if (!$basicChecks) { | ||
117 | + | ||
118 | + if ($value === 'L') { | ||
119 | + return true; | ||
120 | + } | ||
121 | + | ||
122 | + if (preg_match('/^(.*)W$/', $value, $matches)) { | ||
123 | + return $this->validate($matches[1]); | ||
124 | + } | ||
125 | + | ||
126 | + return false; | ||
127 | + } | ||
128 | + | ||
129 | + return $basicChecks; | ||
130 | + } | ||
131 | +} |
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | + | ||
5 | +use DateTime; | ||
6 | +use InvalidArgumentException; | ||
7 | + | ||
8 | + | ||
9 | +/** | ||
10 | + * Day of week field. Allows: * / , - ? L # | ||
11 | + * | ||
12 | + * Days of the week can be represented as a number 0-7 (0|7 = Sunday) | ||
13 | + * or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT. | ||
14 | + * | ||
15 | + * 'L' stands for "last". It allows you to specify constructs such as | ||
16 | + * "the last Friday" of a given month. | ||
17 | + * | ||
18 | + * '#' is allowed for the day-of-week field, and must be followed by a | ||
19 | + * number between one and five. It allows you to specify constructs such as | ||
20 | + * "the second Friday" of a given month. | ||
21 | + */ | ||
22 | +class DayOfWeekField extends AbstractField | ||
23 | +{ | ||
24 | + protected $rangeStart = 0; | ||
25 | + protected $rangeEnd = 7; | ||
26 | + | ||
27 | + protected $nthRange; | ||
28 | + | ||
29 | + protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; | ||
30 | + | ||
31 | + public function __construct() | ||
32 | + { | ||
33 | + $this->nthRange = range(1, 5); | ||
34 | + parent::__construct(); | ||
35 | + } | ||
36 | + | ||
37 | + public function isSatisfiedBy(DateTime $date, $value) | ||
38 | + { | ||
39 | + if ($value == '?') { | ||
40 | + return true; | ||
41 | + } | ||
42 | + | ||
43 | + // Convert text day of the week values to integers | ||
44 | + $value = $this->convertLiterals($value); | ||
45 | + | ||
46 | + $currentYear = $date->format('Y'); | ||
47 | + $currentMonth = $date->format('m'); | ||
48 | + $lastDayOfMonth = $date->format('t'); | ||
49 | + | ||
50 | + // Find out if this is the last specific weekday of the month | ||
51 | + if (strpos($value, 'L')) { | ||
52 | + $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L'))); | ||
53 | + $tdate = clone $date; | ||
54 | + $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); | ||
55 | + while ($tdate->format('w') != $weekday) { | ||
56 | + $tdateClone = new DateTime(); | ||
57 | + $tdate = $tdateClone | ||
58 | + ->setTimezone($tdate->getTimezone()) | ||
59 | + ->setDate($currentYear, $currentMonth, --$lastDayOfMonth); | ||
60 | + } | ||
61 | + | ||
62 | + return $date->format('j') == $lastDayOfMonth; | ||
63 | + } | ||
64 | + | ||
65 | + // Handle # hash tokens | ||
66 | + if (strpos($value, '#')) { | ||
67 | + list($weekday, $nth) = explode('#', $value); | ||
68 | + | ||
69 | + if (!is_numeric($nth)) { | ||
70 | + throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given"); | ||
71 | + } else { | ||
72 | + $nth = (int) $nth; | ||
73 | + } | ||
74 | + | ||
75 | + // 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601 | ||
76 | + if ($weekday === '0') { | ||
77 | + $weekday = 7; | ||
78 | + } | ||
79 | + | ||
80 | + $weekday = $this->convertLiterals($weekday); | ||
81 | + | ||
82 | + // Validate the hash fields | ||
83 | + if ($weekday < 0 || $weekday > 7) { | ||
84 | + throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given"); | ||
85 | + } | ||
86 | + | ||
87 | + if (!in_array($nth, $this->nthRange)) { | ||
88 | + throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given"); | ||
89 | + } | ||
90 | + | ||
91 | + // The current weekday must match the targeted weekday to proceed | ||
92 | + if ($date->format('N') != $weekday) { | ||
93 | + return false; | ||
94 | + } | ||
95 | + | ||
96 | + $tdate = clone $date; | ||
97 | + $tdate->setDate($currentYear, $currentMonth, 1); | ||
98 | + $dayCount = 0; | ||
99 | + $currentDay = 1; | ||
100 | + while ($currentDay < $lastDayOfMonth + 1) { | ||
101 | + if ($tdate->format('N') == $weekday) { | ||
102 | + if (++$dayCount >= $nth) { | ||
103 | + break; | ||
104 | + } | ||
105 | + } | ||
106 | + $tdate->setDate($currentYear, $currentMonth, ++$currentDay); | ||
107 | + } | ||
108 | + | ||
109 | + return $date->format('j') == $currentDay; | ||
110 | + } | ||
111 | + | ||
112 | + // Handle day of the week values | ||
113 | + if (strpos($value, '-')) { | ||
114 | + $parts = explode('-', $value); | ||
115 | + if ($parts[0] == '7') { | ||
116 | + $parts[0] = '0'; | ||
117 | + } elseif ($parts[1] == '0') { | ||
118 | + $parts[1] = '7'; | ||
119 | + } | ||
120 | + $value = implode('-', $parts); | ||
121 | + } | ||
122 | + | ||
123 | + // Test to see which Sunday to use -- 0 == 7 == Sunday | ||
124 | + $format = in_array(7, str_split($value)) ? 'N' : 'w'; | ||
125 | + $fieldValue = $date->format($format); | ||
126 | + | ||
127 | + return $this->isSatisfied($fieldValue, $value); | ||
128 | + } | ||
129 | + | ||
130 | + public function increment(DateTime $date, $invert = false) | ||
131 | + { | ||
132 | + if ($invert) { | ||
133 | + $date->modify('-1 day'); | ||
134 | + $date->setTime(23, 59, 0); | ||
135 | + } else { | ||
136 | + $date->modify('+1 day'); | ||
137 | + $date->setTime(0, 0, 0); | ||
138 | + } | ||
139 | + | ||
140 | + return $this; | ||
141 | + } | ||
142 | + | ||
143 | + /** | ||
144 | + * @inheritDoc | ||
145 | + */ | ||
146 | + public function validate($value) | ||
147 | + { | ||
148 | + $basicChecks = parent::validate($value); | ||
149 | + | ||
150 | + if (!$basicChecks) { | ||
151 | + // Handle the # value | ||
152 | + if (strpos($value, '#') !== false) { | ||
153 | + $chunks = explode('#', $value); | ||
154 | + $chunks[0] = $this->convertLiterals($chunks[0]); | ||
155 | + | ||
156 | + if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && in_array($chunks[1], $this->nthRange)) { | ||
157 | + return true; | ||
158 | + } | ||
159 | + } | ||
160 | + | ||
161 | + if (preg_match('/^(.*)L$/', $value, $matches)) { | ||
162 | + return $this->validate($matches[1]); | ||
163 | + } | ||
164 | + | ||
165 | + return false; | ||
166 | + } | ||
167 | + | ||
168 | + return $basicChecks; | ||
169 | + } | ||
170 | +} |
addons/crontab/library/Cron/FieldFactory.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | + | ||
5 | +use InvalidArgumentException; | ||
6 | + | ||
7 | +/** | ||
8 | + * CRON field factory implementing a flyweight factory | ||
9 | + * @link http://en.wikipedia.org/wiki/Cron | ||
10 | + */ | ||
11 | +class FieldFactory | ||
12 | +{ | ||
13 | + /** | ||
14 | + * @var array Cache of instantiated fields | ||
15 | + */ | ||
16 | + private $fields = array(); | ||
17 | + | ||
18 | + /** | ||
19 | + * Get an instance of a field object for a cron expression position | ||
20 | + * | ||
21 | + * @param int $position CRON expression position value to retrieve | ||
22 | + * | ||
23 | + * @return FieldInterface | ||
24 | + * @throws InvalidArgumentException if a position is not valid | ||
25 | + */ | ||
26 | + public function getField($position) | ||
27 | + { | ||
28 | + if (!isset($this->fields[$position])) { | ||
29 | + switch ($position) { | ||
30 | + case 0: | ||
31 | + $this->fields[$position] = new MinutesField(); | ||
32 | + break; | ||
33 | + case 1: | ||
34 | + $this->fields[$position] = new HoursField(); | ||
35 | + break; | ||
36 | + case 2: | ||
37 | + $this->fields[$position] = new DayOfMonthField(); | ||
38 | + break; | ||
39 | + case 3: | ||
40 | + $this->fields[$position] = new MonthField(); | ||
41 | + break; | ||
42 | + case 4: | ||
43 | + $this->fields[$position] = new DayOfWeekField(); | ||
44 | + break; | ||
45 | + default: | ||
46 | + throw new InvalidArgumentException( | ||
47 | + $position . ' is not a valid position' | ||
48 | + ); | ||
49 | + } | ||
50 | + } | ||
51 | + | ||
52 | + return $this->fields[$position]; | ||
53 | + } | ||
54 | +} |
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | +use DateTime; | ||
5 | + | ||
6 | +/** | ||
7 | + * CRON field interface | ||
8 | + */ | ||
9 | +interface FieldInterface | ||
10 | +{ | ||
11 | + /** | ||
12 | + * Check if the respective value of a DateTime field satisfies a CRON exp | ||
13 | + * | ||
14 | + * @param DateTime $date DateTime object to check | ||
15 | + * @param string $value CRON expression to test against | ||
16 | + * | ||
17 | + * @return bool Returns TRUE if satisfied, FALSE otherwise | ||
18 | + */ | ||
19 | + public function isSatisfiedBy(DateTime $date, $value); | ||
20 | + | ||
21 | + /** | ||
22 | + * When a CRON expression is not satisfied, this method is used to increment | ||
23 | + * or decrement a DateTime object by the unit of the cron field | ||
24 | + * | ||
25 | + * @param DateTime $date DateTime object to change | ||
26 | + * @param bool $invert (optional) Set to TRUE to decrement | ||
27 | + * | ||
28 | + * @return FieldInterface | ||
29 | + */ | ||
30 | + public function increment(DateTime $date, $invert = false); | ||
31 | + | ||
32 | + /** | ||
33 | + * Validates a CRON expression for a given field | ||
34 | + * | ||
35 | + * @param string $value CRON expression value to validate | ||
36 | + * | ||
37 | + * @return bool Returns TRUE if valid, FALSE otherwise | ||
38 | + */ | ||
39 | + public function validate($value); | ||
40 | +} |
addons/crontab/library/Cron/HoursField.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | +use DateTime; | ||
5 | +use DateTimeZone; | ||
6 | + | ||
7 | + | ||
8 | +/** | ||
9 | + * Hours field. Allows: * , / - | ||
10 | + */ | ||
11 | +class HoursField extends AbstractField | ||
12 | +{ | ||
13 | + protected $rangeStart = 0; | ||
14 | + protected $rangeEnd = 23; | ||
15 | + | ||
16 | + public function isSatisfiedBy(DateTime $date, $value) | ||
17 | + { | ||
18 | + return $this->isSatisfied($date->format('H'), $value); | ||
19 | + } | ||
20 | + | ||
21 | + public function increment(DateTime $date, $invert = false, $parts = null) | ||
22 | + { | ||
23 | + // Change timezone to UTC temporarily. This will | ||
24 | + // allow us to go back or forwards and hour even | ||
25 | + // if DST will be changed between the hours. | ||
26 | + if (is_null($parts) || $parts == '*') { | ||
27 | + $timezone = $date->getTimezone(); | ||
28 | + $date->setTimezone(new DateTimeZone('UTC')); | ||
29 | + if ($invert) { | ||
30 | + $date->modify('-1 hour'); | ||
31 | + } else { | ||
32 | + $date->modify('+1 hour'); | ||
33 | + } | ||
34 | + $date->setTimezone($timezone); | ||
35 | + | ||
36 | + $date->setTime($date->format('H'), $invert ? 59 : 0); | ||
37 | + return $this; | ||
38 | + } | ||
39 | + | ||
40 | + $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts); | ||
41 | + $hours = array(); | ||
42 | + foreach ($parts as $part) { | ||
43 | + $hours = array_merge($hours, $this->getRangeForExpression($part, 23)); | ||
44 | + } | ||
45 | + | ||
46 | + $current_hour = $date->format('H'); | ||
47 | + $position = $invert ? count($hours) - 1 : 0; | ||
48 | + if (count($hours) > 1) { | ||
49 | + for ($i = 0; $i < count($hours) - 1; $i++) { | ||
50 | + if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) || | ||
51 | + ($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) { | ||
52 | + $position = $invert ? $i : $i + 1; | ||
53 | + break; | ||
54 | + } | ||
55 | + } | ||
56 | + } | ||
57 | + | ||
58 | + $hour = $hours[$position]; | ||
59 | + if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) { | ||
60 | + $date->modify(($invert ? '-' : '+') . '1 day'); | ||
61 | + $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); | ||
62 | + } | ||
63 | + else { | ||
64 | + $date->setTime($hour, $invert ? 59 : 0); | ||
65 | + } | ||
66 | + | ||
67 | + return $this; | ||
68 | + } | ||
69 | +} |
addons/crontab/library/Cron/MinutesField.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | + | ||
5 | +use DateTime; | ||
6 | + | ||
7 | + | ||
8 | +/** | ||
9 | + * Minutes field. Allows: * , / - | ||
10 | + */ | ||
11 | +class MinutesField extends AbstractField | ||
12 | +{ | ||
13 | + protected $rangeStart = 0; | ||
14 | + protected $rangeEnd = 59; | ||
15 | + | ||
16 | + public function isSatisfiedBy(DateTime $date, $value) | ||
17 | + { | ||
18 | + return $this->isSatisfied($date->format('i'), $value); | ||
19 | + } | ||
20 | + | ||
21 | + public function increment(DateTime $date, $invert = false, $parts = null) | ||
22 | + { | ||
23 | + if (is_null($parts)) { | ||
24 | + if ($invert) { | ||
25 | + $date->modify('-1 minute'); | ||
26 | + } else { | ||
27 | + $date->modify('+1 minute'); | ||
28 | + } | ||
29 | + return $this; | ||
30 | + } | ||
31 | + | ||
32 | + $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts); | ||
33 | + $minutes = array(); | ||
34 | + foreach ($parts as $part) { | ||
35 | + $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59)); | ||
36 | + } | ||
37 | + | ||
38 | + $current_minute = $date->format('i'); | ||
39 | + $position = $invert ? count($minutes) - 1 : 0; | ||
40 | + if (count($minutes) > 1) { | ||
41 | + for ($i = 0; $i < count($minutes) - 1; $i++) { | ||
42 | + if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) || | ||
43 | + ($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) { | ||
44 | + $position = $invert ? $i : $i + 1; | ||
45 | + break; | ||
46 | + } | ||
47 | + } | ||
48 | + } | ||
49 | + | ||
50 | + if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) { | ||
51 | + $date->modify(($invert ? '-' : '+') . '1 hour'); | ||
52 | + $date->setTime($date->format('H'), $invert ? 59 : 0); | ||
53 | + } | ||
54 | + else { | ||
55 | + $date->setTime($date->format('H'), $minutes[$position]); | ||
56 | + } | ||
57 | + | ||
58 | + return $this; | ||
59 | + } | ||
60 | +} |
addons/crontab/library/Cron/MonthField.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace Cron; | ||
4 | + | ||
5 | +use DateTime; | ||
6 | + | ||
7 | +/** | ||
8 | + * Month field. Allows: * , / - | ||
9 | + */ | ||
10 | +class MonthField extends AbstractField | ||
11 | +{ | ||
12 | + protected $rangeStart = 1; | ||
13 | + protected $rangeEnd = 12; | ||
14 | + protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', | ||
15 | + 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC']; | ||
16 | + | ||
17 | + public function isSatisfiedBy(DateTime $date, $value) | ||
18 | + { | ||
19 | + $value = $this->convertLiterals($value); | ||
20 | + | ||
21 | + return $this->isSatisfied($date->format('m'), $value); | ||
22 | + } | ||
23 | + | ||
24 | + public function increment(DateTime $date, $invert = false) | ||
25 | + { | ||
26 | + if ($invert) { | ||
27 | + $date->modify('last day of previous month'); | ||
28 | + $date->setTime(23, 59); | ||
29 | + } else { | ||
30 | + $date->modify('first day of next month'); | ||
31 | + $date->setTime(0, 0); | ||
32 | + } | ||
33 | + | ||
34 | + return $this; | ||
35 | + } | ||
36 | + | ||
37 | + | ||
38 | +} |
addons/crontab/model/Crontab.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace addons\crontab\model; | ||
4 | + | ||
5 | +use think\Model; | ||
6 | + | ||
7 | +class Crontab extends Model | ||
8 | +{ | ||
9 | + | ||
10 | + // 开启自动写入时间戳字段 | ||
11 | + protected $autoWriteTimestamp = 'integer'; | ||
12 | + // 定义时间戳字段名 | ||
13 | + protected $createTime = 'createtime'; | ||
14 | + protected $updateTime = 'updatetime'; | ||
15 | + // 定义字段类型 | ||
16 | + protected $type = [ | ||
17 | + ]; | ||
18 | + // 追加属性 | ||
19 | + protected $append = [ | ||
20 | + 'type_text' | ||
21 | + ]; | ||
22 | + | ||
23 | + public static function getTypeList() | ||
24 | + { | ||
25 | + return [ | ||
26 | + 'url' => __('Request Url'), | ||
27 | + 'sql' => __('Execute Sql Script'), | ||
28 | + 'shell' => __('Execute Shell'), | ||
29 | + ]; | ||
30 | + } | ||
31 | + | ||
32 | + public function getTypeTextAttr($value, $data) | ||
33 | + { | ||
34 | + $typelist = self::getTypeList(); | ||
35 | + $value = $value ? $value : $data['type']; | ||
36 | + return $value && isset($typelist[$value]) ? $typelist[$value] : $value; | ||
37 | + } | ||
38 | + | ||
39 | + protected function setBegintimeAttr($value) | ||
40 | + { | ||
41 | + return $value && !is_numeric($value) ? strtotime($value) : $value; | ||
42 | + } | ||
43 | + | ||
44 | + protected function setEndtimeAttr($value) | ||
45 | + { | ||
46 | + return $value && !is_numeric($value) ? strtotime($value) : $value; | ||
47 | + } | ||
48 | + | ||
49 | + protected function setExecutetimeAttr($value) | ||
50 | + { | ||
51 | + return $value && !is_numeric($value) ? strtotime($value) : $value; | ||
52 | + } | ||
53 | + | ||
54 | +} |
1 | +<?php | ||
2 | + | ||
3 | +namespace app\admin\controller\general; | ||
4 | + | ||
5 | +use app\common\controller\Backend; | ||
6 | +use Cron\CronExpression; | ||
7 | + | ||
8 | +/** | ||
9 | + * 定时任务 | ||
10 | + * | ||
11 | + * @icon fa fa-tasks | ||
12 | + * @remark 按照设定的时间进行任务的执行,目前支持三种任务:请求URL、执行SQL、执行Shell。 | ||
13 | + */ | ||
14 | +class Crontab extends Backend | ||
15 | +{ | ||
16 | + | ||
17 | + protected $model = null; | ||
18 | + protected $noNeedRight = ['check_schedule', 'get_schedule_future']; | ||
19 | + | ||
20 | + public function _initialize() | ||
21 | + { | ||
22 | + parent::_initialize(); | ||
23 | + $this->model = model('Crontab'); | ||
24 | + $this->view->assign('typeList', \app\admin\model\Crontab::getTypeList()); | ||
25 | + $this->assignconfig('typeList', \app\admin\model\Crontab::getTypeList()); | ||
26 | + } | ||
27 | + | ||
28 | + /** | ||
29 | + * 查看 | ||
30 | + */ | ||
31 | + public function index() | ||
32 | + { | ||
33 | + if ($this->request->isAjax()) { | ||
34 | + list($where, $sort, $order, $offset, $limit) = $this->buildparams(); | ||
35 | + $total = $this->model | ||
36 | + ->where($where) | ||
37 | + ->order($sort, $order) | ||
38 | + ->count(); | ||
39 | + | ||
40 | + $list = $this->model | ||
41 | + ->where($where) | ||
42 | + ->order($sort, $order) | ||
43 | + ->limit($offset, $limit) | ||
44 | + ->select(); | ||
45 | + $time = time(); | ||
46 | + foreach ($list as $k => &$v) { | ||
47 | + $cron = CronExpression::factory($v['schedule']); | ||
48 | + $v['nexttime'] = $time > $v['endtime'] ? __('None') : $cron->getNextRunDate()->getTimestamp(); | ||
49 | + } | ||
50 | + $result = array("total" => $total, "rows" => $list); | ||
51 | + | ||
52 | + return json($result); | ||
53 | + } | ||
54 | + return $this->view->fetch(); | ||
55 | + } | ||
56 | + | ||
57 | + /** | ||
58 | + * 判断Crontab格式是否正确 | ||
59 | + * @internal | ||
60 | + */ | ||
61 | + public function check_schedule() | ||
62 | + { | ||
63 | + $row = $this->request->post("row/a"); | ||
64 | + $schedule = isset($row['schedule']) ? $row['schedule'] : ''; | ||
65 | + if (CronExpression::isValidExpression($schedule)) { | ||
66 | + $this->success(); | ||
67 | + } else { | ||
68 | + $this->error(__('Crontab format invalid')); | ||
69 | + } | ||
70 | + } | ||
71 | + | ||
72 | + /** | ||
73 | + * 根据Crontab表达式读取未来七次的时间 | ||
74 | + * @internal | ||
75 | + */ | ||
76 | + public function get_schedule_future() | ||
77 | + { | ||
78 | + $time = []; | ||
79 | + $schedule = $this->request->post('schedule'); | ||
80 | + $days = (int)$this->request->post('days'); | ||
81 | + try { | ||
82 | + $cron = CronExpression::factory($schedule); | ||
83 | + for ($i = 0; $i < $days; $i++) { | ||
84 | + $time[] = $cron->getNextRunDate(null, $i)->format('Y-m-d H:i:s'); | ||
85 | + } | ||
86 | + } catch (\Exception $e) { | ||
87 | + | ||
88 | + } | ||
89 | + | ||
90 | + $this->success("", null, ['futuretime' => $time]); | ||
91 | + } | ||
92 | + | ||
93 | +} |
1 | +<?php | ||
2 | + | ||
3 | +namespace app\admin\controller\general; | ||
4 | + | ||
5 | +use app\common\controller\Backend; | ||
6 | + | ||
7 | +/** | ||
8 | + * 定时任务 | ||
9 | + * | ||
10 | + * @icon fa fa-tasks | ||
11 | + * @remark 类似于Linux的Crontab定时任务,可以按照设定的时间进行任务的执行 | ||
12 | + */ | ||
13 | +class CrontabLog extends Backend | ||
14 | +{ | ||
15 | + | ||
16 | + protected $model = null; | ||
17 | + | ||
18 | + public function _initialize() | ||
19 | + { | ||
20 | + parent::_initialize(); | ||
21 | + $this->model = model('CrontabLog'); | ||
22 | + $this->view->assign('statusList', $this->model->getStatusList()); | ||
23 | + $this->assignconfig('statusList', $this->model->getStatusList()); | ||
24 | + } | ||
25 | + | ||
26 | + /** | ||
27 | + * 查看 | ||
28 | + */ | ||
29 | + public function index() | ||
30 | + { | ||
31 | + if ($this->request->isAjax()) { | ||
32 | + list($where, $sort, $order, $offset, $limit) = $this->buildparams(); | ||
33 | + $total = $this->model | ||
34 | + ->where($where) | ||
35 | + ->order($sort, $order) | ||
36 | + ->count(); | ||
37 | + | ||
38 | + $list = $this->model | ||
39 | + ->where($where) | ||
40 | + ->order($sort, $order) | ||
41 | + ->limit($offset, $limit) | ||
42 | + ->select(); | ||
43 | + $list = collection($list)->toArray(); | ||
44 | + $result = array("total" => $total, "rows" => $list); | ||
45 | + | ||
46 | + return json($result); | ||
47 | + } | ||
48 | + return $this->view->fetch(); | ||
49 | + } | ||
50 | + | ||
51 | + public function detail($ids = null) | ||
52 | + { | ||
53 | + $row = $this->model->get($ids); | ||
54 | + if (!$row) { | ||
55 | + $this->error(__('No Results were found')); | ||
56 | + } | ||
57 | + $this->view->assign("row", $row); | ||
58 | + return $this->view->fetch(); | ||
59 | + } | ||
60 | + | ||
61 | +} |
1 | +<?php | ||
2 | + | ||
3 | +return [ | ||
4 | + 'Title' => '任务标题', | ||
5 | + 'Maximums' => '最多执行', | ||
6 | + 'Sleep' => '延迟秒数', | ||
7 | + 'Schedule' => '执行周期', | ||
8 | + 'Executes' => '执行次数', | ||
9 | + 'Completed' => '已完成', | ||
10 | + 'Expired' => '已过期', | ||
11 | + 'Hidden' => '已禁用', | ||
12 | + 'Logs' => '日志信息', | ||
13 | + 'Crontab rules' => 'Crontab规则', | ||
14 | + 'No limit' => '无限制', | ||
15 | + 'Execute time' => '最后执行时间', | ||
16 | + 'Request Url' => '请求URL', | ||
17 | + 'Execute Sql Script' => '执行SQL', | ||
18 | + 'Execute Shell' => '执行Shell', | ||
19 | + 'Crontab format invalid' => 'Crontab格式错误', | ||
20 | + 'Next execute time' => '下次预计时间', | ||
21 | + 'The next %s times the execution time' => '接下来 %s 次的执行时间', | ||
22 | +]; |
application/admin/model/Crontab.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace app\admin\model; | ||
4 | + | ||
5 | +use think\Model; | ||
6 | + | ||
7 | +class Crontab extends Model | ||
8 | +{ | ||
9 | + | ||
10 | + // 开启自动写入时间戳字段 | ||
11 | + protected $autoWriteTimestamp = 'int'; | ||
12 | + // 定义时间戳字段名 | ||
13 | + protected $createTime = 'createtime'; | ||
14 | + protected $updateTime = 'updatetime'; | ||
15 | + // 定义字段类型 | ||
16 | + protected $type = [ | ||
17 | + ]; | ||
18 | + // 追加属性 | ||
19 | + protected $append = [ | ||
20 | + 'type_text' | ||
21 | + ]; | ||
22 | + | ||
23 | + public static function getTypeList() | ||
24 | + { | ||
25 | + return [ | ||
26 | + 'url' => __('Request Url'), | ||
27 | + 'sql' => __('Execute Sql Script'), | ||
28 | + 'shell' => __('Execute Shell'), | ||
29 | + ]; | ||
30 | + } | ||
31 | + | ||
32 | + public function getTypeTextAttr($value, $data) | ||
33 | + { | ||
34 | + $typelist = self::getTypeList(); | ||
35 | + $value = $value ? $value : $data['type']; | ||
36 | + return $value && isset($typelist[$value]) ? $typelist[$value] : $value; | ||
37 | + } | ||
38 | + | ||
39 | + protected function setBegintimeAttr($value) | ||
40 | + { | ||
41 | + return $value && !is_numeric($value) ? strtotime($value) : $value; | ||
42 | + } | ||
43 | + | ||
44 | + protected function setEndtimeAttr($value) | ||
45 | + { | ||
46 | + return $value && !is_numeric($value) ? strtotime($value) : $value; | ||
47 | + } | ||
48 | + | ||
49 | + protected function setExecutetimeAttr($value) | ||
50 | + { | ||
51 | + return $value && !is_numeric($value) ? strtotime($value) : $value; | ||
52 | + } | ||
53 | + | ||
54 | +} |
application/admin/model/CrontabLog.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace app\admin\model; | ||
4 | + | ||
5 | +use think\Model; | ||
6 | + | ||
7 | +class CrontabLog extends Model | ||
8 | +{ | ||
9 | + | ||
10 | + // 开启自动写入时间戳字段 | ||
11 | + protected $autoWriteTimestamp = 'int'; | ||
12 | + // 定义时间戳字段名 | ||
13 | + protected $createTime = false; | ||
14 | + protected $updateTime = false; | ||
15 | + // 定义字段类型 | ||
16 | + protected $type = [ | ||
17 | + ]; | ||
18 | + // 追加属性 | ||
19 | + protected $append = [ | ||
20 | + ]; | ||
21 | + | ||
22 | + public function getStatusList() | ||
23 | + { | ||
24 | + return ['normal' => __('Normal'), 'hidden' => __('Hidden')]; | ||
25 | + } | ||
26 | + | ||
27 | + public function getStatusTextAttr($value, $data) | ||
28 | + { | ||
29 | + $value = $value ? $value : (isset($data['status']) ? $data['status'] : ''); | ||
30 | + $list = $this->getStatusList(); | ||
31 | + return isset($list[$value]) ? $list[$value] : ''; | ||
32 | + } | ||
33 | + | ||
34 | +} |
1 | +<style type="text/css"> | ||
2 | + #schedulepicker { | ||
3 | + padding-top:7px; | ||
4 | + } | ||
5 | + #schedulepicker h5 { | ||
6 | + line-height: 30px; | ||
7 | + } | ||
8 | + #schedulepicker .list-group { | ||
9 | + margin-bottom: 0; | ||
10 | + } | ||
11 | +</style> | ||
12 | +<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action=""> | ||
13 | + <div class="form-group"> | ||
14 | + <label for="title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label> | ||
15 | + <div class="col-xs-12 col-sm-8"> | ||
16 | + <input type="text" class="form-control" id="title" name="row[title]" value="" data-rule="required" /> | ||
17 | + </div> | ||
18 | + </div> | ||
19 | + <div class="form-group"> | ||
20 | + <label for="type" class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label> | ||
21 | + <div class="col-xs-12 col-sm-8"> | ||
22 | + {:build_select('row[type]', $typeList, null, ['class'=>'form-control', 'data-rule'=>'required'])} | ||
23 | + </div> | ||
24 | + </div> | ||
25 | + <div class="form-group"> | ||
26 | + <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label> | ||
27 | + <div class="col-xs-12 col-sm-8"> | ||
28 | + <textarea name="row[content]" id="c-content" cols="30" rows="5" class="form-control" data-rule="required"></textarea> | ||
29 | + </div> | ||
30 | + </div> | ||
31 | + <div class="form-group"> | ||
32 | + <label for="schedule" class="control-label col-xs-12 col-sm-2">{:__('Schedule')}:</label> | ||
33 | + <div class="col-xs-12 col-sm-8"> | ||
34 | + <div class="input-group margin-bottom-sm"> | ||
35 | + <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)"/> | ||
36 | + <span class="input-group-btn"> | ||
37 | + <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> | ||
38 | + </span> | ||
39 | + <span class="msg-box n-right"></span> | ||
40 | + </div> | ||
41 | + <div id="schedulepicker"> | ||
42 | + <pre><code>* * * * * | ||
43 | +- - - - - | ||
44 | +| | | | +--- day of week (0 - 7) (Sunday=0 or 7) | ||
45 | +| | | +-------- month (1 - 12) | ||
46 | +| | +------------- day of month (1 - 31) | ||
47 | +| +------------------ hour (0 - 23) | ||
48 | ++----------------------- min (0 - 59)</code></pre> | ||
49 | + <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> | ||
50 | + <ol id="scheduleresult" class="list-group"> | ||
51 | + </ol> | ||
52 | + </div> | ||
53 | + </div> | ||
54 | + </div> | ||
55 | + <div class="form-group"> | ||
56 | + <label for="maximums" class="control-label col-xs-12 col-sm-2">{:__('Maximums')}:</label> | ||
57 | + <div class="col-xs-12 col-sm-4"> | ||
58 | + <input type="number" class="form-control" id="maximums" name="row[maximums]" value="0" data-rule="required" size="6" data-tip="0表示无限制" /> | ||
59 | + </div> | ||
60 | + </div> | ||
61 | + <div class="form-group"> | ||
62 | + <label for="begintime" class="control-label col-xs-12 col-sm-2">{:__('Begin time')}:</label> | ||
63 | + <div class="col-xs-12 col-sm-4"> | ||
64 | + <input type="text" class="form-control datetimepicker" id="begintime" name="row[begintime]" value="" data-rule="{:__('Begin time')}:required" size="6" /> | ||
65 | + </div> | ||
66 | + </div> | ||
67 | + <div class="form-group"> | ||
68 | + <label for="endtime" class="control-label col-xs-12 col-sm-2">{:__('End time')}:</label> | ||
69 | + <div class="col-xs-12 col-sm-4"> | ||
70 | + <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" /> | ||
71 | + </div> | ||
72 | + </div> | ||
73 | + <div class="form-group"> | ||
74 | + <label for="weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label> | ||
75 | + <div class="col-xs-12 col-sm-4"> | ||
76 | + <input type="text" class="form-control" id="weigh" name="row[weigh]" value="0" data-rule="required" size="6" /> | ||
77 | + </div> | ||
78 | + </div> | ||
79 | + <div class="form-group"> | ||
80 | + <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label> | ||
81 | + <div class="col-xs-12 col-sm-8"> | ||
82 | + {:build_radios('row[status]', ['normal'=>__('Normal'), 'hidden'=>__('Hidden')])} | ||
83 | + </div> | ||
84 | + </div> | ||
85 | + <div class="form-group hide layer-footer"> | ||
86 | + <label class="control-label col-xs-12 col-sm-2"></label> | ||
87 | + <div class="col-xs-12 col-sm-8"> | ||
88 | + <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button> | ||
89 | + <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button> | ||
90 | + </div> | ||
91 | + </div> | ||
92 | + | ||
93 | +</form> |
1 | +<style type="text/css"> | ||
2 | + #schedulepicker { | ||
3 | + padding-top: 7px; | ||
4 | + } | ||
5 | + #schedulepicker h5 { | ||
6 | + line-height: 30px; | ||
7 | + } | ||
8 | + #schedulepicker .list-group { | ||
9 | + margin-bottom: 0; | ||
10 | + } | ||
11 | +</style> | ||
12 | +<form id="edit-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action=""> | ||
13 | + <div class="form-group"> | ||
14 | + <label for="title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label> | ||
15 | + <div class="col-xs-12 col-sm-8"> | ||
16 | + <input type="text" class="form-control" id="title" name="row[title]" value="{$row.title|htmlentities}" data-rule="required"/> | ||
17 | + </div> | ||
18 | + </div> | ||
19 | + <div class="form-group"> | ||
20 | + <label for="type" class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label> | ||
21 | + <div class="col-xs-12 col-sm-8"> | ||
22 | + {:build_select('row[type]', $typeList, $row['type'], ['class'=>'form-control', 'data-rule'=>'required'])} | ||
23 | + </div> | ||
24 | + </div> | ||
25 | + <div class="form-group"> | ||
26 | + <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label> | ||
27 | + <div class="col-xs-12 col-sm-8"> | ||
28 | + <textarea name="row[content]" id="c-content" cols="30" rows="5" class="form-control" data-rule="required">{$row.content|htmlentities}</textarea> | ||
29 | + </div> | ||
30 | + </div> | ||
31 | + <div class="form-group"> | ||
32 | + <label for="schedule" class="control-label col-xs-12 col-sm-2">{:__('Schedule')}:</label> | ||
33 | + <div class="col-xs-12 col-sm-8"> | ||
34 | + <div class="input-group margin-bottom-sm"> | ||
35 | + <input type="text" class="form-control" id="schedule" style="font-size:12px;font-family: Verdana;word-spacing:23px;" name="row[schedule]" value="{$row.schedule|htmlentities}" data-rule="required; remote(general/crontab/check_schedule)"/> | ||
36 | + <span class="input-group-btn"> | ||
37 | + <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> | ||
38 | + </span> | ||
39 | + <span class="msg-box n-right"></span> | ||
40 | + </div> | ||
41 | + | ||
42 | + <div id="schedulepicker"> | ||
43 | + <pre><code>* * * * * | ||
44 | +- - - - - | ||
45 | +| | | | +--- day of week (0 - 7) (Sunday=0 or 7) | ||
46 | +| | | +-------- month (1 - 12) | ||
47 | +| | +------------- day of month (1 - 31) | ||
48 | +| +------------------ hour (0 - 23) | ||
49 | ++----------------------- min (0 - 59)</code></pre> | ||
50 | + <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> | ||
51 | + <ol id="scheduleresult" class="list-group"> | ||
52 | + </ol> | ||
53 | + </div> | ||
54 | + </div> | ||
55 | + </div> | ||
56 | + <div class="form-group"> | ||
57 | + <label for="maximums" class="control-label col-xs-12 col-sm-2">{:__('Maximums')}:</label> | ||
58 | + <div class="col-xs-12 col-sm-4"> | ||
59 | + <input type="number" class="form-control" id="maximums" name="row[maximums]" value="{$row.maximums}" data-rule="required" size="6"/> | ||
60 | + </div> | ||
61 | + </div> | ||
62 | + <div class="form-group"> | ||
63 | + <label for="begintime" class="control-label col-xs-12 col-sm-2">{:__('Begin time')}:</label> | ||
64 | + <div class="col-xs-12 col-sm-4"> | ||
65 | + <input type="text" class="form-control datetimepicker" id="begintime" name="row[begintime]" value="{$row.begintime|datetime}" data-rule="{:__('Begin time')}:required" size="6"/> | ||
66 | + </div> | ||
67 | + </div> | ||
68 | + <div class="form-group"> | ||
69 | + <label for="endtime" class="control-label col-xs-12 col-sm-2">{:__('End time')}:</label> | ||
70 | + <div class="col-xs-12 col-sm-4"> | ||
71 | + <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"/> | ||
72 | + </div> | ||
73 | + </div> | ||
74 | + <div class="form-group"> | ||
75 | + <label for="weigh" class="control-label col-xs-12 col-sm-2">{:__('Weigh')}:</label> | ||
76 | + <div class="col-xs-12 col-sm-4"> | ||
77 | + <input type="text" class="form-control" id="weigh" name="row[weigh]" value="{$row.weigh}" data-rule="required" size="6"/> | ||
78 | + </div> | ||
79 | + </div> | ||
80 | + <div class="form-group"> | ||
81 | + <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label> | ||
82 | + <div class="col-xs-12 col-sm-8"> | ||
83 | + {:build_radios('row[status]', ['normal'=>__('Normal'), 'completed'=>__('Completed'), 'expired'=>__('Expired'), 'hidden'=>__('Hidden')], $row['status'])} | ||
84 | + </div> | ||
85 | + </div> | ||
86 | + <div class="form-group hide layer-footer"> | ||
87 | + <label class="control-label col-xs-12 col-sm-2"></label> | ||
88 | + <div class="col-xs-12 col-sm-8"> | ||
89 | + <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button> | ||
90 | + <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button> | ||
91 | + </div> | ||
92 | + </div> | ||
93 | + | ||
94 | +</form> |
1 | +<div class="panel panel-default panel-intro"> | ||
2 | + | ||
3 | + <div class="panel-heading"> | ||
4 | + {:build_heading(null,FALSE)} | ||
5 | + <ul class="nav nav-tabs" data-field="type"> | ||
6 | + <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li> | ||
7 | + {foreach name="typeList" item="vo"} | ||
8 | + <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li> | ||
9 | + {/foreach} | ||
10 | + </ul> | ||
11 | + </div> | ||
12 | + | ||
13 | + <div class="panel-body"> | ||
14 | + <div id="myTabContent" class="tab-content"> | ||
15 | + <div class="tab-pane fade active in" id="one"> | ||
16 | + <div class="widget-body no-padding"> | ||
17 | + <div id="toolbar" class="toolbar"> | ||
18 | + {:build_toolbar('refresh,add,edit,del')} | ||
19 | + </div> | ||
20 | + <table id="table" class="table table-striped table-bordered table-hover table-nowrap" | ||
21 | + data-operate-edit="{:$auth->check('general/crontab/edit')}" | ||
22 | + data-operate-del="{:$auth->check('general/crontab/del')}" | ||
23 | + width="100%"> | ||
24 | + </table> | ||
25 | + </div> | ||
26 | + </div> | ||
27 | + | ||
28 | + </div> | ||
29 | + </div> | ||
30 | +</div> |
1 | +<style type="text/css"> | ||
2 | + #schedulepicker { | ||
3 | + padding-top:7px; | ||
4 | + } | ||
5 | +</style> | ||
6 | +<form id="edit-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action=""> | ||
7 | + <div class="form-group"> | ||
8 | + <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label> | ||
9 | + <div class="col-xs-12 col-sm-10"> | ||
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> | ||
11 | + </div> | ||
12 | + </div> | ||
13 | + <div class="form-group"> | ||
14 | + <label for="executetime" class="control-label col-xs-12 col-sm-2">{:__('End time')}:</label> | ||
15 | + <div class="col-xs-12 col-sm-4"> | ||
16 | + <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 /> | ||
17 | + </div> | ||
18 | + </div> | ||
19 | + <div class="form-group"> | ||
20 | + <label for="completetime" class="control-label col-xs-12 col-sm-2">{:__('End time')}:</label> | ||
21 | + <div class="col-xs-12 col-sm-4"> | ||
22 | + <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 /> | ||
23 | + </div> | ||
24 | + </div> | ||
25 | + <div class="form-group"> | ||
26 | + <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label> | ||
27 | + <div class="col-xs-12 col-sm-8"> | ||
28 | + <div style="padding-top:8px;"> | ||
29 | + {if $row['status']=='success'}<span class="label label-success">{:__('Success')}</span>{else/}<span class="label label-danger">{:__('Failure')}</span>{/if} | ||
30 | + </div> | ||
31 | + </div> | ||
32 | + </div> | ||
33 | + <div class="form-group hide layer-footer"> | ||
34 | + <label class="control-label col-xs-12 col-sm-2"></label> | ||
35 | + <div class="col-xs-12 col-sm-8"> | ||
36 | + <button type="button" class="btn btn-success btn-embossed" onclick="parent.Layer.close(parent.Layer.getFrameIndex(window.name))">{:__('Close')}</button> | ||
37 | + </div> | ||
38 | + </div> | ||
39 | + | ||
40 | +</form> |
1 | +<div class="panel panel-default panel-intro"> | ||
2 | + | ||
3 | + <div class="panel-heading"> | ||
4 | + {:build_heading(null,FALSE)} | ||
5 | + <ul class="nav nav-tabs" data-field="status"> | ||
6 | + <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li> | ||
7 | + {foreach name="statusList" item="vo"} | ||
8 | + <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li> | ||
9 | + {/foreach} | ||
10 | + </ul> | ||
11 | + </div> | ||
12 | + | ||
13 | + <div class="panel-body"> | ||
14 | + <div id="myTabContent" class="tab-content"> | ||
15 | + <div class="tab-pane fade active in" id="one"> | ||
16 | + <div class="widget-body no-padding"> | ||
17 | + <div id="toolbar" class="toolbar"> | ||
18 | + {:build_toolbar('refresh,del')} | ||
19 | + </div> | ||
20 | + <table id="table" class="table table-striped table-bordered table-hover" | ||
21 | + data-operate-detail="{:$auth->check('general/crontab/detail')}" | ||
22 | + data-operate-del="{:$auth->check('general/crontab/del')}" | ||
23 | + width="100%"> | ||
24 | + </table> | ||
25 | + </div> | ||
26 | + </div> | ||
27 | + | ||
28 | + </div> | ||
29 | + </div> | ||
30 | +</div> |
public/assets/js/backend/general/crontab.js
0 → 100644
1 | +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) { | ||
2 | + | ||
3 | + var Controller = { | ||
4 | + index: function () { | ||
5 | + // 初始化表格参数配置 | ||
6 | + Table.api.init({ | ||
7 | + extend: { | ||
8 | + index_url: 'general/crontab/index', | ||
9 | + add_url: 'general/crontab/add', | ||
10 | + edit_url: 'general/crontab/edit', | ||
11 | + del_url: 'general/crontab/del', | ||
12 | + multi_url: 'general/crontab/multi', | ||
13 | + table: 'crontab' | ||
14 | + } | ||
15 | + }); | ||
16 | + | ||
17 | + var table = $("#table"); | ||
18 | + | ||
19 | + // 初始化表格 | ||
20 | + table.bootstrapTable({ | ||
21 | + url: $.fn.bootstrapTable.defaults.extend.index_url, | ||
22 | + sortName: 'weigh', | ||
23 | + fixedColumns: true, | ||
24 | + fixedRightNumber: 1, | ||
25 | + columns: [ | ||
26 | + [ | ||
27 | + {field: 'state', checkbox: true,}, | ||
28 | + {field: 'id', title: 'ID'}, | ||
29 | + {field: 'type', title: __('Type'), searchList: Config.typeList, formatter: Table.api.formatter.label, custom: {sql: 'warning', url: 'info', shell: 'success'}}, | ||
30 | + {field: 'title', title: __('Title')}, | ||
31 | + {field: 'maximums', title: __('Maximums'), formatter: Controller.api.formatter.maximums}, | ||
32 | + {field: 'executes', title: __('Executes')}, | ||
33 | + {field: 'begintime', title: __('Begin time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange'}, | ||
34 | + {field: 'endtime', title: __('End time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange'}, | ||
35 | + {field: 'nexttime', title: __('Next execute time'), formatter: Controller.api.formatter.nexttime, operate: false, addclass: 'datetimerange', sortable: true}, | ||
36 | + {field: 'executetime', title: __('Execute time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true}, | ||
37 | + {field: 'weigh', title: __('Weigh')}, | ||
38 | + {field: 'status', title: __('Status'), searchList: {"normal": __('Normal'), "hidden": __('Hidden'), "expired": __('Expired'), "completed": __('Completed')}, formatter: Table.api.formatter.status}, | ||
39 | + { | ||
40 | + field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate, | ||
41 | + buttons: [ | ||
42 | + { | ||
43 | + name: "detail", | ||
44 | + icon: "fa fa-list", | ||
45 | + title: function (row, index) { | ||
46 | + return __('Logs') + "[" + row['title'] + "]"; | ||
47 | + }, | ||
48 | + text: __('Logs'), | ||
49 | + classname: "btn btn-xs btn-info btn-dialog", | ||
50 | + url: "general/crontab_log/index?crontab_id={ids}", | ||
51 | + } | ||
52 | + ] | ||
53 | + } | ||
54 | + ] | ||
55 | + ] | ||
56 | + }); | ||
57 | + | ||
58 | + // 为表格绑定事件 | ||
59 | + Table.api.bindevent(table); | ||
60 | + }, | ||
61 | + add: function () { | ||
62 | + Controller.api.bindevent(); | ||
63 | + }, | ||
64 | + edit: function () { | ||
65 | + Controller.api.bindevent(); | ||
66 | + }, | ||
67 | + api: { | ||
68 | + bindevent: function () { | ||
69 | + $('#schedule').on('valid.field', function (e, result) { | ||
70 | + $("#pickdays").trigger("change"); | ||
71 | + }); | ||
72 | + Form.api.bindevent($("form[role=form]")); | ||
73 | + $(document).on("change", "#pickdays", function () { | ||
74 | + Fast.api.ajax({url: "general/crontab/get_schedule_future", data: {schedule: $("#schedule").val(), days: $(this).val()}}, function (data, ret) { | ||
75 | + if (typeof data.futuretime !== 'undefined' && $.isArray(data.futuretime)) { | ||
76 | + var result = []; | ||
77 | + $.each(data.futuretime, function (i, j) { | ||
78 | + result.push("<li class='list-group-item'>" + j + "<span class='badge'>" + (i + 1) + "</span></li>"); | ||
79 | + }); | ||
80 | + $("#scheduleresult").html(result.join("")); | ||
81 | + } else { | ||
82 | + $("#scheduleresult").html(""); | ||
83 | + } | ||
84 | + return false; | ||
85 | + }); | ||
86 | + }); | ||
87 | + $("#pickdays").trigger("change"); | ||
88 | + }, | ||
89 | + formatter: { | ||
90 | + nexttime: function (value, row, index) { | ||
91 | + if (isNaN(value)) { | ||
92 | + return value; | ||
93 | + } else { | ||
94 | + return Table.api.formatter.datetime.call(this, value, row, index); | ||
95 | + } | ||
96 | + }, | ||
97 | + maximums: function (value, row, index) { | ||
98 | + return value === 0 ? __('No limit') : value; | ||
99 | + } | ||
100 | + } | ||
101 | + } | ||
102 | + }; | ||
103 | + return Controller; | ||
104 | +}); |
1 | +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) { | ||
2 | + | ||
3 | + var Controller = { | ||
4 | + index: function () { | ||
5 | + // 初始化表格参数配置 | ||
6 | + Table.api.init({ | ||
7 | + extend: { | ||
8 | + index_url: 'general/crontab_log/index', | ||
9 | + add_url: 'general/crontab_log/add', | ||
10 | + edit_url: '', | ||
11 | + del_url: 'general/crontab_log/del', | ||
12 | + multi_url: 'general/crontab_log/multi', | ||
13 | + table: 'crontab' | ||
14 | + } | ||
15 | + }); | ||
16 | + | ||
17 | + var table = $("#table"); | ||
18 | + | ||
19 | + // 初始化表格 | ||
20 | + table.bootstrapTable({ | ||
21 | + url: $.fn.bootstrapTable.defaults.extend.index_url, | ||
22 | + sortName: 'id', | ||
23 | + columns: [ | ||
24 | + [ | ||
25 | + {field: 'state', checkbox: true,}, | ||
26 | + {field: 'id', title: 'ID'}, | ||
27 | + {field: 'crontab_id', title: __('Crontab_id'), formatter: Table.api.formatter.search}, | ||
28 | + {field: 'executetime', title: __('Execute time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true}, | ||
29 | + {field: 'completetime', title: __('Complete time'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true}, | ||
30 | + {field: 'status', title: __('Status'), searchList: Config.statusList, custom: {success: 'success', failure: 'danger'}, formatter: Table.api.formatter.status}, | ||
31 | + { | ||
32 | + field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate, | ||
33 | + buttons: [ | ||
34 | + { | ||
35 | + name: "detail", | ||
36 | + text: __("Result"), | ||
37 | + classname: "btn btn-xs btn-info btn-dialog", | ||
38 | + icon: "fa fa-file", | ||
39 | + url: "general/crontab_log/detail", | ||
40 | + extend: "data-window='parent'" | ||
41 | + } | ||
42 | + ] | ||
43 | + } | ||
44 | + ] | ||
45 | + ] | ||
46 | + }); | ||
47 | + | ||
48 | + // 为表格绑定事件 | ||
49 | + Table.api.bindevent(table); | ||
50 | + }, | ||
51 | + add: function () { | ||
52 | + Controller.api.bindevent(); | ||
53 | + }, | ||
54 | + edit: function () { | ||
55 | + Controller.api.bindevent(); | ||
56 | + }, | ||
57 | + api: { | ||
58 | + bindevent: function () { | ||
59 | + Form.api.bindevent($("form[role=form]")); | ||
60 | + | ||
61 | + }, | ||
62 | + } | ||
63 | + }; | ||
64 | + return Controller; | ||
65 | +}); |
-
请 注册 或 登录 后发表评论