feature(1.2.8): 新增 websocket 模块

- import gorilla/websocket
- 新增 实用工具箱->WebSocket 栏目
This commit is contained in:
新亮
2021-09-20 19:18:15 +08:00
parent 77e8d58298
commit 17be120c06
31 changed files with 848 additions and 19 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,32 @@
document.write('<script type="text/javascript" src="../../bootstrap/js/bootstrap-notify/bootstrap-notify.min.js"></script>');
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
});
}
@@ -98,6 +98,9 @@
</div>
<input type="text" class="form-control" id="mysql_name" value="{{ .Config.MySQL.Write.Name }}"
placeholder="请输入数据库名">
<div class="input-group-append">
<span class="input-group-text"><code>请确保此数据库已存在!</code></span>
</div>
</div>
<div class="input-group mb-3">
@@ -95,6 +95,21 @@
<p>5、退出重新登录,即可以看到自己新增的模块。</p>
<hr/>
<p class="h6">
<span class="badge badge-pill badge-warning">v1.2.7</span>
->
<span class="badge badge-pill badge-warning">v1.2.8</span>
</p>
<p>1、源代码升级:
<mark>拉取最新代码,覆盖旧版本代码即可。</mark>
</p>
<p>2、系统管理员->菜单管理,新增侧边栏:实用工具箱 -> WebSocket,同时添加菜单栏的功能权限。</p>
<p>3、系统管理员->管理员,对管理员进行菜单授权。</p>
<p>4、退出重新登录,即可以看到自己新增的模块。</p>
<hr/>
</div>
</div>
</div>
+184
View File
@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
<link href="../../bootstrap/js/jquery-confirm/jquery-confirm.min.css" rel="stylesheet">
<link href="../../bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="../../bootstrap/css/materialdesignicons.min.css" rel="stylesheet">
<link href="../../bootstrap/css/animate.min.css" rel="stylesheet">
<link href="../../bootstrap/css/style.min.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid p-t-15">
<div class="row">
<div class="col-lg-6">
<div class="card">
<header class="card-header">
<div class="card-title">WebSocket 示例</div>
</header>
<div class="card-body">
<ul class="lyear-timeline lyear-timeline-left timeline-content">
<li class="lyear-timeline-item">
<div class="lyear-timeline-item-dot">
<span class="badge"></span>
</div>
<div class="lyear-timeline-item-content">
<p class="mb-1"><strong class="connect-content"></strong></p>
<p class="mb-0 connect-url"></p>
<p>
<time class="mb-3 connect-time"></time>
</p>
</div>
</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<div class="card-title">WebSocket 示例</div>
</div>
<div class="card-body">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#send_message"
aria-selected="true">发送消息</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade active show" id="send_message">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">消息内容</span>
</div>
<input type="text" class="form-control" id="message_content" placeholder="请输入消息内容">
</div>
<button type="button" id="btnSend" class="btn btn-primary">发送</button>
<button type="button" id="btnSending" class="btn btn-primary" disabled
style="display: none">
<span class="spinner-grow spinner-grow-sm" role="status" aria-hidden="true"></span>
发送中...
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="../../bootstrap/js/jquery.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/popper.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/jquery-confirm/jquery-confirm.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/httpclient/httpclient.js"></script>
<script type="text/javascript" src="../../bootstrap/js/bootstrap-notify/notify.js"></script>
<script type="text/javascript">
$(document).ready(function () {
const ws = new WebSocket("ws://127.0.0.1:9999/socket/system/message");
//连接打开时触发
ws.onopen = function (evt) {
if (evt.isTrusted) {
$(".badge").addClass("badge-success");
$(".connect-content").html("Socket 连接成功");
$(".connect-url").html("连接地址:" + evt.currentTarget.url);
$(".connect-time").html(getCSTTime());
} else {
$(".badge").addClass("badge-danger");
$(".connect-content").html("Socket 连接失败");
$(".connect-url").html("请确认连接地址是否正确");
$(".connect-time").html(getCSTTime());
}
};
//接收到消息时触发
ws.onmessage = function (evt) {
const messageData = JSON.parse(evt.data);
let html = '<li class="lyear-timeline-item">';
html += '<div class="lyear-timeline-item-dot"><span class="badge badge-success"></span></div>';
html += '<div class="lyear-timeline-item-content">';
html += '<p class="mb-1"><strong>' + messageData.username + '</strong></p>';
html += '<p class="mb-0">' + messageData.message + '</p>';
html += '<p><time class="mb-3">' + messageData.time + '</time></p>';
html += '</div></li>';
$(".timeline-content").prepend(html);
};
//连接关闭时触发
ws.onclose = function (evt) {
let html = '<li class="lyear-timeline-item">';
html += '<div class="lyear-timeline-item-dot"><span class="badge badge-muted"></span></div>';
html += '<div class="lyear-timeline-item-content">';
html += '<p class="mb-1"><strong>Socket 连接关闭</strong></p>';
html += '<p class="mb-0">连接已关闭</p>';
html += '<p><time class="mb-3">' + getCSTTime() + '</time></p>';
html += '</div></li>';
$(".timeline-content").prepend(html);
};
function getCSTTime() {
let date = new Date();
let datetime = date.getFullYear() + "-" // "年"
+ ((date.getMonth() + 1) > 10 ? (date.getMonth() + 1) : "0" + (date.getMonth() + 1)) + "-" // "月"
+ (date.getDate() < 10 ? "0" + date.getDate() : date.getDate()) + " " // "日"
+ (date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ":" // "小时"
+ (date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes()) + ":" // "分钟"
+ (date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds()); // "秒"
return datetime;
}
$('#btnSend').on('click', function () {
const messageContent = $("#message_content").val();
if (messageContent === "") {
$.alert({
title: '温馨提示',
icon: 'mdi mdi-alert',
type: 'orange',
content: '请输入消息内容。',
});
return false;
}
AjaxForm(
"POST",
"/api/tool/send_message",
{message: messageContent},
function () {
$(this).hide();
$("#btnSendLoading").show();
},
function (data) {
$("#btnSendLoading").hide();
$("#btnSend").show();
if (data.status === 'OK') {
$("#message_content").val("");
SuccessNotify("消息发送成功");
}
},
function (response) {
$("#btnSendLoading").hide();
$("#btnSend").show();
AjaxError(response);
}
);
});
})
</script>
</body>
</html>
+1 -1
View File
@@ -2,7 +2,7 @@ package configs
const (
// ProjectVersion 项目版本
ProjectVersion = "v1.2.7"
ProjectVersion = "v1.2.8"
// ProjectName 项目名称
ProjectName = "go-gin-api"
+103 -4
View File
@@ -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": {
+103 -4
View File
@@ -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": {
+70 -4
View File
@@ -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"
+1
View File
@@ -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
+6
View File
@@ -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=
@@ -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)
}
}
@@ -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 {
+2
View File
@@ -28,6 +28,8 @@ const (
SendEmailError = 10117
MySQLExecError = 10118
GoVersionError = 10119
SocketConnectError = 10120
SocketSendError = 10121
AuthorizedCreateError = 20101
AuthorizedListError = 20102
+2
View File
@@ -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",
+2
View File
@@ -20,6 +20,8 @@ var zhCNText = map[int]string{
SendEmailError: "发送邮件失败",
MySQLExecError: "SQL 执行失败",
GoVersionError: "Go 版本不满足要求",
SocketConnectError: "Socket 未连接",
SocketSendError: "Socket 消息发送失败",
AuthorizedCreateError: "创建调用方失败",
AuthorizedListError: "获取调用方列表失败",
+3
View File
@@ -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
+4 -3
View File
@@ -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())
}
}
+16
View File
@@ -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())
}
}
+1
View File
@@ -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())
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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)
}
}
@@ -17,6 +17,7 @@ type Handler interface {
LogsView() core.HandlerFunc
CacheView() core.HandlerFunc
DataView() core.HandlerFunc
WebsocketView() core.HandlerFunc
}
type handler struct {
@@ -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()
}
}
@@ -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() {}
@@ -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() {}
@@ -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))
}
}
@@ -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))
}
}
@@ -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
}