diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index adb9810a..a7ff71f0 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -4,6 +4,8 @@ namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
+use App\Models\Cron;
+use Illuminate\Support\Carbon;
class Kernel extends ConsoleKernel
{
@@ -16,6 +18,21 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
$schedule->command('monitor')->everyMinute();
+ // 查询所有计划任务
+ $crons = Cron::all();
+ foreach ($crons as $cron) {
+ $schedule->exec('bash /www/server/cron/'.$cron->shell)->withoutOverlapping()->cron($cron->time)->appendOutputTo('/www/server/cron/logs/'.$cron->id.'.log')->when(function (
+ ) use ($cron) {
+ return (boolean) $cron->status;
+ })->after(function () use ($cron) {
+ $cron->updated_at = now();
+ $cron->save();
+ })->onSuccess(function () use ($cron) {
+ shell_exec('echo "'.Carbon::now()->toDateTimeString().' 任务执行成功" >> /www/server/cron/logs/'.$cron->id.'.log');
+ })->onFailure(function () use ($cron) {
+ shell_exec('echo "'.Carbon::now()->toDateTimeString().' 任务执行失败" >> /www/server/cron/logs/'.$cron->id.'.log');
+ });
+ }
}
/**
diff --git a/app/Http/Controllers/Api/CronsController.php b/app/Http/Controllers/Api/CronsController.php
new file mode 100644
index 00000000..7488c867
--- /dev/null
+++ b/app/Http/Controllers/Api/CronsController.php
@@ -0,0 +1,205 @@
+input('limit', 10);
+
+ $crons = Cron::query()->orderBy('id', 'desc')->paginate($limit);
+ $cronData = [];
+
+ foreach ($crons as $k => $v) {
+ // 格式化时间
+ $cronData[$k]['id'] = $v['id'];
+ $cronData[$k]['name'] = $v['name'];
+ $cronData[$k]['status'] = $v['status'];
+ $cronData[$k]['type'] = $v['type'];
+ $cronData[$k]['time'] = $v['time'];
+ $cronData[$k]['shell'] = $v['shell'];
+ $cronData[$k]['script'] = @file_get_contents('/www/server/cron/'.$v['shell']);
+ $cronData[$k]['created_at'] = Carbon::create($v['created_at'])->toDateTimeString();
+ $cronData[$k]['updated_at'] = Carbon::create($v['updated_at'])->toDateTimeString();
+ }
+
+ $data['code'] = 0;
+ $data['msg'] = 'success';
+ $data['count'] = $crons->total();
+ $data['data'] = $cronData;
+ return response()->json($data);
+ }
+
+ /**
+ * 添加计划任务
+ */
+ public function add(Request $request): JsonResponse
+ {
+ // 消毒
+ try {
+ $credentials = $this->validate($request, [
+ 'name' => 'required|max:255',
+ 'time' => ['required', 'regex:/^((\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+)(\,(\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+))*\s?){5}$/'],
+ 'script' => 'required',
+ ]);
+ } catch (ValidationException $e) {
+ return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
+ }
+
+ // 将script写入shell文件
+ $shellDir = '/www/server/cron/';
+ $shellLogDir = '/www/server/cron/logs/';
+ if (!is_dir($shellDir)) {
+ mkdir($shellDir, 0755, true);
+ }
+ if (!is_dir($shellLogDir)) {
+ mkdir($shellLogDir, 0755, true);
+ }
+ $shellFile = uniqid().'.sh';
+ file_put_contents($shellDir.$shellFile, $credentials['script']);
+
+ $cron = new Cron();
+ $cron->name = $credentials['name'];
+ $cron->status = 1;
+ $cron->type = '脚本';
+ $cron->time = $credentials['time'];
+ $cron->shell = $shellFile;
+ $cron->save();
+
+ $data['code'] = 0;
+ $data['msg'] = 'success';
+ return response()->json($data);
+ }
+
+ /**
+ * 修改计划任务
+ */
+ public function edit(Request $request): JsonResponse
+ {
+ // 消毒
+ try {
+ $credentials = $this->validate($request, [
+ 'id' => 'required|integer',
+ 'name' => 'required|max:255',
+ 'time' => ['required', 'regex:/^((\*|\d+|\d+-\d+|\d+\/\d+)(\,(\*|\d+|\d+-\d+|\d+\/\d+))*\s?){5}$/'],
+ 'script' => 'required',
+ ]);
+ } catch (ValidationException $e) {
+ return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
+ }
+
+ $cron = Cron::query()->find($credentials['id']);
+ $cron->name = $credentials['name'];
+ $cron->time = $credentials['time'];
+ // 将script写入shell文件
+ $shellDir = '/www/server/cron/';
+ $shellLogDir = '/www/server/cron/logs/';
+ if (!is_dir($shellDir)) {
+ mkdir($shellDir, 0755, true);
+ }
+ if (!is_dir($shellLogDir)) {
+ mkdir($shellLogDir, 0755, true);
+ }
+ $shellFile = $cron->shell;
+ file_put_contents($shellDir.$shellFile, $credentials['script']);
+ $cron->save();
+
+ $data['code'] = 0;
+ $data['msg'] = 'success';
+ return response()->json($data);
+ }
+
+ /**
+ * 删除计划任务
+ */
+ public function delete(Request $request): JsonResponse
+ {
+ // 消毒
+ try {
+ $credentials = $this->validate($request, [
+ 'id' => 'required|integer',
+ ]);
+ } catch (ValidationException $e) {
+ return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
+ }
+
+ $cron = Cron::query()->find($credentials['id']);
+ // 删除shell文件
+ $shellDir = '/www/server/cron/';
+ $shellFile = $cron->shell;
+ @unlink($shellDir.$shellFile);
+ // 删除日志文件
+ $shellLogDir = '/www/server/cron/logs/';
+ $shellLogFile = $shellFile.'.log';
+ @unlink($shellLogDir.$shellLogFile);
+ $cron->delete();
+
+ $data['code'] = 0;
+ $data['msg'] = 'success';
+ return response()->json($data);
+ }
+
+ /**
+ * 修改计划任务状态
+ */
+ public function setStatus(Request $request): JsonResponse
+ {
+ // 消毒
+ try {
+ $credentials = $this->validate($request, [
+ 'id' => 'required|integer',
+ 'status' => 'required|integer',
+ ]);
+ } catch (ValidationException $e) {
+ return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
+ }
+
+ $cron = Cron::query()->find($credentials['id']);
+ $cron->status = $credentials['status'];
+ $cron->save();
+
+ $data['code'] = 0;
+ $data['msg'] = 'success';
+ return response()->json($data);
+ }
+
+ /**
+ * 获取计划任务日志
+ */
+ public function getLog(Request $request): JsonResponse
+ {
+ // 消毒
+ try {
+ $credentials = $this->validate($request, [
+ 'id' => 'required|integer',
+ ]);
+ } catch (ValidationException $e) {
+ return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
+ }
+
+ $log = @file_get_contents('/www/server/cron/logs/'.$credentials['id'].'.log');
+ if ($log === false) {
+ $log = '暂无日志';
+ }
+
+ $data['code'] = 0;
+ $data['msg'] = 'success';
+ $data['data'] = $log;
+ return response()->json($data);
+ }
+}
diff --git a/app/Http/Controllers/Api/InfosController.php b/app/Http/Controllers/Api/InfosController.php
index 0612a1c5..5f4a0ddb 100644
--- a/app/Http/Controllers/Api/InfosController.php
+++ b/app/Http/Controllers/Api/InfosController.php
@@ -27,45 +27,45 @@ class InfosController extends Controller
),
array(
"name" => "website",
- "title" => "网站",
+ "title" => "网站管理",
"icon" => "layui-icon-website",
"jump" => "website/list"
),
array(
"name" => "monitor",
- "title" => "监控",
+ "title" => "资源监控",
"icon" => "layui-icon-chart-screen",
"jump" => "monitor"
),
array(
"name" => "safe",
- "title" => "安全",
+ "title" => "系统安全",
"icon" => "layui-icon-auz",
"jump" => "safe"
),
array(
"name" => "file",
- "title" => "文件",
+ "title" => "文件管理",
"icon" => "layui-icon-file",
"jump" => "file"
),
+ array(
+ "name" => "cron",
+ "title" => "计划任务",
+ "icon" => "layui-icon-date",
+ "jump" => "cron"
+ ),
array(
"name" => "plugin",
- "title" => "插件",
+ "title" => "插件中心",
"icon" => "layui-icon-app",
"jump" => "plugin"
),
array(
"name" => "setting",
- "title" => "设置",
+ "title" => "面板设置",
"icon" => "layui-icon-set",
"jump" => "setting"
- ),
- array(
- "name" => "logout",
- "title" => "退出",
- "icon" => "layui-icon-logout",
- "jump" => "logout"
)
)
);
diff --git a/app/Models/Cron.php b/app/Models/Cron.php
new file mode 100644
index 00000000..74cde163
--- /dev/null
+++ b/app/Models/Cron.php
@@ -0,0 +1,22 @@
+id();
+ $table->string('name')->nullable()->comment('任务名称');
+ $table->boolean('status')->comment('任务状态');
+ $table->string('type')->comment('任务类型');
+ $table->string('time')->comment('任务周期');
+ $table->text('shell')->comment('任务脚本文件');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('crons');
+ }
+};
diff --git a/public/panel/modules/cron.css b/public/panel/modules/cron.css
new file mode 100644
index 00000000..241bad3b
--- /dev/null
+++ b/public/panel/modules/cron.css
@@ -0,0 +1,194 @@
+
+/* 样式加载完毕的标识 */
+html #layuicss-cron {
+ display: none;
+ position: absolute;
+ width: 1989px;
+}
+
+
+/* 主体结构 */
+.layui-cron {
+ width: 700px;
+ position: absolute;
+ z-index: 99999999;
+ margin: 5px 0;
+ border-radius: 2px;
+ font-size: 14px;
+ -webkit-animation-duration: 0.3s;
+ animation-duration: 0.3s;
+ -webkit-animation-fill-mode: both;
+ animation-fill-mode: both;
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 5px 0px;
+ -webkit-animation-name: cron-upbit;
+ animation-name: cron-upbit;
+ border: 1px solid #e6e6e6;
+}
+
+.layui-cron-main ul {
+ padding-left: 10px;
+}
+
+@-webkit-keyframes cron-upbit {
+
+ /* 微微往上滑入 */
+ from {
+ -webkit-transform: translate3d(0, 20px, 0);
+ opacity: 0.3;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+@keyframes cron-upbit {
+ from {
+ transform: translate3d(0, 20px, 0);
+ opacity: 0.3;
+ }
+
+ to {
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+/* tabs */
+.layui-cron>.layui-tab {
+ margin: 0;
+ box-shadow: none;
+ border: none;
+}
+
+/* 行 */
+.cron-row {
+ padding-left: 13px;
+}
+/* 格 */
+.cron-grid {
+ padding-left: 15px;
+}
+
+/* 表达式 */
+.cron-title {
+ font-weight: 700;
+ font-size: 14px;
+ margin: 10px;
+ margin-bottom: 0;
+}
+
+.cron-box {
+ margin: 10px;
+}
+
+.cron-box+.cron-box {
+ margin-top: 0;
+}
+
+/* 按钮 */
+.cron-footer-btns {
+ text-align: right;
+ margin-right: 10px;
+ margin-bottom: 10px;
+}
+
+.cron-footer-btns span {
+ height: 26px;
+ line-height: 26px;
+ margin: 0 0 0 -1px;
+ padding: 0 10px;
+ border: 1px solid #C9C9C9;
+ background-color: #fff;
+ white-space: nowrap;
+ vertical-align: top;
+ border-radius: 2px;
+ display: inline-block;
+ cursor: pointer;
+ font-size: 12px;
+ box-sizing: border-box;
+ color: #666;
+}
+
+.cron-footer-btns span:hover {
+ color: #5FB878;
+}
+
+
+/* 表单 */
+.layui-cron .layui-form-radio {
+ margin-right: 0;
+}
+
+.cron-form {
+ line-height: 28px;
+ font-size: 14px;
+}
+
+.cron-input-mid {
+ display: inline-block;
+ vertical-align: middle;
+ margin-top: 6px;
+ background-color: #e5e5e5;
+ padding: 0 12px;
+ height: 28px;
+ line-height: 28px;
+ border: 1px solid #ccc;
+ box-sizing: border-box;
+}
+
+.cron-input {
+ display: inline-block;
+ vertical-align: middle;
+ margin-top: 6px;
+ padding: 0 8px;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ height: 28px;
+ line-height: 28px;
+ box-sizing: border-box;
+ width: 80px;
+ -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
+ -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+ transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
+ transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+ transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
+}
+
+.cron-input:focus {
+ outline: 0;
+ border: 1px solid #01AAED;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 4px 0px #01AAED;
+ translate: 1s;
+}
+
+.layui-cron .layui-form-checkbox[lay-skin="primary"] span {
+ padding-right: 10px;
+ min-width: 16px;
+}
+
+.layui-cron .layui-form-checkbox[lay-skin="primary"] {
+ padding-left: 22px;
+ margin-top: 5px;
+}
+.layui-cron input[type=number] {
+ -moz-appearance:textfield;
+}
+.layui-cron input[type=number]::-webkit-inner-spin-button,
+.layui-cron input[type=number]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+.cron-tips{
+ color: grey;
+ line-height: 28px;
+ height: 28px;
+ display: inline-block;
+ vertical-align: middle;
+ margin-top: 8px;
+ margin-left: 5px;
+}
diff --git a/public/panel/modules/cron.js b/public/panel/modules/cron.js
new file mode 100644
index 00000000..4bc9931a
--- /dev/null
+++ b/public/panel/modules/cron.js
@@ -0,0 +1,961 @@
+/**
+ @ Name:layui.cron Cron表达式解析器
+ @ Author:贝哥哥
+ @ License:MIT
+ */
+
+layui.define(['lay', 'element', 'form'], function (exports) { //假如该组件依赖 layui.form
+ var $ = layui.$, layer = layui.layer, lay = layui.lay, element = layui.element, form = layui.form
+
+
+ //字符常量
+ , MOD_NAME = 'cron', ELEM = '.layui-cron', THIS = 'layui-this', SHOW = 'layui-show', HIDE = 'layui-hide'
+
+ , ELEM_STATIC = 'layui-cron-static', ELEM_FOOTER = 'layui-cron-footer', ELEM_CONFIRM = '.cron-btns-confirm',
+ ELEM_HINT = 'layui-cron-hint'
+
+ , ELEM_RUN_HINT = 'layui-cron-run-hint'
+
+ //外部接口
+ , cron = {
+ v: '2.0.0' // cron 组件当前版本
+ , index: layui.cron ? (layui.cron.index + 10000) : 0 // corn 实例标识
+
+ //设置全局项
+ , set: function (options) {
+ var that = this;
+ that.config = $.extend({}, that.config, options);
+ return that;
+ }
+
+ //事件监听
+ , on: function (events, callback) {
+ return layui.onevent.call(this, MOD_NAME, events, callback);
+ }
+
+ //主体CSS等待事件
+ , ready: function (fn) {
+ var cssPath = layui.cache.base + "cron.css?v=" + cron.v;
+ layui.link(cssPath, fn, "cron"); //此处的“cron”要对应 cron.css 中的样式: html #layuicss-cron{}
+ return this;
+ }
+ }
+
+ //操作当前实例
+ , thisIns = function () {
+ var that = this, options = that.config, id = options.id || options.index;
+
+ return {
+ //提示框
+ hint: function (content) {
+ that.hint.call(that, content);
+ }, config: options
+ }
+ }
+
+ //构造器,创建实例
+ , Class = function (options) {
+ var that = this;
+ that.index = ++cron.index;
+ that.config = $.extend({}, that.config, cron.config, options);
+ cron.ready(function () {
+ that.init();
+ });
+ };
+
+ //默认配置
+ Class.prototype.config = {
+ value: null // 当前表达式值,每秒执行一次
+ ,
+ isInitValue: true //用于控制是否自动向元素填充初始值(需配合 value 参数使用)
+ ,
+ lang: "cn" //语言,只支持cn/en,即中文和英文
+ ,
+ tabs: [{key: 'minutes', range: '0-59'}, {
+ key: 'hours', range: '0-23'
+ }, {key: 'days', range: '1-31'}, {key: 'months', range: '1-12'}, {key: 'weeks', range: '1-7'}],
+ defaultCron: {minutes: "*", hours: "*", days: "*", months: "*", weeks: "*"},
+ trigger: "click" //呼出控件的事件
+ ,
+ btns: ['run', 'confirm'] //右下角显示的按钮,会按照数组顺序排列
+ ,
+ position: null //控件定位方式定位, 默认absolute,支持:fixed/absolute/static
+ ,
+ zIndex: null //控件层叠顺序
+ ,
+ show: false //是否直接显示,如果设置 true,则默认直接显示控件
+ ,
+ showBottom: true //是否显示底部栏
+ ,
+ done: null //控件选择完毕后的回调,点击运行/确定也均会触发
+ ,
+ run: null // 最近运行时间接口
+ };
+
+ //多语言
+ Class.prototype.lang = function () {
+ var that = this, options = that.config, text = {
+ cn: {
+ tabs: [{title: "分"}, {title: "时"}, {title: "日"}, {title: "月"}, {
+ title: "周",
+ rateBegin: "第",
+ rateMid: "周的星期",
+ rateEnd: ""
+ }],
+ every: "每",
+ unspecified: "不指定",
+ period: "周期",
+ periodFrom: "从",
+ rate: "按照",
+ rateBegin: "从",
+ rateMid: "开始,每",
+ rateEnd: "执行一次",
+ weekday: "工作日",
+ weekdayPrefix: "每月",
+ weekdaySuffix: "号最近的那个工作日",
+ lastday: "本月最后一日",
+ lastweek: "本月最后一个星期",
+ custom: "指定",
+ tools: {
+ confirm: '确定', run: '运行'
+ },
+ formatError: ['Cron格式不合法', '
已为你重置']
+ }, en: {
+ tabs: [{title: "Minutes"}, {title: "Hours"}, {title: "Days"}, {title: "Months"}, {title: "Weeks"}],
+ every: "Every ",
+ unspecified: "Unspecified",
+ period: "Period",
+ periodFrom: "From",
+ rate: "According to",
+ rateBegin: "begin at",
+ rateMid: ", every",
+ rateEnd: " execute once",
+ weekday: "Weekday",
+ weekdayPrefix: "Every month at ",
+ weekdaySuffix: "号最近的那个工作日",
+ lastday: "Last day of the month",
+ lastweek: "本月最后一个星期",
+ custom: "Custom",
+ tools: {
+ confirm: 'Confirm', run: 'Run'
+ },
+ formatError: ['The cron format error', '
It has been reset']
+ }
+ };
+ return text[options.lang] || text['cn'];
+ };
+
+ //初始准备
+ Class.prototype.init = function () {
+ var that = this, options = that.config, isStatic = options.position === 'static';
+
+ options.elem = lay(options.elem);
+
+ options.eventElem = lay(options.eventElem);
+
+ if (!options.elem[0]) return;
+
+ //如果不是input|textarea元素,则默认采用click事件
+ if (!that.isInput(options.elem[0])) {
+ if (options.trigger === 'focus') {
+ options.trigger = 'click';
+ }
+ }
+
+ // 设置渲染所绑定元素的唯一KEY
+ if (!options.elem.attr('lay-key')) {
+ options.elem.attr('lay-key', that.index);
+ options.eventElem.attr('lay-key', that.index);
+ }
+
+ // 当前实例主面板ID
+ that.elemID = 'layui-icon' + options.elem.attr('lay-key');
+
+ //默认赋值
+ if (options.value && options.isInitValue) {
+ that.setValue(options.value);
+ }
+ if (!options.value) {
+ options.value = options.elem[0].value || '';
+ }
+ var cronArr = options.value.split(' ');
+ if (cronArr.length >= 6) {
+ options.cron = {
+ minutes: cronArr[0],
+ hours: cronArr[1],
+ days: cronArr[2],
+ months: cronArr[3],
+ weeks: cronArr[4],
+ };
+ } else {
+ options.cron = lay.extend({}, options.defaultCron);
+ }
+
+
+ if (options.show || isStatic) that.render();
+ isStatic || that.events();
+
+
+ };
+
+
+ // 控件主体渲染
+ Class.prototype.render = function () {
+ var that = this, options = that.config, lang = that.lang(), isStatic = options.position === 'static',
+ tabFilter = 'cron-tab' + options.elem.attr('lay-key')
+ //主面板
+ , elem = that.elem = lay.elem('div', {
+ id: that.elemID, 'class': ['layui-cron', isStatic ? (' ' + ELEM_STATIC) : ''].join('')
+ })
+
+ // tab 内容区域
+ , elemTab = that.elemTab = lay.elem('div', {
+ 'class': 'layui-tab layui-tab-card', 'lay-filter': tabFilter
+ }), tabHead = lay.elem('ul', {
+ 'class': 'layui-tab-title'
+ }), tabContent = lay.elem('div', {
+ 'class': 'layui-tab-content'
+ })
+
+ //底部区域
+ , divFooter = that.footer = lay.elem('div', {
+ 'class': ELEM_FOOTER
+ });
+
+ if (options.zIndex) elem.style.zIndex = options.zIndex;
+
+ // 生成tab 内容区域
+ elemTab.appendChild(tabHead);
+ elemTab.appendChild(tabContent);
+ lay.each(lang.tabs, function (i, item) {
+ // 表头
+ var li = lay.elem('li', {
+ 'class': i === 0 ? THIS : "", 'lay-id': i
+ });
+ li.innerHTML = item.title;
+ tabHead.appendChild(li);
+
+ // 表体
+ tabContent.appendChild(that.getTabContentChildElem(i));
+ });
+
+ // 主区域
+ elemMain = that.elemMain = lay.elem('div', {
+ 'class': 'layui-cron-main'
+ });
+ elemMain.appendChild(elemTab);
+
+ //生成底部栏
+ lay(divFooter).html(function () {
+ var html = [], btns = [];
+ lay.each(options.btns, function (i, item) {
+ var title = lang.tools[item] || 'btn';
+ btns.push('' + title + '');
+ });
+ html.push('