diff --git a/assets/bootstrap/js/bootstrap-notify/bootstrap-notify.min.js b/assets/bootstrap/js/bootstrap-notify/bootstrap-notify.min.js
new file mode 100644
index 0000000..f5ad385
--- /dev/null
+++ b/assets/bootstrap/js/bootstrap-notify/bootstrap-notify.min.js
@@ -0,0 +1,2 @@
+/* Project: Bootstrap Growl = v3.1.3 | Description: Turns standard Bootstrap alerts into "Growl-like" notifications. | Author: Mouse0270 aka Robert McIntosh | License: MIT License | Website: https://github.com/mouse0270/bootstrap-growl */
+!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t("object"==typeof exports?require("jquery"):jQuery)}(function(t){function e(e,i,n){var i={content:{message:"object"==typeof i?i.message:i,title:i.title?i.title:"",icon:i.icon?i.icon:"",url:i.url?i.url:"#",target:i.target?i.target:"-"}};n=t.extend(!0,{},i,n),this.settings=t.extend(!0,{},s,n),this._defaults=s,"-"==this.settings.content.target&&(this.settings.content.target=this.settings.url_target),this.animations={start:"webkitAnimationStart oanimationstart MSAnimationStart animationstart",end:"webkitAnimationEnd oanimationend MSAnimationEnd animationend"},"number"==typeof this.settings.offset&&(this.settings.offset={x:this.settings.offset,y:this.settings.offset}),this.init()}var s={element:"body",position:null,type:"info",allow_dismiss:!0,newest_on_top:!1,showProgressbar:!1,placement:{from:"top",align:"right"},offset:20,spacing:10,z_index:1031,delay:5e3,timer:1e3,url_target:"_blank",mouse_over:null,animate:{enter:"animated fadeInDown",exit:"animated fadeOutUp"},onShow:null,onShown:null,onClose:null,onClosed:null,icon_type:"class",template:'
'};String.format=function(){for(var t=arguments[0],e=1;e .progress-bar').removeClass("progress-bar-"+t.settings.type),t.settings.type=i[e],this.$ele.addClass("alert-"+i[e]).find('[data-notify="progressbar"] > .progress-bar').addClass("progress-bar-"+i[e]);break;case"icon":var n=this.$ele.find('[data-notify="icon"]');"class"==t.settings.icon_type.toLowerCase()?n.removeClass(t.settings.content.icon).addClass(i[e]):(n.is("img")||n.find("img"),n.attr("src",i[e]));break;case"progress":var a=t.settings.delay-t.settings.delay*(i[e]/100);this.$ele.data("notify-delay",a),this.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i[e]).css("width",i[e]+"%");break;case"url":this.$ele.find('[data-notify="url"]').attr("href",i[e]);break;case"target":this.$ele.find('[data-notify="url"]').attr("target",i[e]);break;default:this.$ele.find('[data-notify="'+e+'"]').html(i[e])}var o=this.$ele.outerHeight()+parseInt(t.settings.spacing)+parseInt(t.settings.offset.y);t.reposition(o)},close:function(){t.close()}}},buildNotify:function(){var e=this.settings.content;this.$ele=t(String.format(this.settings.template,this.settings.type,e.title,e.message,e.url,e.target)),this.$ele.attr("data-notify-position",this.settings.placement.from+"-"+this.settings.placement.align),this.settings.allow_dismiss||this.$ele.find('[data-notify="dismiss"]').css("display","none"),(this.settings.delay<=0&&!this.settings.showProgressbar||!this.settings.showProgressbar)&&this.$ele.find('[data-notify="progressbar"]').remove()},setIcon:function(){"class"==this.settings.icon_type.toLowerCase()?this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon):this.$ele.find('[data-notify="icon"]').is("img")?this.$ele.find('[data-notify="icon"]').attr("src",this.settings.content.icon):this.$ele.find('[data-notify="icon"]').append('
')},styleURL:function(){this.$ele.find('[data-notify="url"]').css({backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)",height:"100%",left:"0px",position:"absolute",top:"0px",width:"100%",zIndex:this.settings.z_index+1}),this.$ele.find('[data-notify="dismiss"]').css({position:"absolute",right:"10px",top:"5px",zIndex:this.settings.z_index+2})},placement:function(){var e=this,s=this.settings.offset.y,i={display:"inline-block",margin:"0px auto",position:this.settings.position?this.settings.position:"body"===this.settings.element?"fixed":"absolute",transition:"all .5s ease-in-out",zIndex:this.settings.z_index},n=!1,a=this.settings;switch(t('[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])').each(function(){return s=Math.max(s,parseInt(t(this).css(a.placement.from))+parseInt(t(this).outerHeight())+parseInt(a.spacing))}),1==this.settings.newest_on_top&&(s=this.settings.offset.y),i[this.settings.placement.from]=s+"px",this.settings.placement.align){case"left":case"right":i[this.settings.placement.align]=this.settings.offset.x+"px";break;case"center":i.left=0,i.right=0}this.$ele.css(i).addClass(this.settings.animate.enter),t.each(Array("webkit","moz","o","ms",""),function(t,s){e.$ele[0].style[s+"AnimationIterationCount"]=1}),t(this.settings.element).append(this.$ele),1==this.settings.newest_on_top&&(s=parseInt(s)+parseInt(this.settings.spacing)+this.$ele.outerHeight(),this.reposition(s)),t.isFunction(e.settings.onShow)&&e.settings.onShow.call(this.$ele),this.$ele.one(this.animations.start,function(){n=!0}).one(this.animations.end,function(){t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)}),setTimeout(function(){n||t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)},600)},bind:function(){var e=this;if(this.$ele.find('[data-notify="dismiss"]').on("click",function(){e.close()}),this.$ele.mouseover(function(){t(this).data("data-hover","true")}).mouseout(function(){t(this).data("data-hover","false")}),this.$ele.data("data-hover","false"),this.settings.delay>0){e.$ele.data("notify-delay",e.settings.delay);var s=setInterval(function(){var t=parseInt(e.$ele.data("notify-delay"))-e.settings.timer;if("false"===e.$ele.data("data-hover")&&"pause"==e.settings.mouse_over||"pause"!=e.settings.mouse_over){var i=(e.settings.delay-t)/e.settings.delay*100;e.$ele.data("notify-delay",t),e.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i).css("width",i+"%")}t<=-e.settings.timer&&(clearInterval(s),e.close())},e.settings.timer)}},close:function(){var e=this,s=parseInt(this.$ele.css(this.settings.placement.from)),i=!1;this.$ele.data("closing","true").addClass(this.settings.animate.exit),e.reposition(s),t.isFunction(e.settings.onClose)&&e.settings.onClose.call(this.$ele),this.$ele.one(this.animations.start,function(){i=!0}).one(this.animations.end,function(){t(this).remove(),t.isFunction(e.settings.onClosed)&&e.settings.onClosed.call(this)}),setTimeout(function(){i||(e.$ele.remove(),e.settings.onClosed&&e.settings.onClosed(e.$ele))},600)},reposition:function(e){var s=this,i='[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])',n=this.$ele.nextAll(i);1==this.settings.newest_on_top&&(n=this.$ele.prevAll(i)),n.each(function(){t(this).css(s.settings.placement.from,e),e=parseInt(e)+parseInt(s.settings.spacing)+t(this).outerHeight()})}}),t.notify=function(t,s){var i=new e(this,t,s);return i.notify},t.notifyDefaults=function(e){return s=t.extend(!0,{},s,e)},t.notifyClose=function(e){"undefined"==typeof e||"all"==e?t("[data-notify]").find('[data-notify="dismiss"]').trigger("click"):t('[data-notify-position="'+e+'"]').find('[data-notify="dismiss"]').trigger("click")}});
\ No newline at end of file
diff --git a/assets/bootstrap/js/bootstrap-notify/notify.js b/assets/bootstrap/js/bootstrap-notify/notify.js
new file mode 100644
index 0000000..6bad70a
--- /dev/null
+++ b/assets/bootstrap/js/bootstrap-notify/notify.js
@@ -0,0 +1,32 @@
+document.write('');
+
+function SuccessNotify(content) {
+ $.notify({
+ icon: "mdi mdi-alert",
+ title: "",
+ message: content,
+ url: "",
+ target: ""
+ }, {
+ type: "success",
+ allow_dismiss: true,
+ newest_on_top: false,
+ placement: {
+ from: "top",
+ align: "right",
+ },
+ offset: {
+ x: "20",
+ y: "20"
+ },
+ spacing: "10",
+ z_index: "1031",
+ delay: "3000",
+ animate: {
+ enter: "animated fadeInDown",
+ exit: "animated fadeOutUp"
+ },
+ onClosed: null,
+ mouse_over: null
+ });
+}
diff --git a/assets/templates/install/install_view.html b/assets/templates/install/install_view.html
index b0a690e..ed04270 100644
--- a/assets/templates/install/install_view.html
+++ b/assets/templates/install/install_view.html
@@ -98,6 +98,9 @@
+
+ 请确保此数据库已存在!
+
diff --git a/assets/templates/tool/tool_websocket.html b/assets/templates/tool/tool_websocket.html
new file mode 100644
index 0000000..b92177e
--- /dev/null
+++ b/assets/templates/tool/tool_websocket.html
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/configs/constants.go b/configs/constants.go
index 2afae41..8f80db7 100644
--- a/configs/constants.go
+++ b/configs/constants.go
@@ -2,7 +2,7 @@ package configs
const (
// ProjectVersion 项目版本
- ProjectVersion = "v1.2.7"
+ ProjectVersion = "v1.2.8"
// ProjectName 项目名称
ProjectName = "go-gin-api"
diff --git a/docs/docs.go b/docs/docs.go
index 13c83d6..e95312c 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -177,9 +177,9 @@ var doc = `{
},
"/api/admin/login": {
"post": {
- "description": "管理员登出",
+ "description": "管理员登录",
"consumes": [
- "application/json"
+ "multipart/form-data"
],
"produces": [
"application/json"
@@ -187,12 +187,28 @@ var doc = `{
"tags": [
"API.admin"
],
- "summary": "管理员登出",
+ "summary": "管理员登录",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "用户名",
+ "name": "username",
+ "in": "formData",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "密码",
+ "name": "password",
+ "in": "formData",
+ "required": true
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/admin_handler.logoutResponse"
+ "$ref": "#/definitions/admin_handler.loginResponse"
}
},
"400": {
@@ -1102,6 +1118,42 @@ var doc = `{
}
}
}
+ },
+ "patch": {
+ "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": {
@@ -1907,6 +1959,44 @@ var doc = `{
}
}
}
+ },
+ "/api/tool/send_message": {
+ "post": {
+ "description": "发送消息",
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "API.tool"
+ ],
+ "summary": "发送消息",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "消息内容",
+ "name": "message",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/tool_handler.sendMessageResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/code.Failure"
+ }
+ }
+ }
+ }
}
},
"definitions": {
@@ -2794,6 +2884,15 @@ var doc = `{
}
}
},
+ "tool_handler.sendMessageResponse": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "description": "状态",
+ "type": "string"
+ }
+ }
+ },
"tool_handler.tableColumn": {
"type": "object",
"properties": {
diff --git a/docs/swagger.json b/docs/swagger.json
index 9403473..71939c5 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -160,9 +160,9 @@
},
"/api/admin/login": {
"post": {
- "description": "管理员登出",
+ "description": "管理员登录",
"consumes": [
- "application/json"
+ "multipart/form-data"
],
"produces": [
"application/json"
@@ -170,12 +170,28 @@
"tags": [
"API.admin"
],
- "summary": "管理员登出",
+ "summary": "管理员登录",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "用户名",
+ "name": "username",
+ "in": "formData",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "密码",
+ "name": "password",
+ "in": "formData",
+ "required": true
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/admin_handler.logoutResponse"
+ "$ref": "#/definitions/admin_handler.loginResponse"
}
},
"400": {
@@ -1085,6 +1101,42 @@
}
}
}
+ },
+ "patch": {
+ "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": {
@@ -1890,6 +1942,44 @@
}
}
}
+ },
+ "/api/tool/send_message": {
+ "post": {
+ "description": "发送消息",
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "API.tool"
+ ],
+ "summary": "发送消息",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "消息内容",
+ "name": "message",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/tool_handler.sendMessageResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/code.Failure"
+ }
+ }
+ }
+ }
}
},
"definitions": {
@@ -2777,6 +2867,15 @@
}
}
},
+ "tool_handler.sendMessageResponse": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "description": "状态",
+ "type": "string"
+ }
+ }
+ },
"tool_handler.tableColumn": {
"type": "object",
"properties": {
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index 47bbc4e..cbd4ea9 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -615,6 +615,12 @@ definitions:
type: object
type: array
type: object
+ tool_handler.sendMessageResponse:
+ properties:
+ status:
+ description: 状态
+ type: string
+ type: object
tool_handler.tableColumn:
properties:
column_comment:
@@ -776,20 +782,31 @@ paths:
/api/admin/login:
post:
consumes:
- - application/json
- description: 管理员登出
+ - multipart/form-data
+ description: 管理员登录
+ parameters:
+ - description: 用户名
+ in: formData
+ name: username
+ required: true
+ type: string
+ - description: 密码
+ in: formData
+ name: password
+ required: true
+ type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/admin_handler.logoutResponse'
+ $ref: '#/definitions/admin_handler.loginResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/code.Failure'
- summary: 管理员登出
+ summary: 管理员登录
tags:
- API.admin
/api/admin/menu:
@@ -1368,6 +1385,30 @@ paths:
summary: 获取单条任务详情
tags:
- API.cron
+ patch:
+ 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:
@@ -1904,4 +1945,29 @@ paths:
summary: HashIds 加密
tags:
- API.tool
+ /api/tool/send_message:
+ post:
+ consumes:
+ - multipart/form-data
+ description: 发送消息
+ parameters:
+ - description: 消息内容
+ in: formData
+ name: message
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/tool_handler.sendMessageResponse'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/code.Failure'
+ summary: 发送消息
+ tags:
+ - API.tool
swagger: "2.0"
diff --git a/go.mod b/go.mod
index 7c0ef67..fe54311 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
github.com/go-playground/validator/v10 v10.4.1
github.com/go-redis/redis/v7 v7.4.0
github.com/golang/protobuf v1.5.2
+ github.com/gorilla/websocket v1.4.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
diff --git a/go.sum b/go.sum
index 0a650d4..1845613 100644
--- a/go.sum
+++ b/go.sum
@@ -107,6 +107,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -146,6 +147,7 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
@@ -501,6 +503,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
+github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
@@ -511,6 +514,7 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX
github.com/shirou/gopsutil v3.21.2+incompatible h1:U+YvJfjCh6MslYlIAXvPtzhW3YZEtc9uncueUNpD/0A=
github.com/shirou/gopsutil v3.21.2+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -571,8 +575,10 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
github.com/ugorji/go/codec v1.1.13 h1:013LbFhocBoIqgHeIHKlV4JWYhqogATYWZhIcH0WHn4=
github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
+github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
diff --git a/internal/api/controller/tool_handler/func_sendmessage.go b/internal/api/controller/tool_handler/func_sendmessage.go
new file mode 100644
index 0000000..c41f5d7
--- /dev/null
+++ b/internal/api/controller/tool_handler/func_sendmessage.go
@@ -0,0 +1,90 @@
+package tool_handler
+
+import (
+ "encoding/json"
+ "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/internal/websocket/socket_conn/system_message"
+ "github.com/xinliangnote/go-gin-api/pkg/errno"
+ "github.com/xinliangnote/go-gin-api/pkg/time_parse"
+)
+
+type sendMessageRequest struct {
+ Message string `form:"message"` // 消息内容
+}
+
+type sendMessageResponse struct {
+ Status string `json:"status"` // 状态
+}
+
+// SendMessage 发送消息
+// @Summary 发送消息
+// @Description 发送消息
+// @Tags API.tool
+// @Accept multipart/form-data
+// @Produce json
+// @Param message formData string true "消息内容"
+// @Success 200 {object} sendMessageResponse
+// @Failure 400 {object} code.Failure
+// @Router /api/tool/send_message [post]
+func (h *handler) SendMessage() core.HandlerFunc {
+ type messageBody struct {
+ Username string `json:"username"`
+ Message string `json:"message"`
+ Time string `json:"time"`
+ }
+
+ return func(ctx core.Context) {
+ req := new(sendMessageRequest)
+ res := new(sendMessageResponse)
+ if err := ctx.ShouldBindForm(req); err != nil {
+ ctx.AbortWithError(errno.NewError(
+ http.StatusBadRequest,
+ code.ParamBindError,
+ validation.Error(err)).WithErr(err),
+ )
+ return
+ }
+
+ conn, err := system_message.GetConn()
+ if err != nil {
+ ctx.AbortWithError(errno.NewError(
+ http.StatusBadRequest,
+ code.SocketConnectError,
+ code.Text(code.SocketConnectError)).WithErr(err),
+ )
+ return
+ }
+
+ messageData := new(messageBody)
+ messageData.Username = ctx.UserName()
+ messageData.Message = req.Message
+ messageData.Time = time_parse.CSTLayoutString()
+
+ messageJsonData, err := json.Marshal(messageData)
+ if err != nil {
+ ctx.AbortWithError(errno.NewError(
+ http.StatusBadRequest,
+ code.SocketSendError,
+ code.Text(code.SocketSendError)).WithErr(err),
+ )
+ return
+ }
+
+ err = conn.OnSend(messageJsonData)
+ if err != nil {
+ ctx.AbortWithError(errno.NewError(
+ http.StatusBadRequest,
+ code.SocketSendError,
+ code.Text(code.SocketSendError)).WithErr(err),
+ )
+ return
+ }
+
+ res.Status = "OK"
+ ctx.Payload(res)
+ }
+}
diff --git a/internal/api/controller/tool_handler/handler.go b/internal/api/controller/tool_handler/handler.go
index 3df8087..5c66330 100644
--- a/internal/api/controller/tool_handler/handler.go
+++ b/internal/api/controller/tool_handler/handler.go
@@ -49,6 +49,11 @@ type Handler interface {
// @Tags API.tool
// @Router /api/tool/data/mysql [post]
SearchMySQL() core.HandlerFunc
+
+ // SendMessage 发送消息
+ // @Tags API.tool
+ // @Router /api/tool/send_message [post]
+ SendMessage() core.HandlerFunc
}
type handler struct {
diff --git a/internal/pkg/code/code.go b/internal/pkg/code/code.go
index e20f055..27b847f 100644
--- a/internal/pkg/code/code.go
+++ b/internal/pkg/code/code.go
@@ -28,6 +28,8 @@ const (
SendEmailError = 10117
MySQLExecError = 10118
GoVersionError = 10119
+ SocketConnectError = 10120
+ SocketSendError = 10121
AuthorizedCreateError = 20101
AuthorizedListError = 20102
diff --git a/internal/pkg/code/en-us.go b/internal/pkg/code/en-us.go
index 4f7f6f6..b1b50e4 100644
--- a/internal/pkg/code/en-us.go
+++ b/internal/pkg/code/en-us.go
@@ -20,6 +20,8 @@ var enUSText = map[int]string{
SendEmailError: "Failed to send mail",
MySQLExecError: "SQL execution failed",
GoVersionError: "Go Version mismatch",
+ SocketConnectError: "Socket not connected",
+ SocketSendError: "Socket message sending failed",
AuthorizedCreateError: "Failed to create caller",
AuthorizedListError: "Failed to get caller list",
diff --git a/internal/pkg/code/zh-cn.go b/internal/pkg/code/zh-cn.go
index a937a8c..837f37a 100644
--- a/internal/pkg/code/zh-cn.go
+++ b/internal/pkg/code/zh-cn.go
@@ -20,6 +20,8 @@ var zhCNText = map[int]string{
SendEmailError: "发送邮件失败",
MySQLExecError: "SQL 执行失败",
GoVersionError: "Go 版本不满足要求",
+ SocketConnectError: "Socket 未连接",
+ SocketSendError: "Socket 消息发送失败",
AuthorizedCreateError: "创建调用方失败",
AuthorizedListError: "获取调用方列表失败",
diff --git a/internal/router/router.go b/internal/router/router.go
index cfb9ba8..0f0a9a4 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -103,6 +103,9 @@ func NewHTTPServer(logger *zap.Logger, cronLogger *zap.Logger) (*Server, error)
// 设置 GraphQL 路由
setGraphQLRouter(r)
+ // 设置 Socket 路由
+ setSocketRouter(r)
+
s := new(Server)
s.Mux = mux
s.Db = r.db
diff --git a/internal/router/router_api.go b/internal/router/router_api.go
index 15b31f3..e645e5b 100644
--- a/internal/router/router_api.go
+++ b/internal/router/router_api.go
@@ -75,6 +75,7 @@ func setApiRouter(r *resource) {
api.GET("/tool/data/dbs", toolHandler.Dbs())
api.POST("/tool/data/tables", toolHandler.Tables())
api.POST("/tool/data/mysql", toolHandler.SearchMySQL())
+ api.POST("/tool/send_message", toolHandler.SendMessage())
// config
configHandler := config_handler.New(r.logger, r.db, r.cache)
@@ -84,10 +85,10 @@ func setApiRouter(r *resource) {
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.GET("/cron/:id", core.AliasForRecordMetrics("/api/cron/detail"), cronHandler.Detail())
+ api.POST("/cron/:id", core.AliasForRecordMetrics("/api/cron/modify"), cronHandler.Modify())
api.PATCH("/cron/used", cronHandler.UpdateUsed())
- api.PATCH("/cron/exec/:id", cronHandler.Execute())
+ api.PATCH("/cron/exec/:id", core.AliasForRecordMetrics("/api/cron/exec"), cronHandler.Execute())
}
}
diff --git a/internal/router/router_socket.go b/internal/router/router_socket.go
new file mode 100644
index 0000000..ed3d203
--- /dev/null
+++ b/internal/router/router_socket.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "github.com/xinliangnote/go-gin-api/internal/websocket/socket_conn/system_message"
+)
+
+func setSocketRouter(r *resource) {
+ systemMessage := system_message.New(r.logger, r.db, r.cache)
+
+ // 无需记录日志
+ socket := r.mux.Group("/socket", r.middles.DisableLog())
+ {
+ // 系统消息
+ socket.GET("/system/message", systemMessage.Connect())
+ }
+}
diff --git a/internal/router/router_web.go b/internal/router/router_web.go
index 0722d31..07fa8ea 100644
--- a/internal/router/router_web.go
+++ b/internal/router/router_web.go
@@ -81,6 +81,7 @@ func setWebRouter(r *resource) {
web.GET("/tool/logs", toolHandler.LogsView())
web.GET("/tool/cache", toolHandler.CacheView())
web.GET("/tool/data", toolHandler.DataView())
+ web.GET("/tool/websocket", toolHandler.WebsocketView())
// 后台任务
web.GET("/cron/list", cronTaskHandler.ListView())
diff --git a/internal/web/controller/install_handler/mysql_table/table_admin_menu.go b/internal/web/controller/install_handler/mysql_table/table_admin_menu.go
index 5bdc6ab..1a00ced 100644
--- a/internal/web/controller/install_handler/mysql_table/table_admin_menu.go
+++ b/internal/web/controller/install_handler/mysql_table/table_admin_menu.go
@@ -49,7 +49,8 @@ func CreateAdminMenuTableDataSql() (sql string) {
sql += "(21, 1, 2, 'init'),"
sql += "(22, 1, 22, 'init'),"
sql += "(23, 1, 23, 'init'),"
- sql += "(24, 1, 24, 'init');"
+ sql += "(24, 1, 24, 'init'),"
+ sql += "(25, 1, 25, 'init');"
return
}
diff --git a/internal/web/controller/install_handler/mysql_table/table_menu.go b/internal/web/controller/install_handler/mysql_table/table_menu.go
index 132c8a5..de66054 100644
--- a/internal/web/controller/install_handler/mysql_table/table_menu.go
+++ b/internal/web/controller/install_handler/mysql_table/table_menu.go
@@ -63,7 +63,8 @@ func CreateMenuTableDataSql() (sql string) {
sql += "(21, 16, '接口指标', '/metrics', '', 2, 706, 'init'),"
sql += "(22, 16, '服务升级', '/upgrade', '', 2, 701, 'init'),"
sql += "(23, 0, '后台任务', '', 'mdi-av-timer', 1, 40, 'init'),"
- sql += "(24, 23, '任务列表', '/cron/list', '', 2, 401, 'init');"
+ sql += "(24, 23, '任务列表', '/cron/list', '', 2, 401, 'init'),"
+ sql += "(25, 16, 'WebSocket', '/tool/websocket', '', 2, 707, 'init');"
return
}
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 6775274..a9d98d3 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
@@ -80,7 +80,8 @@ func CreateMenuActionTableDataSql() (sql string) {
sql += "(44, 24, 'GET', '/api/cron', 'init'),"
sql += "(45, 24, 'GET', '/api/cron/*', 'init'),"
sql += "(46, 24, 'PATCH', '/api/cron/used', 'init'),"
- sql += "(47, 24, 'PATCH', '/api/cron/exec/*', 'init');"
+ sql += "(47, 24, 'PATCH', '/api/cron/exec/*', 'init'),"
+ sql += "(48, 25, 'POST', '/api/tool/send_message', 'init');"
return
}
diff --git a/internal/web/controller/tool_handler/func_websocketview.go b/internal/web/controller/tool_handler/func_websocketview.go
new file mode 100644
index 0000000..d4878c0
--- /dev/null
+++ b/internal/web/controller/tool_handler/func_websocketview.go
@@ -0,0 +1,11 @@
+package tool_handler
+
+import (
+ "github.com/xinliangnote/go-gin-api/internal/pkg/core"
+)
+
+func (h *handler) WebsocketView() core.HandlerFunc {
+ return func(c core.Context) {
+ c.HTML("tool_websocket", nil)
+ }
+}
diff --git a/internal/web/controller/tool_handler/handler.go b/internal/web/controller/tool_handler/handler.go
index bd3ca06..33f34ec 100644
--- a/internal/web/controller/tool_handler/handler.go
+++ b/internal/web/controller/tool_handler/handler.go
@@ -17,6 +17,7 @@ type Handler interface {
LogsView() core.HandlerFunc
CacheView() core.HandlerFunc
DataView() core.HandlerFunc
+ WebsocketView() core.HandlerFunc
}
type handler struct {
diff --git a/internal/websocket/socket_conn/system_message/func_connect.go b/internal/websocket/socket_conn/system_message/func_connect.go
new file mode 100755
index 0000000..bbd7e3c
--- /dev/null
+++ b/internal/websocket/socket_conn/system_message/func_connect.go
@@ -0,0 +1,34 @@
+package system_message
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/xinliangnote/go-gin-api/internal/pkg/core"
+ "github.com/xinliangnote/go-gin-api/internal/websocket/socket_server"
+
+ "github.com/gorilla/websocket"
+)
+
+func (h *handler) Connect() core.HandlerFunc {
+ var upGrader = websocket.Upgrader{
+ HandshakeTimeout: 5 * time.Second,
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+ }
+
+ return func(ctx core.Context) {
+ ws, err := upGrader.Upgrade(ctx.ResponseWriter(), ctx.Request(), nil)
+ if err != nil {
+ return
+ }
+
+ server, err = socket_server.New(h.logger, h.db, h.cache, ws)
+ if err != nil {
+ return
+ }
+
+ go server.OnMessage()
+ }
+}
diff --git a/internal/websocket/socket_conn/system_message/handler.go b/internal/websocket/socket_conn/system_message/handler.go
new file mode 100644
index 0000000..0e9ceda
--- /dev/null
+++ b/internal/websocket/socket_conn/system_message/handler.go
@@ -0,0 +1,46 @@
+package system_message
+
+import (
+ "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"
+ "github.com/xinliangnote/go-gin-api/internal/websocket/socket_server"
+ "github.com/xinliangnote/go-gin-api/pkg/errors"
+
+ "go.uber.org/zap"
+)
+
+var _ Handler = (*handler)(nil)
+
+var server socket_server.Server
+
+type Handler interface {
+ i()
+
+ // Connect 建立 Socket 连接
+ Connect() core.HandlerFunc
+}
+
+type handler struct {
+ logger *zap.Logger
+ cache cache.Repo
+ db db.Repo
+}
+
+func New(logger *zap.Logger, db db.Repo, cache cache.Repo) Handler {
+ return &handler{
+ logger: logger,
+ cache: cache,
+ db: db,
+ }
+}
+
+func GetConn() (socket_server.Server, error) {
+ if server != nil {
+ return server, nil
+ }
+
+ return nil, errors.New("conn is nil")
+}
+
+func (h *handler) i() {}
diff --git a/internal/websocket/socket_server/server.go b/internal/websocket/socket_server/server.go
new file mode 100644
index 0000000..cf4954a
--- /dev/null
+++ b/internal/websocket/socket_server/server.go
@@ -0,0 +1,59 @@
+package socket_server
+
+import (
+ "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/gorilla/websocket"
+ "go.uber.org/zap"
+)
+
+var _ Server = (*server)(nil)
+
+type server struct {
+ logger *zap.Logger
+ db db.Repo
+ cache cache.Repo
+ socket *websocket.Conn
+}
+
+type Server interface {
+ i()
+
+ // OnMessage 接收消息
+ OnMessage()
+
+ // OnSend 发送消息
+ OnSend(message []byte) error
+
+ // OnClose 关闭
+ OnClose()
+}
+
+func New(logger *zap.Logger, db db.Repo, cache cache.Repo, conn *websocket.Conn) (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")
+ }
+
+ if conn == nil {
+ return nil, errors.New("conn required")
+ }
+
+ return &server{
+ logger: logger,
+ db: db,
+ cache: cache,
+ socket: conn,
+ }, nil
+}
+
+func (s *server) i() {}
diff --git a/internal/websocket/socket_server/server_on_close.go b/internal/websocket/socket_server/server_on_close.go
new file mode 100644
index 0000000..a70c555
--- /dev/null
+++ b/internal/websocket/socket_server/server_on_close.go
@@ -0,0 +1,10 @@
+package socket_server
+
+import "go.uber.org/zap"
+
+func (s *server) OnClose() {
+ err := s.socket.Close()
+ if err != nil {
+ s.logger.Error("socket on closed error", zap.Error(err))
+ }
+}
diff --git a/internal/websocket/socket_server/server_on_message.go b/internal/websocket/socket_server/server_on_message.go
new file mode 100644
index 0000000..f33c60c
--- /dev/null
+++ b/internal/websocket/socket_server/server_on_message.go
@@ -0,0 +1,21 @@
+package socket_server
+
+import "go.uber.org/zap"
+
+func (s *server) OnMessage() {
+ defer func() {
+ s.OnClose()
+ }()
+
+ for {
+ //接收消息
+ _, message, err := s.socket.ReadMessage()
+ if err != nil {
+ s.logger.Error("socket on message error", zap.Error(err))
+ break
+ }
+
+ // 为了便于演示,仅输出到日志文件
+ s.logger.Info("receive message: " + string(message))
+ }
+}
diff --git a/internal/websocket/socket_server/server_on_send.go b/internal/websocket/socket_server/server_on_send.go
new file mode 100644
index 0000000..d77bc1f
--- /dev/null
+++ b/internal/websocket/socket_server/server_on_send.go
@@ -0,0 +1,15 @@
+package socket_server
+
+import (
+ "github.com/gorilla/websocket"
+ "go.uber.org/zap"
+)
+
+func (s *server) OnSend(message []byte) error {
+ err := s.socket.WriteMessage(websocket.TextMessage, message)
+ if err != nil {
+ s.OnClose()
+ s.logger.Error("socket on send error", zap.Error(err))
+ }
+ return err
+}