diff --git a/assets/templates/admin/admin_list.html b/assets/templates/admin/admin_list.html index 3cb71a1..49c380b 100644 --- a/assets/templates/admin/admin_list.html +++ b/assets/templates/admin/admin_list.html @@ -81,7 +81,7 @@ function (data) { if (data.list.length > 0) { var totalNum = data.pagination.total; //总条数 - var pageNum = Math.ceil(totalNum / data.pagination.pre_page_count); //分页的总页数 + var pageNum = Math.ceil(totalNum / data.pagination.per_page_count); //分页的总页数 $("#paginationDiv").pagination({ current: data.pagination.current_page, diff --git a/assets/templates/authorized/authorized_list.html b/assets/templates/authorized/authorized_list.html index 310fd6d..6e82914 100644 --- a/assets/templates/authorized/authorized_list.html +++ b/assets/templates/authorized/authorized_list.html @@ -79,7 +79,7 @@ function (data) { if (data.list.length > 0) { var totalNum = data.pagination.total; //总条数 - var pageNum = Math.ceil(totalNum / data.pagination.pre_page_count); //分页的总页数 + var pageNum = Math.ceil(totalNum / data.pagination.per_page_count); //分页的总页数 $("#paginationDiv").pagination({ current: data.pagination.current_page, diff --git a/assets/templates/cron_task/cron_task_list.html b/assets/templates/cron_task/cron_task_list.html index 2122d45..18abb73 100644 --- a/assets/templates/cron_task/cron_task_list.html +++ b/assets/templates/cron_task/cron_task_list.html @@ -93,7 +93,7 @@ function (data) { if (data.list.length > 0) { var totalNum = data.pagination.total; //总条数 - var pageNum = Math.ceil(totalNum / data.pagination.pre_page_count); //分页的总页数 + var pageNum = Math.ceil(totalNum / data.pagination.per_page_count); //分页的总页数 $("#paginationDiv").pagination({ current: data.pagination.current_page, @@ -271,11 +271,52 @@ // 手动执行 $(document).on('click', '.btn-exec', function () { - $.alert({ - title: '温馨提示', + const id = $(this).attr('data-id'); + $.confirm({ + title: '谨慎操作', + content: '确认要 手动执行 吗?', icon: 'mdi mdi-alert', - type: 'orange', - content: '功能开发中...', + animation: 'scale', + closeAnimation: 'zoom', + buttons: { + okay: { + text: '确认', + keys: ['enter'], + btnClass: 'btn-orange', + action: function () { + AjaxForm( + "PATCH", + "/api/cron/exec/" + id, + {}, + function () { + }, + function (data) { + $.alert({ + title: '操作成功', + icon: 'mdi mdi-check-decagram', + type: 'green', + content: '编号:' + data.id + ' 任务已开始运行。', + buttons: { + okay: { + text: '关闭', + action: function () { + location.reload(); + } + } + } + }); + }, + function (response) { + AjaxError(response); + } + ); + } + }, + cancel: { + text: '取消', + keys: ['ctrl', 'shift'], + } + } }); }) }) diff --git a/configs/constants.go b/configs/constants.go index 39cc08d..2afae41 100644 --- a/configs/constants.go +++ b/configs/constants.go @@ -13,8 +13,11 @@ const ( // ProjectPort 项目端口 ProjectPort = ":9999" - // ProjectLogFile 项目日志存放文件 - ProjectLogFile = "./logs/" + ProjectName + "-access.log" + // ProjectAccessLogFile 项目访问日志存放文件 + ProjectAccessLogFile = "./logs/" + ProjectName + "-access.log" + + // ProjectCronLogFile 项目后台任务日志存放文件 + ProjectCronLogFile = "./logs/" + ProjectName + "-cron.log" // ProjectInstallMark 项目安装完成标识 ProjectInstallMark = "INSTALL.lock" diff --git a/docs/docs.go b/docs/docs.go index 69d9285..13c83d6 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -289,7 +289,7 @@ var doc = `{ }, "/api/admin/modify_password": { "patch": { - "description": "修改个人信息", + "description": "修改密码", "consumes": [ "multipart/form-data" ], @@ -299,19 +299,19 @@ var doc = `{ "tags": [ "API.admin" ], - "summary": "修改个人信息", + "summary": "修改密码", "parameters": [ { "type": "string", - "description": "昵称", - "name": "nickname", + "description": "旧密码", + "name": "old_password", "in": "formData", "required": true }, { "type": "string", - "description": "手机号", - "name": "mobile", + "description": "新密码", + "name": "new_password", "in": "formData", "required": true } @@ -320,7 +320,7 @@ var doc = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin_handler.modifyPersonalInfoResponse" + "$ref": "#/definitions/admin_handler.modifyPasswordResponse" } }, "400": { @@ -883,6 +883,403 @@ var doc = `{ } } }, + "/api/cron": { + "get": { + "description": "任务列表", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "任务列表", + "parameters": [ + { + "type": "integer", + "description": "第几页", + "name": "page", + "in": "query" + }, + { + "type": "string", + "description": "每页显示条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "任务名称", + "name": "name", + "in": "query" + }, + { + "type": "integer", + "description": "执行方式 1:shell 2:http", + "name": "protocol", + "in": "query" + }, + { + "type": "integer", + "description": "是否启用 1:是 -1:否", + "name": "is_used", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.listResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + }, + "post": { + "description": "创建任务", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "创建任务", + "parameters": [ + { + "type": "string", + "description": "任务名称", + "name": "name", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "crontab 表达式", + "name": "spec", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "执行命令", + "name": "command", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "执行方式 1:shell 2:http", + "name": "protocol", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "http 请求方式 1:get 2:post", + "name": "http_method", + "in": "formData" + }, + { + "type": "integer", + "description": "超时时间(单位:秒)", + "name": "timeout", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "重试次数", + "name": "retry_times", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "重试间隔(单位:秒)", + "name": "retry_interval", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知", + "name": "notify_status", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "通知类型 1:邮件 2:webhook", + "name": "notify_type", + "in": "formData" + }, + { + "type": "string", + "description": "通知者邮箱地址(多个用,分割)", + "name": "notify_receiver_email", + "in": "formData" + }, + { + "type": "string", + "description": "通知匹配关键字(多个用,分割)", + "name": "notify_keyword", + "in": "formData" + }, + { + "type": "string", + "description": "备注", + "name": "remark", + "in": "formData" + }, + { + "type": "integer", + "description": "是否启用 1:是 -1:否", + "name": "is_used", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.createResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/api/cron/:id": { + "get": { + "description": "获取单条任务详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "获取单条任务详情", + "parameters": [ + { + "type": "string", + "description": "hashId", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.detailResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/api/cron/used": { + "patch": { + "description": "更新任务为启用/禁用", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "更新任务为启用/禁用", + "parameters": [ + { + "type": "string", + "description": "Hashid", + "name": "id", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "是否启用 1:是 -1:否", + "name": "used", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.updateUsedResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/api/cron/{id}": { + "post": { + "description": "编辑任务", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "编辑任务", + "parameters": [ + { + "type": "string", + "description": "Hashid", + "name": "id", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "任务名称", + "name": "name", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "crontab 表达式", + "name": "spec", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "执行命令", + "name": "command", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "执行方式 1:shell 2:http", + "name": "protocol", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "http 请求方式 1:get 2:post", + "name": "http_method", + "in": "formData" + }, + { + "type": "integer", + "description": "超时时间(单位:秒)", + "name": "timeout", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "重试次数", + "name": "retry_times", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "重试间隔(单位:秒)", + "name": "retry_interval", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知", + "name": "notify_status", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "通知类型 1:邮件 2:webhook", + "name": "notify_type", + "in": "formData" + }, + { + "type": "string", + "description": "通知者邮箱地址(多个用,分割)", + "name": "notify_receiver_email", + "in": "formData" + }, + { + "type": "string", + "description": "通知匹配关键字(多个用,分割)", + "name": "notify_keyword", + "in": "formData" + }, + { + "type": "string", + "description": "备注", + "name": "remark", + "in": "formData" + }, + { + "type": "integer", + "description": "是否启用 1:是 -1:否", + "name": "is_used", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.modifyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/api/menu": { "get": { "description": "菜单列表", @@ -1633,7 +2030,7 @@ var doc = `{ "current_page": { "type": "integer" }, - "pre_page_count": { + "per_page_count": { "type": "integer" }, "total": { @@ -1888,7 +2285,7 @@ var doc = `{ "current_page": { "type": "integer" }, - "pre_page_count": { + "per_page_count": { "type": "integer" }, "total": { @@ -1929,6 +2326,204 @@ var doc = `{ } } }, + "cron_handler.createResponse": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "cron_handler.detailResponse": { + "type": "object", + "properties": { + "command": { + "description": "执行命令", + "type": "string" + }, + "http_method": { + "description": "http 请求方式 1:get 2:post", + "type": "integer" + }, + "is_used": { + "description": "是否启用 1:是 -1:否", + "type": "integer" + }, + "name": { + "description": "任务名称", + "type": "string" + }, + "notify_keyword": { + "description": "通知匹配关键字(多个用,分割)", + "type": "string" + }, + "notify_receiver_email": { + "description": "通知者邮箱地址(多个用,分割)", + "type": "string" + }, + "notify_status": { + "description": "执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知", + "type": "integer" + }, + "notify_type": { + "description": "通知类型 1:邮件 2:webhook", + "type": "integer" + }, + "protocol": { + "description": "执行方式 1:shell 2:http", + "type": "integer" + }, + "remark": { + "description": "备注", + "type": "string" + }, + "retry_interval": { + "description": "重试间隔(单位:秒)", + "type": "integer" + }, + "retry_times": { + "description": "重试次数", + "type": "integer" + }, + "spec": { + "description": "crontab 表达式", + "type": "string" + }, + "timeout": { + "description": "超时时间(单位:秒)", + "type": "integer" + } + } + }, + "cron_handler.listData": { + "type": "object", + "properties": { + "command": { + "description": "执行命令", + "type": "string" + }, + "created_at": { + "description": "创建时间", + "type": "string" + }, + "created_user": { + "description": "创建人", + "type": "string" + }, + "hashid": { + "description": "hashid", + "type": "string" + }, + "http_method": { + "description": "http 请求方式 1:get 2:post", + "type": "integer" + }, + "http_method_text": { + "description": "http 请求方式", + "type": "string" + }, + "id": { + "description": "ID", + "type": "integer" + }, + "is_used": { + "description": "是否启用 1=启用 2=禁用", + "type": "integer" + }, + "is_used_text": { + "description": "是否启用", + "type": "string" + }, + "name": { + "description": "任务名称", + "type": "string" + }, + "notify_status": { + "description": "执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知", + "type": "integer" + }, + "notify_status_text": { + "description": "执行结束是否通知", + "type": "string" + }, + "protocol": { + "description": "执行方式 1:shell 2:http", + "type": "integer" + }, + "protocol_text": { + "description": "执行方式", + "type": "string" + }, + "retry_interval": { + "description": "重试间隔(单位:秒)", + "type": "integer" + }, + "retry_times": { + "description": "重试次数", + "type": "integer" + }, + "spec": { + "description": "crontab 表达式", + "type": "string" + }, + "timeout": { + "description": "超时时间(单位:秒)", + "type": "integer" + }, + "updated_at": { + "description": "更新时间", + "type": "string" + }, + "updated_user": { + "description": "更新人", + "type": "string" + } + } + }, + "cron_handler.listResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/cron_handler.listData" + } + }, + "pagination": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "per_page_count": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + }, + "cron_handler.modifyResponse": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "cron_handler.updateUsedResponse": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, "menu_handler.createActionResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index d5d30d8..9403473 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -272,7 +272,7 @@ }, "/api/admin/modify_password": { "patch": { - "description": "修改个人信息", + "description": "修改密码", "consumes": [ "multipart/form-data" ], @@ -282,19 +282,19 @@ "tags": [ "API.admin" ], - "summary": "修改个人信息", + "summary": "修改密码", "parameters": [ { "type": "string", - "description": "昵称", - "name": "nickname", + "description": "旧密码", + "name": "old_password", "in": "formData", "required": true }, { "type": "string", - "description": "手机号", - "name": "mobile", + "description": "新密码", + "name": "new_password", "in": "formData", "required": true } @@ -303,7 +303,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/admin_handler.modifyPersonalInfoResponse" + "$ref": "#/definitions/admin_handler.modifyPasswordResponse" } }, "400": { @@ -866,6 +866,403 @@ } } }, + "/api/cron": { + "get": { + "description": "任务列表", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "任务列表", + "parameters": [ + { + "type": "integer", + "description": "第几页", + "name": "page", + "in": "query" + }, + { + "type": "string", + "description": "每页显示条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "任务名称", + "name": "name", + "in": "query" + }, + { + "type": "integer", + "description": "执行方式 1:shell 2:http", + "name": "protocol", + "in": "query" + }, + { + "type": "integer", + "description": "是否启用 1:是 -1:否", + "name": "is_used", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.listResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + }, + "post": { + "description": "创建任务", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "创建任务", + "parameters": [ + { + "type": "string", + "description": "任务名称", + "name": "name", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "crontab 表达式", + "name": "spec", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "执行命令", + "name": "command", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "执行方式 1:shell 2:http", + "name": "protocol", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "http 请求方式 1:get 2:post", + "name": "http_method", + "in": "formData" + }, + { + "type": "integer", + "description": "超时时间(单位:秒)", + "name": "timeout", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "重试次数", + "name": "retry_times", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "重试间隔(单位:秒)", + "name": "retry_interval", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知", + "name": "notify_status", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "通知类型 1:邮件 2:webhook", + "name": "notify_type", + "in": "formData" + }, + { + "type": "string", + "description": "通知者邮箱地址(多个用,分割)", + "name": "notify_receiver_email", + "in": "formData" + }, + { + "type": "string", + "description": "通知匹配关键字(多个用,分割)", + "name": "notify_keyword", + "in": "formData" + }, + { + "type": "string", + "description": "备注", + "name": "remark", + "in": "formData" + }, + { + "type": "integer", + "description": "是否启用 1:是 -1:否", + "name": "is_used", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.createResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/api/cron/:id": { + "get": { + "description": "获取单条任务详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "获取单条任务详情", + "parameters": [ + { + "type": "string", + "description": "hashId", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.detailResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/api/cron/used": { + "patch": { + "description": "更新任务为启用/禁用", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "更新任务为启用/禁用", + "parameters": [ + { + "type": "string", + "description": "Hashid", + "name": "id", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "是否启用 1:是 -1:否", + "name": "used", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.updateUsedResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/api/cron/{id}": { + "post": { + "description": "编辑任务", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API.cron" + ], + "summary": "编辑任务", + "parameters": [ + { + "type": "string", + "description": "Hashid", + "name": "id", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "任务名称", + "name": "name", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "crontab 表达式", + "name": "spec", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "执行命令", + "name": "command", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "执行方式 1:shell 2:http", + "name": "protocol", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "http 请求方式 1:get 2:post", + "name": "http_method", + "in": "formData" + }, + { + "type": "integer", + "description": "超时时间(单位:秒)", + "name": "timeout", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "重试次数", + "name": "retry_times", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "重试间隔(单位:秒)", + "name": "retry_interval", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知", + "name": "notify_status", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "通知类型 1:邮件 2:webhook", + "name": "notify_type", + "in": "formData" + }, + { + "type": "string", + "description": "通知者邮箱地址(多个用,分割)", + "name": "notify_receiver_email", + "in": "formData" + }, + { + "type": "string", + "description": "通知匹配关键字(多个用,分割)", + "name": "notify_keyword", + "in": "formData" + }, + { + "type": "string", + "description": "备注", + "name": "remark", + "in": "formData" + }, + { + "type": "integer", + "description": "是否启用 1:是 -1:否", + "name": "is_used", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/cron_handler.modifyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/api/menu": { "get": { "description": "菜单列表", @@ -1616,7 +2013,7 @@ "current_page": { "type": "integer" }, - "pre_page_count": { + "per_page_count": { "type": "integer" }, "total": { @@ -1871,7 +2268,7 @@ "current_page": { "type": "integer" }, - "pre_page_count": { + "per_page_count": { "type": "integer" }, "total": { @@ -1912,6 +2309,204 @@ } } }, + "cron_handler.createResponse": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "cron_handler.detailResponse": { + "type": "object", + "properties": { + "command": { + "description": "执行命令", + "type": "string" + }, + "http_method": { + "description": "http 请求方式 1:get 2:post", + "type": "integer" + }, + "is_used": { + "description": "是否启用 1:是 -1:否", + "type": "integer" + }, + "name": { + "description": "任务名称", + "type": "string" + }, + "notify_keyword": { + "description": "通知匹配关键字(多个用,分割)", + "type": "string" + }, + "notify_receiver_email": { + "description": "通知者邮箱地址(多个用,分割)", + "type": "string" + }, + "notify_status": { + "description": "执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知", + "type": "integer" + }, + "notify_type": { + "description": "通知类型 1:邮件 2:webhook", + "type": "integer" + }, + "protocol": { + "description": "执行方式 1:shell 2:http", + "type": "integer" + }, + "remark": { + "description": "备注", + "type": "string" + }, + "retry_interval": { + "description": "重试间隔(单位:秒)", + "type": "integer" + }, + "retry_times": { + "description": "重试次数", + "type": "integer" + }, + "spec": { + "description": "crontab 表达式", + "type": "string" + }, + "timeout": { + "description": "超时时间(单位:秒)", + "type": "integer" + } + } + }, + "cron_handler.listData": { + "type": "object", + "properties": { + "command": { + "description": "执行命令", + "type": "string" + }, + "created_at": { + "description": "创建时间", + "type": "string" + }, + "created_user": { + "description": "创建人", + "type": "string" + }, + "hashid": { + "description": "hashid", + "type": "string" + }, + "http_method": { + "description": "http 请求方式 1:get 2:post", + "type": "integer" + }, + "http_method_text": { + "description": "http 请求方式", + "type": "string" + }, + "id": { + "description": "ID", + "type": "integer" + }, + "is_used": { + "description": "是否启用 1=启用 2=禁用", + "type": "integer" + }, + "is_used_text": { + "description": "是否启用", + "type": "string" + }, + "name": { + "description": "任务名称", + "type": "string" + }, + "notify_status": { + "description": "执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知", + "type": "integer" + }, + "notify_status_text": { + "description": "执行结束是否通知", + "type": "string" + }, + "protocol": { + "description": "执行方式 1:shell 2:http", + "type": "integer" + }, + "protocol_text": { + "description": "执行方式", + "type": "string" + }, + "retry_interval": { + "description": "重试间隔(单位:秒)", + "type": "integer" + }, + "retry_times": { + "description": "重试次数", + "type": "integer" + }, + "spec": { + "description": "crontab 表达式", + "type": "string" + }, + "timeout": { + "description": "超时时间(单位:秒)", + "type": "integer" + }, + "updated_at": { + "description": "更新时间", + "type": "string" + }, + "updated_user": { + "description": "更新人", + "type": "string" + } + } + }, + "cron_handler.listResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/cron_handler.listData" + } + }, + "pagination": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "per_page_count": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + }, + "cron_handler.modifyResponse": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "cron_handler.updateUsedResponse": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, "menu_handler.createActionResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bece00b..47bbc4e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -83,7 +83,7 @@ definitions: properties: current_page: type: integer - pre_page_count: + per_page_count: type: integer total: type: integer @@ -259,7 +259,7 @@ definitions: properties: current_page: type: integer - pre_page_count: + per_page_count: type: integer total: type: integer @@ -286,6 +286,148 @@ definitions: description: 邮箱地址 type: string type: object + cron_handler.createResponse: + properties: + id: + description: 主键ID + type: integer + type: object + cron_handler.detailResponse: + properties: + command: + description: 执行命令 + type: string + http_method: + description: http 请求方式 1:get 2:post + type: integer + is_used: + description: 是否启用 1:是 -1:否 + type: integer + name: + description: 任务名称 + type: string + notify_keyword: + description: 通知匹配关键字(多个用,分割) + type: string + notify_receiver_email: + description: 通知者邮箱地址(多个用,分割) + type: string + notify_status: + description: 执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知 + type: integer + notify_type: + description: 通知类型 1:邮件 2:webhook + type: integer + protocol: + description: 执行方式 1:shell 2:http + type: integer + remark: + description: 备注 + type: string + retry_interval: + description: 重试间隔(单位:秒) + type: integer + retry_times: + description: 重试次数 + type: integer + spec: + description: crontab 表达式 + type: string + timeout: + description: 超时时间(单位:秒) + type: integer + type: object + cron_handler.listData: + properties: + command: + description: 执行命令 + type: string + created_at: + description: 创建时间 + type: string + created_user: + description: 创建人 + type: string + hashid: + description: hashid + type: string + http_method: + description: http 请求方式 1:get 2:post + type: integer + http_method_text: + description: http 请求方式 + type: string + id: + description: ID + type: integer + is_used: + description: 是否启用 1=启用 2=禁用 + type: integer + is_used_text: + description: 是否启用 + type: string + name: + description: 任务名称 + type: string + notify_status: + description: 执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知 + type: integer + notify_status_text: + description: 执行结束是否通知 + type: string + protocol: + description: 执行方式 1:shell 2:http + type: integer + protocol_text: + description: 执行方式 + type: string + retry_interval: + description: 重试间隔(单位:秒) + type: integer + retry_times: + description: 重试次数 + type: integer + spec: + description: crontab 表达式 + type: string + timeout: + description: 超时时间(单位:秒) + type: integer + updated_at: + description: 更新时间 + type: string + updated_user: + description: 更新人 + type: string + type: object + cron_handler.listResponse: + properties: + list: + items: + $ref: '#/definitions/cron_handler.listData' + type: array + pagination: + properties: + current_page: + type: integer + per_page_count: + type: integer + total: + type: integer + type: object + type: object + cron_handler.modifyResponse: + properties: + id: + description: 主键ID + type: integer + type: object + cron_handler.updateUsedResponse: + properties: + id: + description: 主键ID + type: integer + type: object menu_handler.createActionResponse: properties: id: @@ -709,16 +851,16 @@ paths: patch: consumes: - multipart/form-data - description: 修改个人信息 + description: 修改密码 parameters: - - description: 昵称 + - description: 旧密码 in: formData - name: nickname + name: old_password required: true type: string - - description: 手机号 + - description: 新密码 in: formData - name: mobile + name: new_password required: true type: string produces: @@ -727,12 +869,12 @@ paths: "200": description: OK schema: - $ref: '#/definitions/admin_handler.modifyPersonalInfoResponse' + $ref: '#/definitions/admin_handler.modifyPasswordResponse' "400": description: Bad Request schema: $ref: '#/definitions/code.Failure' - summary: 修改个人信息 + summary: 修改密码 tags: - API.admin /api/admin/offline: @@ -1077,6 +1219,275 @@ paths: summary: 修改邮件配置 tags: - API.config + /api/cron: + get: + consumes: + - multipart/form-data + description: 任务列表 + parameters: + - description: 第几页 + in: query + name: page + type: integer + - description: 每页显示条数 + in: query + name: page_size + type: string + - description: 任务名称 + in: query + name: name + type: string + - description: 执行方式 1:shell 2:http + in: query + name: protocol + type: integer + - description: 是否启用 1:是 -1:否 + in: query + name: is_used + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/cron_handler.listResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + summary: 任务列表 + tags: + - API.cron + post: + consumes: + - multipart/form-data + description: 创建任务 + parameters: + - description: 任务名称 + in: formData + name: name + required: true + type: string + - description: crontab 表达式 + in: formData + name: spec + required: true + type: string + - description: 执行命令 + in: formData + name: command + required: true + type: string + - description: 执行方式 1:shell 2:http + in: formData + name: protocol + required: true + type: integer + - description: http 请求方式 1:get 2:post + in: formData + name: http_method + type: integer + - description: 超时时间(单位:秒) + in: formData + name: timeout + required: true + type: integer + - description: 重试次数 + in: formData + name: retry_times + required: true + type: integer + - description: 重试间隔(单位:秒) + in: formData + name: retry_interval + required: true + type: integer + - description: 执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知 + in: formData + name: notify_status + required: true + type: integer + - description: 通知类型 1:邮件 2:webhook + in: formData + name: notify_type + type: integer + - description: 通知者邮箱地址(多个用,分割) + in: formData + name: notify_receiver_email + type: string + - description: 通知匹配关键字(多个用,分割) + in: formData + name: notify_keyword + type: string + - description: 备注 + in: formData + name: remark + type: string + - description: 是否启用 1:是 -1:否 + in: formData + name: is_used + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/cron_handler.createResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + summary: 创建任务 + tags: + - API.cron + /api/cron/:id: + get: + consumes: + - application/json + description: 获取单条任务详情 + parameters: + - description: hashId + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/cron_handler.detailResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + summary: 获取单条任务详情 + tags: + - API.cron + /api/cron/{id}: + post: + consumes: + - multipart/form-data + description: 编辑任务 + parameters: + - description: Hashid + in: formData + name: id + required: true + type: string + - description: 任务名称 + in: formData + name: name + required: true + type: string + - description: crontab 表达式 + in: formData + name: spec + required: true + type: string + - description: 执行命令 + in: formData + name: command + required: true + type: string + - description: 执行方式 1:shell 2:http + in: formData + name: protocol + required: true + type: integer + - description: http 请求方式 1:get 2:post + in: formData + name: http_method + type: integer + - description: 超时时间(单位:秒) + in: formData + name: timeout + required: true + type: integer + - description: 重试次数 + in: formData + name: retry_times + required: true + type: integer + - description: 重试间隔(单位:秒) + in: formData + name: retry_interval + required: true + type: integer + - description: 执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知 + in: formData + name: notify_status + required: true + type: integer + - description: 通知类型 1:邮件 2:webhook + in: formData + name: notify_type + type: integer + - description: 通知者邮箱地址(多个用,分割) + in: formData + name: notify_receiver_email + type: string + - description: 通知匹配关键字(多个用,分割) + in: formData + name: notify_keyword + type: string + - description: 备注 + in: formData + name: remark + type: string + - description: 是否启用 1:是 -1:否 + in: formData + name: is_used + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/cron_handler.modifyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + summary: 编辑任务 + tags: + - API.cron + /api/cron/used: + patch: + consumes: + - multipart/form-data + description: 更新任务为启用/禁用 + parameters: + - description: Hashid + in: formData + name: id + required: true + type: string + - description: 是否启用 1:是 -1:否 + in: formData + name: used + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/cron_handler.updateUsedResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + summary: 更新任务为启用/禁用 + tags: + - API.cron /api/menu: get: consumes: diff --git a/go.mod b/go.mod index 5d794d0..a9d956a 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-playground/validator/v10 v10.2.0 github.com/go-redis/redis/v7 v7.4.0 github.com/golang/protobuf v1.5.2 + github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5 github.com/jinzhu/gorm v1.9.16 github.com/onsi/ginkgo v1.14.2 // indirect github.com/onsi/gomega v1.10.4 // indirect diff --git a/go.sum b/go.sum index fc75227..7b1721b 100644 --- a/go.sum +++ b/go.sum @@ -325,6 +325,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5 h1:kCvm3G3u+eTRbjfLPyfsfznJtraYEfZer/UvQ6CaQhI= +github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5/go.mod h1:6DM2KNNK69jRu0lAHmYK9LYxmqpNjYHOaNp/ZxttD4U= github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= diff --git a/internal/api/controller/admin_handler/func_list.go b/internal/api/controller/admin_handler/func_list.go index a5845d6..099df4c 100755 --- a/internal/api/controller/admin_handler/func_list.go +++ b/internal/api/controller/admin_handler/func_list.go @@ -41,7 +41,7 @@ type listResponse struct { Pagination struct { Total int `json:"total"` CurrentPage int `json:"current_page"` - PrePageCount int `json:"pre_page_count"` + PerPageCount int `json:"per_page_count"` } `json:"pagination"` } @@ -109,7 +109,7 @@ func (h *handler) List() core.HandlerFunc { return } res.Pagination.Total = cast.ToInt(resCountData) - res.Pagination.PrePageCount = pageSize + res.Pagination.PerPageCount = pageSize res.Pagination.CurrentPage = page res.List = make([]listData, len(resListData)) diff --git a/internal/api/controller/authorized_handler/func_list.go b/internal/api/controller/authorized_handler/func_list.go index a5c37e5..e2ab8ec 100755 --- a/internal/api/controller/authorized_handler/func_list.go +++ b/internal/api/controller/authorized_handler/func_list.go @@ -40,7 +40,7 @@ type listResponse struct { Pagination struct { Total int `json:"total"` CurrentPage int `json:"current_page"` - PrePageCount int `json:"pre_page_count"` + PerPageCount int `json:"per_page_count"` } `json:"pagination"` } @@ -109,7 +109,7 @@ func (h *handler) List() core.HandlerFunc { return } res.Pagination.Total = cast.ToInt(resCountData) - res.Pagination.PrePageCount = pageSize + res.Pagination.PerPageCount = pageSize res.Pagination.CurrentPage = page res.List = make([]listData, len(resListData)) diff --git a/internal/api/controller/cron_handler/func_detail.go b/internal/api/controller/cron_handler/func_detail.go index c3ff66f..e7217aa 100755 --- a/internal/api/controller/cron_handler/func_detail.go +++ b/internal/api/controller/cron_handler/func_detail.go @@ -1,12 +1,12 @@ package cron_handler import ( - "github.com/xinliangnote/go-gin-api/internal/pkg/validation" "net/http" "github.com/xinliangnote/go-gin-api/internal/api/service/cron_service" "github.com/xinliangnote/go-gin-api/internal/pkg/code" "github.com/xinliangnote/go-gin-api/internal/pkg/core" + "github.com/xinliangnote/go-gin-api/internal/pkg/validation" "github.com/xinliangnote/go-gin-api/pkg/errno" "github.com/spf13/cast" diff --git a/internal/api/controller/cron_handler/func_execute.go b/internal/api/controller/cron_handler/func_execute.go new file mode 100755 index 0000000..98db53a --- /dev/null +++ b/internal/api/controller/cron_handler/func_execute.go @@ -0,0 +1,68 @@ +package cron_handler + +import ( + "net/http" + + "github.com/xinliangnote/go-gin-api/internal/pkg/code" + "github.com/xinliangnote/go-gin-api/internal/pkg/core" + "github.com/xinliangnote/go-gin-api/internal/pkg/validation" + "github.com/xinliangnote/go-gin-api/pkg/errno" + + "github.com/spf13/cast" +) + +type executeRequest struct { + Id string `uri:"id"` // HashID +} + +type executeResponse struct { + Id int `json:"id"` // ID +} + +// Execute 手动执行单条任务 +// @Summary 手动执行单条任务 +// @Description 手动执行单条任务 +// @Tags API.cron +// @Accept json +// @Produce json +// @Param id path string true "hashId" +// @Success 200 {object} detailResponse +// @Failure 400 {object} code.Failure +// @Router /api/cron/:id [patch] +func (h *handler) Execute() core.HandlerFunc { + return func(ctx core.Context) { + req := new(executeRequest) + res := new(executeResponse) + if err := ctx.ShouldBindURI(req); err != nil { + ctx.AbortWithError(errno.NewError( + http.StatusBadRequest, + code.ParamBindError, + validation.Error(err)).WithErr(err), + ) + return + } + + ids, err := h.hashids.HashidsDecode(req.Id) + if err != nil { + ctx.AbortWithError(errno.NewError( + http.StatusBadRequest, + code.HashIdsDecodeError, + code.Text(code.HashIdsDecodeError)).WithErr(err), + ) + return + } + + err = h.cronService.Execute(ctx, cast.ToInt32(ids[0])) + if err != nil { + ctx.AbortWithError(errno.NewError( + http.StatusBadRequest, + code.CronExecuteError, + code.Text(code.CronExecuteError)).WithErr(err), + ) + return + } + + res.Id = ids[0] + ctx.Payload(res) + } +} diff --git a/internal/api/controller/cron_handler/func_list.go b/internal/api/controller/cron_handler/func_list.go index c547b73..489c960 100755 --- a/internal/api/controller/cron_handler/func_list.go +++ b/internal/api/controller/cron_handler/func_list.go @@ -1,13 +1,13 @@ package cron_handler import ( - "github.com/xinliangnote/go-gin-api/internal/pkg/validation" "net/http" "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" "github.com/xinliangnote/go-gin-api/internal/api/service/cron_service" "github.com/xinliangnote/go-gin-api/internal/pkg/code" "github.com/xinliangnote/go-gin-api/internal/pkg/core" + "github.com/xinliangnote/go-gin-api/internal/pkg/validation" "github.com/xinliangnote/go-gin-api/pkg/errno" "github.com/xinliangnote/go-gin-api/pkg/time_parse" @@ -50,7 +50,7 @@ type listResponse struct { Pagination struct { Total int `json:"total"` CurrentPage int `json:"current_page"` - PrePageCount int `json:"pre_page_count"` + PerPageCount int `json:"per_page_count"` } `json:"pagination"` } @@ -119,7 +119,7 @@ func (h *handler) List() core.HandlerFunc { } res.Pagination.Total = cast.ToInt(resCountData) - res.Pagination.PrePageCount = pageSize + res.Pagination.PerPageCount = pageSize res.Pagination.CurrentPage = page res.List = make([]listData, len(resListData)) diff --git a/internal/api/controller/cron_handler/func_modify.go b/internal/api/controller/cron_handler/func_modify.go index e22298c..da78166 100755 --- a/internal/api/controller/cron_handler/func_modify.go +++ b/internal/api/controller/cron_handler/func_modify.go @@ -1,12 +1,13 @@ package cron_handler import ( + "net/http" + "github.com/xinliangnote/go-gin-api/internal/api/service/cron_service" "github.com/xinliangnote/go-gin-api/internal/pkg/code" "github.com/xinliangnote/go-gin-api/internal/pkg/core" "github.com/xinliangnote/go-gin-api/internal/pkg/validation" "github.com/xinliangnote/go-gin-api/pkg/errno" - "net/http" ) type modifyRequest struct { diff --git a/internal/api/controller/cron_handler/func_updateused.go b/internal/api/controller/cron_handler/func_updateused.go index e974c72..49cb641 100755 --- a/internal/api/controller/cron_handler/func_updateused.go +++ b/internal/api/controller/cron_handler/func_updateused.go @@ -1,11 +1,11 @@ package cron_handler import ( - "github.com/xinliangnote/go-gin-api/internal/pkg/validation" "net/http" "github.com/xinliangnote/go-gin-api/internal/pkg/code" "github.com/xinliangnote/go-gin-api/internal/pkg/core" + "github.com/xinliangnote/go-gin-api/internal/pkg/validation" "github.com/xinliangnote/go-gin-api/pkg/errno" ) diff --git a/internal/api/controller/cron_handler/handler.go b/internal/api/controller/cron_handler/handler.go index 796c79e..6bb2a52 100644 --- a/internal/api/controller/cron_handler/handler.go +++ b/internal/api/controller/cron_handler/handler.go @@ -3,6 +3,7 @@ package cron_handler import ( "github.com/xinliangnote/go-gin-api/configs" "github.com/xinliangnote/go-gin-api/internal/api/service/cron_service" + "github.com/xinliangnote/go-gin-api/internal/cron/cron_server" "github.com/xinliangnote/go-gin-api/internal/pkg/cache" "github.com/xinliangnote/go-gin-api/internal/pkg/core" "github.com/xinliangnote/go-gin-api/internal/pkg/db" @@ -40,6 +41,11 @@ type Handler interface { // @Tags API.cron // @Router /api/cron/:id [get] Detail() core.HandlerFunc + + // Execute 手动执行任务 + // @Tags API.cron + // @Router /api/cron/:id [patch] + Execute() core.HandlerFunc } type handler struct { @@ -49,12 +55,12 @@ type handler struct { cronService cron_service.Service } -func New(logger *zap.Logger, db db.Repo, cache cache.Repo) Handler { +func New(logger *zap.Logger, db db.Repo, cache cache.Repo, cron cron_server.Server) Handler { return &handler{ logger: logger, cache: cache, hashids: hash.New(configs.Get().HashIds.Secret, configs.Get().HashIds.Length), - cronService: cron_service.New(db, cache), + cronService: cron_service.New(db, cache, cron), } } diff --git a/internal/api/service/cron_service/service.go b/internal/api/service/cron_service/service.go index 37b3352..8f7fe65 100644 --- a/internal/api/service/cron_service/service.go +++ b/internal/api/service/cron_service/service.go @@ -2,6 +2,7 @@ package cron_service import ( "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" + "github.com/xinliangnote/go-gin-api/internal/cron/cron_server" "github.com/xinliangnote/go-gin-api/internal/pkg/cache" "github.com/xinliangnote/go-gin-api/internal/pkg/core" "github.com/xinliangnote/go-gin-api/internal/pkg/db" @@ -17,18 +18,21 @@ type Service interface { PageList(ctx core.Context, searchData *SearchData) (listData []*cron_task_repo.CronTask, err error) PageListCount(ctx core.Context, searchData *SearchData) (total int64, err error) UpdateUsed(ctx core.Context, id int32, used int32) (err error) + Execute(ctx core.Context, id int32) (err error) Detail(ctx core.Context, searchOneData *SearchOneData) (info *cron_task_repo.CronTask, err error) } type service struct { - db db.Repo - cache cache.Repo + db db.Repo + cache cache.Repo + cronServer cron_server.Server } -func New(db db.Repo, cache cache.Repo) Service { +func New(db db.Repo, cache cache.Repo, cron cron_server.Server) Service { return &service{ - db: db, - cache: cache, + db: db, + cache: cache, + cronServer: cron, } } diff --git a/internal/api/service/cron_service/service_create.go b/internal/api/service/cron_service/service_create.go index b58e5c6..055bd3f 100644 --- a/internal/api/service/cron_service/service_create.go +++ b/internal/api/service/cron_service/service_create.go @@ -45,5 +45,7 @@ func (s *service) Create(ctx core.Context, createData *CreateCronTaskData) (id i return 0, err } + s.cronServer.AddTask(model) + return } diff --git a/internal/api/service/cron_service/service_execute.go b/internal/api/service/cron_service/service_execute.go new file mode 100644 index 0000000..0088612 --- /dev/null +++ b/internal/api/service/cron_service/service_execute.go @@ -0,0 +1,21 @@ +package cron_service + +import ( + "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo" + "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" + "github.com/xinliangnote/go-gin-api/internal/pkg/core" +) + +func (s *service) Execute(ctx core.Context, id int32) (err error) { + qb := cron_task_repo.NewQueryBuilder() + qb.WhereId(db_repo.EqualPredicate, id) + info, err := qb.QueryOne(s.db.GetDbR().WithContext(ctx.RequestContext())) + if err != nil { + return err + } + + info.Spec = "手动执行" + go s.cronServer.AddJob(info)() + + return nil +} diff --git a/internal/api/service/cron_service/service_modify.go b/internal/api/service/cron_service/service_modify.go index 0ca9fae..717fdf3 100644 --- a/internal/api/service/cron_service/service_modify.go +++ b/internal/api/service/cron_service/service_modify.go @@ -4,6 +4,8 @@ import ( "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo" "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" "github.com/xinliangnote/go-gin-api/internal/pkg/core" + + "github.com/spf13/cast" ) type ModifyCronTaskData struct { @@ -49,5 +51,21 @@ func (s *service) Modify(ctx core.Context, id int32, modifyData *ModifyCronTaskD return err } + // region 操作定时任务 避免主从同步延迟,在这需要查询主库 + if modifyData.IsUsed == cron_task_repo.IsUsedNo { + s.cronServer.RemoveTask(cast.ToInt(id)) + } else { + qb = cron_task_repo.NewQueryBuilder() + qb.WhereId(db_repo.EqualPredicate, id) + info, err := qb.QueryOne(s.db.GetDbW().WithContext(ctx.RequestContext())) + if err != nil { + return err + } + + s.cronServer.RemoveTask(cast.ToInt(id)) + s.cronServer.AddTask(info) + } + // endregion + return } diff --git a/internal/api/service/cron_service/service_updateused.go b/internal/api/service/cron_service/service_updateused.go index 7bb1953..0cb04de 100644 --- a/internal/api/service/cron_service/service_updateused.go +++ b/internal/api/service/cron_service/service_updateused.go @@ -4,6 +4,8 @@ import ( "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo" "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" "github.com/xinliangnote/go-gin-api/internal/pkg/core" + + "github.com/spf13/cast" ) func (s *service) UpdateUsed(ctx core.Context, id int32, used int32) (err error) { @@ -19,5 +21,22 @@ func (s *service) UpdateUsed(ctx core.Context, id int32, used int32) (err error) return err } + // region 操作定时任务 避免主从同步延迟,在这需要查询主库 + if used == cron_task_repo.IsUsedNo { + s.cronServer.RemoveTask(cast.ToInt(id)) + } else { + qb = cron_task_repo.NewQueryBuilder() + qb.WhereId(db_repo.EqualPredicate, id) + info, err := qb.QueryOne(s.db.GetDbW().WithContext(ctx.RequestContext())) + if err != nil { + return err + } + + s.cronServer.RemoveTask(cast.ToInt(id)) + s.cronServer.AddTask(info) + + } + // endregion + return } diff --git a/internal/cron/cron_server/server.go b/internal/cron/cron_server/server.go new file mode 100644 index 0000000..5cce0f5 --- /dev/null +++ b/internal/cron/cron_server/server.go @@ -0,0 +1,85 @@ +package cron_server + +import ( + "sync" + + "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" + "github.com/xinliangnote/go-gin-api/internal/pkg/cache" + "github.com/xinliangnote/go-gin-api/internal/pkg/db" + "github.com/xinliangnote/go-gin-api/pkg/errors" + + "github.com/jakecoffman/cron" + "go.uber.org/zap" +) + +var _ Server = (*server)(nil) + +type taskCount struct { + wg sync.WaitGroup + exit chan struct{} +} + +func (tc *taskCount) i() {} + +func (tc *taskCount) Add() { + tc.wg.Add(1) +} + +func (tc *taskCount) Done() { + tc.wg.Done() +} + +func (tc *taskCount) Exit() { + tc.wg.Done() + <-tc.exit +} + +func (tc *taskCount) Wait() { + tc.Add() + tc.wg.Wait() + close(tc.exit) +} + +type server struct { + logger *zap.Logger + db db.Repo + cache cache.Repo + cron *cron.Cron + taskCount *taskCount +} + +type Server interface { + i() + Start() + Stop() + AddTask(task *cron_task_repo.CronTask) + AddJob(task *cron_task_repo.CronTask) cron.FuncJob + RemoveTask(taskId int) +} + +func New(logger *zap.Logger, db db.Repo, cache cache.Repo) (Server, error) { + if logger == nil { + return nil, errors.New("logger required") + } + + if db == nil { + return nil, errors.New("db required") + } + + if cache == nil { + return nil, errors.New("cache required") + } + + return &server{ + logger: logger, + db: db, + cache: cache, + cron: cron.New(), + taskCount: &taskCount{ + wg: sync.WaitGroup{}, + exit: make(chan struct{}), + }, + }, nil +} + +func (s *server) i() {} diff --git a/internal/cron/cron_server/service_add_job.go b/internal/cron/cron_server/service_add_job.go new file mode 100644 index 0000000..414e953 --- /dev/null +++ b/internal/cron/cron_server/service_add_job.go @@ -0,0 +1,19 @@ +package cron_server + +import ( + "fmt" + + "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" + + "github.com/jakecoffman/cron" +) + +func (s *server) AddJob(task *cron_task_repo.CronTask) cron.FuncJob { + return func() { + s.taskCount.Add() + defer s.taskCount.Done() + + msg := fmt.Sprintf("开始执行任务:(%d)%s [%s]", task.Id, task.Name, task.Spec) + s.logger.Info(msg) + } +} diff --git a/internal/cron/cron_server/service_add_task.go b/internal/cron/cron_server/service_add_task.go new file mode 100644 index 0000000..b16eb70 --- /dev/null +++ b/internal/cron/cron_server/service_add_task.go @@ -0,0 +1,16 @@ +package cron_server + +import ( + "strings" + + "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" + + "github.com/spf13/cast" +) + +func (s *server) AddTask(task *cron_task_repo.CronTask) { + spec := "0 " + strings.TrimSpace(task.Spec) + name := cast.ToString(task.Id) + + s.cron.AddFunc(spec, s.AddJob(task), name) +} diff --git a/internal/cron/cron_server/service_remove_task.go b/internal/cron/cron_server/service_remove_task.go new file mode 100644 index 0000000..7580d9a --- /dev/null +++ b/internal/cron/cron_server/service_remove_task.go @@ -0,0 +1,8 @@ +package cron_server + +import "github.com/spf13/cast" + +func (s *server) RemoveTask(taskId int) { + name := cast.ToString(taskId) + s.cron.RemoveJob(name) +} diff --git a/internal/cron/cron_server/service_start.go b/internal/cron/cron_server/service_start.go new file mode 100644 index 0000000..73843d4 --- /dev/null +++ b/internal/cron/cron_server/service_start.go @@ -0,0 +1,49 @@ +package cron_server + +import ( + "fmt" + "math" + + "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo" + "github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/cron_task_repo" + + "go.uber.org/zap" +) + +func (s *server) Start() { + s.cron.Start() + go s.taskCount.Wait() + + qb := cron_task_repo.NewQueryBuilder() + qb.WhereIsUsed(db_repo.EqualPredicate, cron_task_repo.IsUsedYES) + totalNum, err := qb.Count(s.db.GetDbR()) + if err != nil { + s.logger.Fatal("cron initialize tasks count err", zap.Error(err)) + } + + pageSize := 50 + maxPage := int(math.Ceil(float64(totalNum) / float64(pageSize))) + + taskNum := 0 + s.logger.Info("开始初始化后台任务") + + for page := 1; page <= maxPage; page++ { + qb = cron_task_repo.NewQueryBuilder() + qb.WhereIsUsed(db_repo.EqualPredicate, cron_task_repo.IsUsedYES) + listData, err := qb. + Limit(pageSize). + Offset((page - 1) * pageSize). + OrderById(false). + QueryAll(s.db.GetDbR()) + if err != nil { + s.logger.Fatal("cron initialize tasks list err", zap.Error(err)) + } + + for _, item := range listData { + s.AddTask(item) + taskNum++ + } + } + + s.logger.Info(fmt.Sprintf("后台任务初始化完成,总数量:%d", taskNum)) +} diff --git a/internal/cron/cron_server/service_stop.go b/internal/cron/cron_server/service_stop.go new file mode 100644 index 0000000..98e4697 --- /dev/null +++ b/internal/cron/cron_server/service_stop.go @@ -0,0 +1,6 @@ +package cron_server + +func (s *server) Stop() { + s.cron.Stop() + s.taskCount.Exit() +} diff --git a/internal/pkg/code/code.go b/internal/pkg/code/code.go index 64feffc..e20f055 100644 --- a/internal/pkg/code/code.go +++ b/internal/pkg/code/code.go @@ -61,10 +61,11 @@ const ( MenuListActionError = 20307 MenuDeleteActionError = 20308 - CronCreateError = 20401 - CronUpdateError = 20402 - CronListError = 20403 - CronDetailError = 20404 + CronCreateError = 20401 + CronUpdateError = 20402 + CronListError = 20403 + CronDetailError = 20404 + CronExecuteError = 20405 ) func Text(code int) string { diff --git a/internal/pkg/code/en-us.go b/internal/pkg/code/en-us.go index 77b0094..4f7f6f6 100644 --- a/internal/pkg/code/en-us.go +++ b/internal/pkg/code/en-us.go @@ -53,8 +53,9 @@ var enUSText = map[int]string{ MenuListActionError: "Failed to get menu action list", MenuDeleteActionError: "Failed to delete menu action", - CronCreateError: "Failed to create cron", - CronUpdateError: "Failed to update menu", - CronListError: "Failed to get cron list", - CronDetailError: "Failed to get cron detail", + CronCreateError: "Failed to create cron", + CronUpdateError: "Failed to update menu", + CronListError: "Failed to get cron list", + CronDetailError: "Failed to get cron detail", + CronExecuteError: "Failed to execute cron", } diff --git a/internal/pkg/code/zh-cn.go b/internal/pkg/code/zh-cn.go index a055b01..a937a8c 100644 --- a/internal/pkg/code/zh-cn.go +++ b/internal/pkg/code/zh-cn.go @@ -53,8 +53,9 @@ var zhCNText = map[int]string{ MenuListActionError: "获取菜单栏功能权限列表失败", MenuDeleteActionError: "删除菜单栏功能权限失败", - CronCreateError: "创建后台任务失败", - CronUpdateError: "更新后台任务失败", - CronListError: "获取定时任务列表失败", - CronDetailError: "获取定时任务详情失败", + CronCreateError: "创建后台任务失败", + CronUpdateError: "更新后台任务失败", + CronListError: "获取定时任务列表失败", + CronDetailError: "获取定时任务详情失败", + CronExecuteError: "手动执行定时任务失败", } diff --git a/internal/router/router.go b/internal/router/router.go index ccbdfb7..cfb9ba8 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,6 +2,7 @@ package router import ( "github.com/xinliangnote/go-gin-api/configs" + "github.com/xinliangnote/go-gin-api/internal/cron/cron_server" "github.com/xinliangnote/go-gin-api/internal/pkg/cache" "github.com/xinliangnote/go-gin-api/internal/pkg/core" "github.com/xinliangnote/go-gin-api/internal/pkg/db" @@ -16,22 +17,24 @@ import ( ) type resource struct { - mux core.Mux - logger *zap.Logger - db db.Repo - cache cache.Repo - grpConn grpc.ClientConn - middles middleware.Middleware + mux core.Mux + logger *zap.Logger + db db.Repo + cache cache.Repo + grpConn grpc.ClientConn + middles middleware.Middleware + cronServer cron_server.Server } type Server struct { - Mux core.Mux - Db db.Repo - Cache cache.Repo - GrpClient grpc.ClientConn + Mux core.Mux + Db db.Repo + Cache cache.Repo + GrpClient grpc.ClientConn + CronServer cron_server.Server } -func NewHTTPServer(logger *zap.Logger) (*Server, error) { +func NewHTTPServer(logger *zap.Logger, cronLogger *zap.Logger) (*Server, error) { if logger == nil { return nil, errors.New("logger required") } @@ -66,6 +69,14 @@ func NewHTTPServer(logger *zap.Logger) (*Server, error) { logger.Fatal("new grpc err", zap.Error(err)) } r.grpConn = gRPCRepo + + // 初始化 CRON Server + cronServer, err := cron_server.New(cronLogger, dbRepo, cacheRepo) + if err != nil { + logger.Fatal("new cron err", zap.Error(err)) + } + cronServer.Start() + r.cronServer = cronServer } mux, err := core.New(logger, @@ -97,6 +108,7 @@ func NewHTTPServer(logger *zap.Logger) (*Server, error) { s.Db = r.db s.Cache = r.cache s.GrpClient = r.grpConn + s.CronServer = r.cronServer return s, nil } diff --git a/internal/router/router_api.go b/internal/router/router_api.go index b6be032..15b31f3 100644 --- a/internal/router/router_api.go +++ b/internal/router/router_api.go @@ -81,12 +81,13 @@ func setApiRouter(r *resource) { api.PATCH("/config/email", configHandler.Email()) // cron - cronHandler := cron_handler.New(r.logger, r.db, r.cache) + cronHandler := cron_handler.New(r.logger, r.db, r.cache, r.cronServer) api.POST("/cron", cronHandler.Create()) api.GET("/cron", cronHandler.List()) api.GET("/cron/:id", cronHandler.Detail()) api.POST("/cron/:id", cronHandler.Modify()) api.PATCH("/cron/used", cronHandler.UpdateUsed()) + api.PATCH("/cron/exec/:id", cronHandler.Execute()) } } diff --git a/internal/web/controller/install_handler/mysql_table/table_menu_action.go b/internal/web/controller/install_handler/mysql_table/table_menu_action.go index 5e628ad..6775274 100644 --- a/internal/web/controller/install_handler/mysql_table/table_menu_action.go +++ b/internal/web/controller/install_handler/mysql_table/table_menu_action.go @@ -79,7 +79,8 @@ func CreateMenuActionTableDataSql() (sql string) { sql += "(43, 24, 'POST', '/api/cron/*', 'init')," sql += "(44, 24, 'GET', '/api/cron', 'init')," sql += "(45, 24, 'GET', '/api/cron/*', 'init')," - sql += "(46, 24, 'PATCH', '/api/cron/used', 'init');" + sql += "(46, 24, 'PATCH', '/api/cron/used', 'init')," + sql += "(47, 24, 'PATCH', '/api/cron/exec/*', 'init');" return } diff --git a/internal/web/controller/tool_handler/func_logsview.go b/internal/web/controller/tool_handler/func_logsview.go index fda8ca3..3c99ab1 100644 --- a/internal/web/controller/tool_handler/func_logsview.go +++ b/internal/web/controller/tool_handler/func_logsview.go @@ -44,7 +44,7 @@ func (h *handler) LogsView() core.HandlerFunc { } return func(c core.Context) { - readLineFromEnd, err := file.NewReadLineFromEnd(configs.ProjectLogFile) + readLineFromEnd, err := file.NewReadLineFromEnd(configs.ProjectAccessLogFile) if err != nil { h.logger.Error("NewReadLineFromEnd err", zap.Error(err)) } diff --git a/logs/go-gin-api-cron.log b/logs/go-gin-api-cron.log new file mode 100755 index 0000000..e69de29 diff --git a/main.go b/main.go index 0e179f8..6143342 100644 --- a/main.go +++ b/main.go @@ -29,23 +29,36 @@ import ( // @host 127.0.0.1:9999 // @BasePath func main() { - // 初始化 logger - loggers, err := logger.NewJSONLogger( + // 初始化 access logger + accessLogger, err := logger.NewJSONLogger( logger.WithDisableConsole(), logger.WithField("domain", fmt.Sprintf("%s[%s]", configs.ProjectName, env.Active().Value())), logger.WithTimeLayout("2006-01-02 15:04:05"), - logger.WithFileP(configs.ProjectLogFile), + logger.WithFileP(configs.ProjectAccessLogFile), ) if err != nil { panic(err) } + // 初始化 cron logger + cronLogger, err := logger.NewJSONLogger( + logger.WithDisableConsole(), + logger.WithField("domain", fmt.Sprintf("%s[%s]", configs.ProjectName, env.Active().Value())), + logger.WithTimeLayout("2006-01-02 15:04:05"), + logger.WithFileP(configs.ProjectCronLogFile), + ) + + if err != nil { + panic(err) + } + defer func() { - _ = loggers.Sync() + _ = accessLogger.Sync() + _ = cronLogger.Sync() }() // 初始化 HTTP 服务 - s, err := router.NewHTTPServer(loggers) + s, err := router.NewHTTPServer(accessLogger, cronLogger) if err != nil { panic(err) } @@ -57,7 +70,7 @@ func main() { go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - loggers.Fatal("http server startup err", zap.Error(err)) + accessLogger.Fatal("http server startup err", zap.Error(err)) } }() @@ -69,7 +82,7 @@ func main() { defer cancel() if err := server.Shutdown(ctx); err != nil { - loggers.Error("server shutdown err", zap.Error(err)) + accessLogger.Error("server shutdown err", zap.Error(err)) } }, @@ -77,11 +90,11 @@ func main() { func() { if s.Db != nil { if err := s.Db.DbWClose(); err != nil { - loggers.Error("dbw close err", zap.Error(err)) + accessLogger.Error("dbw close err", zap.Error(err)) } if err := s.Db.DbRClose(); err != nil { - loggers.Error("dbr close err", zap.Error(err)) + accessLogger.Error("dbr close err", zap.Error(err)) } } }, @@ -90,7 +103,7 @@ func main() { func() { if s.Cache != nil { if err := s.Cache.Close(); err != nil { - loggers.Error("cache close err", zap.Error(err)) + accessLogger.Error("cache close err", zap.Error(err)) } } }, @@ -99,9 +112,16 @@ func main() { func() { if s.GrpClient != nil { if err := s.GrpClient.Conn().Close(); err != nil { - loggers.Error("gRPC client close err", zap.Error(err)) + accessLogger.Error("gRPC client close err", zap.Error(err)) } } }, + + // 关闭 cron Server + func() { + if s.CronServer != nil { + s.CronServer.Stop() + } + }, ) }