From 17be120c06fe92fdaa49182e3445708ef94fdb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B0=E4=BA=AE?= Date: Mon, 20 Sep 2021 19:18:15 +0800 Subject: [PATCH] =?UTF-8?q?feature(1.2.8):=20=E6=96=B0=E5=A2=9E=20websocke?= =?UTF-8?q?t=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - import gorilla/websocket - 新增 实用工具箱->WebSocket 栏目 --- .../bootstrap-notify/bootstrap-notify.min.js | 2 + .../bootstrap/js/bootstrap-notify/notify.js | 32 +++ assets/templates/install/install_view.html | 3 + assets/templates/install/upgrade_view.html | 15 ++ assets/templates/tool/tool_websocket.html | 184 ++++++++++++++++++ configs/constants.go | 2 +- docs/docs.go | 107 +++++++++- docs/swagger.json | 107 +++++++++- docs/swagger.yaml | 74 ++++++- go.mod | 1 + go.sum | 6 + .../tool_handler/func_sendmessage.go | 90 +++++++++ .../api/controller/tool_handler/handler.go | 5 + internal/pkg/code/code.go | 2 + internal/pkg/code/en-us.go | 2 + internal/pkg/code/zh-cn.go | 2 + internal/router/router.go | 3 + internal/router/router_api.go | 7 +- internal/router/router_socket.go | 16 ++ internal/router/router_web.go | 1 + .../mysql_table/table_admin_menu.go | 3 +- .../install_handler/mysql_table/table_menu.go | 3 +- .../mysql_table/table_menu_action.go | 3 +- .../tool_handler/func_websocketview.go | 11 ++ .../web/controller/tool_handler/handler.go | 1 + .../system_message/func_connect.go | 34 ++++ .../socket_conn/system_message/handler.go | 46 +++++ internal/websocket/socket_server/server.go | 59 ++++++ .../socket_server/server_on_close.go | 10 + .../socket_server/server_on_message.go | 21 ++ .../websocket/socket_server/server_on_send.go | 15 ++ 31 files changed, 848 insertions(+), 19 deletions(-) create mode 100644 assets/bootstrap/js/bootstrap-notify/bootstrap-notify.min.js create mode 100644 assets/bootstrap/js/bootstrap-notify/notify.js create mode 100644 assets/templates/tool/tool_websocket.html create mode 100644 internal/api/controller/tool_handler/func_sendmessage.go create mode 100644 internal/router/router_socket.go create mode 100644 internal/web/controller/tool_handler/func_websocketview.go create mode 100755 internal/websocket/socket_conn/system_message/func_connect.go create mode 100644 internal/websocket/socket_conn/system_message/handler.go create mode 100644 internal/websocket/socket_server/server.go create mode 100644 internal/websocket/socket_server/server_on_close.go create mode 100644 internal/websocket/socket_server/server_on_message.go create mode 100644 internal/websocket/socket_server/server_on_send.go 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('Notify Icon')},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/install/upgrade_view.html b/assets/templates/install/upgrade_view.html index fb52f11..52ede79 100644 --- a/assets/templates/install/upgrade_view.html +++ b/assets/templates/install/upgrade_view.html @@ -95,6 +95,21 @@

5、退出重新登录,即可以看到自己新增的模块。


+ +

+ v1.2.7 + -> + v1.2.8 +

+ +

1、源代码升级: + 拉取最新代码,覆盖旧版本代码即可。 +

+

2、系统管理员->菜单管理,新增侧边栏:实用工具箱 -> WebSocket,同时添加菜单栏的功能权限。

+

3、系统管理员->管理员,对管理员进行菜单授权。

+

4、退出重新登录,即可以看到自己新增的模块。

+ +
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 @@ + + + + + + + + + + + + + +
+
+
+
+
+
WebSocket 示例
+
+
+ +
    +
  • +
    + +
    +
    +

    +

    +

    + +

    +
    +
  • +
+ +
+
+
+ +
+
+
+
WebSocket 示例
+
+
+ + +
+
+
+
+ 消息内容 +
+ +
+ + + +
+
+
+
+
+
+
+ + + + + + + + + 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 +}