├── web └── vue-admin │ ├── .eslintignore │ ├── tests │ └── unit │ │ ├── .eslintrc.js │ │ ├── utils │ │ ├── param2Obj.spec.js │ │ ├── validate.spec.js │ │ ├── formatTime.spec.js │ │ └── parseTime.spec.js │ │ └── components │ │ ├── SvgIcon.spec.js │ │ ├── Hamburger.spec.js │ │ └── Breadcrumb.spec.js │ ├── .env.development │ ├── public │ ├── favicon.ico │ └── index.html │ ├── .env.production │ ├── .travis.yml │ ├── src │ ├── assets │ │ └── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ ├── views │ │ ├── nested │ │ │ ├── menu2 │ │ │ │ └── index.vue │ │ │ └── menu1 │ │ │ │ ├── menu1-3 │ │ │ │ └── index.vue │ │ │ │ ├── index.vue │ │ │ │ ├── menu1-2 │ │ │ │ ├── menu1-2-1 │ │ │ │ │ └── index.vue │ │ │ │ ├── menu1-2-2 │ │ │ │ │ └── index.vue │ │ │ │ └── index.vue │ │ │ │ └── menu1-1 │ │ │ │ └── index.vue │ │ ├── API.md │ │ ├── tree │ │ │ └── index.vue │ │ ├── table │ │ │ └── index.vue │ │ └── form │ │ │ └── index.vue │ ├── layout │ │ ├── components │ │ │ ├── index.js │ │ │ ├── Sidebar │ │ │ │ ├── FixiOSBug.js │ │ │ │ ├── Link.vue │ │ │ │ ├── Item.vue │ │ │ │ ├── index.vue │ │ │ │ ├── Logo.vue │ │ │ │ └── SidebarItem.vue │ │ │ ├── AppMain.vue │ │ │ └── Navbar.vue │ │ ├── mixin │ │ │ └── ResizeHandler.js │ │ └── index.vue │ ├── App.vue │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── settings.js │ │ │ └── app.js │ ├── utils │ │ ├── get-page-title.js │ │ ├── auth.js │ │ ├── validate.js │ │ ├── request.js │ │ ├── scroll-to.js │ │ └── index.js │ ├── icons │ │ ├── index.js │ │ ├── svgo.yml │ │ └── svg │ │ │ ├── user.svg │ │ │ ├── table.svg │ │ │ └── form.svg │ ├── settings.js │ ├── styles │ │ ├── mixin.scss │ │ ├── variables.scss │ │ ├── element-ui.scss │ │ ├── transition.scss │ │ ├── index.scss │ │ └── sidebar.scss │ ├── api │ │ ├── consume.js │ │ ├── consumeWorkServerMap.js │ │ └── workServer.js │ ├── main.js │ ├── components │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ └── Pagination │ │ │ └── index.vue │ ├── router │ │ └── index.js │ └── permission.js │ ├── .env.staging │ ├── jsconfig.json │ ├── postcss.config.js │ ├── .gitignore │ ├── .editorconfig │ ├── babel.config.js │ ├── jest.config.js │ ├── build │ └── index.js │ ├── LICENSE │ ├── package.json │ ├── vue.config.js │ └── .eslintrc.js ├── document ├── doc │ ├── README.md │ ├── make.md │ ├── flag.md │ └── quick_start.md ├── README.md ├── protocol │ ├── README.md │ ├── fastcgi.md │ ├── http.md │ └── cbnsq.md └── api │ ├── debug │ └── pprof.md │ ├── service │ ├── status.md │ └── getrole.md │ ├── admin │ └── api │ │ ├── workserver │ │ ├── delete.md │ │ ├── get.md │ │ ├── update.md │ │ ├── all.md │ │ ├── create.md │ │ └── page.md │ │ ├── consumeconfig │ │ ├── delete.md │ │ ├── get.md │ │ ├── update.md │ │ ├── create.md │ │ ├── page.md │ │ └── worklist.md │ │ └── consumeservermap │ │ ├── delete.md │ │ ├── update.md │ │ ├── create.md │ │ └── getWork.md │ └── README.md ├── assets └── images │ ├── admin_work_server.png │ ├── quick_start_demo.png │ ├── admin_consume_config.png │ ├── nsqproxy_flow_chart.png │ ├── quick_start_add_work_server.png │ ├── quick_start_add_consume_config.png │ └── quick_start_add_consume_server_map.png ├── go.mod ├── internal ├── module │ ├── tool │ │ ├── error.go │ │ ├── tool.go │ │ ├── httpsyncpool.go │ │ └── guid.go │ └── logger │ │ ├── logger.go │ │ └── log.go ├── proxy │ ├── handle_test.go │ ├── proxy_test.go │ ├── loadbalance_test.go │ ├── loadbalance.go │ └── handle.go ├── worker │ ├── cbnsq_test.go │ ├── http_test.go │ ├── fastcgi_test.go │ ├── fastcgi.go │ ├── http.go │ ├── error.go │ ├── worker.go │ ├── worker_test.go │ └── cbnsq.go ├── httper │ ├── http.go │ ├── http_test.go │ ├── response.go │ ├── router.go │ ├── workserver.go │ └── consumeservermap.go ├── model │ ├── common_test.go │ ├── consumeservermap.go │ ├── consumeservermap_test.go │ ├── workserver_test.go │ ├── consumeconfig_test.go │ ├── common.go │ └── workserver.go └── backup │ └── live.go ├── .gitignore ├── deployments └── supervisor │ └── nsqproxy.conf ├── LICENSE ├── go.sum ├── Makefile ├── cmd └── nsqproxy.go ├── README.md └── config └── systemconfig.go /web/vue-admin/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /document/doc/README.md: -------------------------------------------------------------------------------- 1 | # 文档 2 | 3 | * [快速体验](quick_start.md) 4 | * [启动参数](flag.md) 5 | * [make命令](make.md) -------------------------------------------------------------------------------- /web/vue-admin/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/vue-admin/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '' 6 | -------------------------------------------------------------------------------- /web/vue-admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/web/vue-admin/public/favicon.ico -------------------------------------------------------------------------------- /assets/images/admin_work_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/assets/images/admin_work_server.png -------------------------------------------------------------------------------- /assets/images/quick_start_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/assets/images/quick_start_demo.png -------------------------------------------------------------------------------- /web/vue-admin/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '' 6 | 7 | -------------------------------------------------------------------------------- /assets/images/admin_consume_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/assets/images/admin_consume_config.png -------------------------------------------------------------------------------- /assets/images/nsqproxy_flow_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/assets/images/nsqproxy_flow_chart.png -------------------------------------------------------------------------------- /web/vue-admin/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /web/vue-admin/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/web/vue-admin/src/assets/404_images/404.png -------------------------------------------------------------------------------- /assets/images/quick_start_add_work_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/assets/images/quick_start_add_work_server.png -------------------------------------------------------------------------------- /assets/images/quick_start_add_consume_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/assets/images/quick_start_add_consume_config.png -------------------------------------------------------------------------------- /document/README.md: -------------------------------------------------------------------------------- 1 | # 文档 2 | 3 | NSQProxy推送给Worker时可用协议 4 | 5 | * [文档](doc/README.md) 6 | * [API文档](api/README.md) 7 | * [协议文档](protocol/README.md) -------------------------------------------------------------------------------- /web/vue-admin/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/web/vue-admin/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /assets/images/quick_start_add_consume_server_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/changba/nsqproxy/HEAD/assets/images/quick_start_add_consume_server_map.png -------------------------------------------------------------------------------- /web/vue-admin/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /document/protocol/README.md: -------------------------------------------------------------------------------- 1 | # 协议 2 | 3 | NSQProxy推送给Worker时可用协议 4 | 5 | * HTTP:[HTTP](http.md) 6 | * FastCGI:[FastCGI](fastcgi.md) 7 | * CBNSQ:[CBNSQ](cbnsq.md) -------------------------------------------------------------------------------- /web/vue-admin/src/views/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /web/vue-admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /web/vue-admin/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /document/api/debug/pprof.md: -------------------------------------------------------------------------------- 1 | ### pprof 2 | 3 | - pprof 4 | 5 | > net.http包原生的的debug/pprof功能 6 | 7 | ``` 8 | GET /debug/pprof/ 9 | GET /debug/pprof/cmdline 10 | GET /debug/pprof/profile 11 | GET /debug/pprof/symbol 12 | GET /debug/pprof/trace 13 | ``` -------------------------------------------------------------------------------- /web/vue-admin/postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /document/api/service/status.md: -------------------------------------------------------------------------------- 1 | ### 查看状态 2 | 3 | - 接口功能 4 | 5 | > 查看服务当前是否可用 6 | 7 | ``` 8 | GET /status 9 | ``` 10 | 11 | - 请求参数 12 | 13 | > 无 14 | 15 | - 返回结果 16 | 17 | ``` 18 | { 19 | code: 200, 20 | msg: "ok", 21 | result: "ok" 22 | } 23 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/changba/nsqproxy 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.5.0 7 | github.com/nsqio/go-nsq v1.0.8 8 | github.com/rakyll/statik v0.1.7 9 | gorm.io/driver/mysql v1.0.1 10 | gorm.io/gorm v1.20.1 11 | ) 12 | -------------------------------------------------------------------------------- /web/vue-admin/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name 7 | } 8 | export default getters 9 | -------------------------------------------------------------------------------- /web/vue-admin/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /web/vue-admin/src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /document/api/service/getrole.md: -------------------------------------------------------------------------------- 1 | ### 查看角色 2 | 3 | - 接口功能 4 | 5 | > 查看当前是主服务还是备服务 6 | 7 | ``` 8 | GET /getRole 9 | ``` 10 | 11 | - 请求参数 12 | 13 | > 无 14 | 15 | - 返回结果 16 | 17 | > 结果有master或slave 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: "master" 24 | } 25 | ``` -------------------------------------------------------------------------------- /internal/module/tool/error.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "github.com/changba/nsqproxy/internal/module/logger" 5 | "runtime/debug" 6 | ) 7 | 8 | func PanicHandlerForLog() { 9 | if err := recover(); err != nil { 10 | logger.Errorf("recover panic: %v\r\n========\r\n%s", err, string(debug.Stack())) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/vue-admin/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /web/vue-admin/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /web/vue-admin/src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'nsqproxy管理后台', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: false, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether show the logo in sidebar 14 | */ 15 | sidebarLogo: false 16 | } 17 | -------------------------------------------------------------------------------- /document/api/admin/api/workserver/delete.md: -------------------------------------------------------------------------------- 1 | ### 删除一个服务器 2 | 3 | - 接口功能 4 | 5 | > 根据主键ID删除一个服务器 6 | 7 | ``` 8 | GET /admin/api/workServer/delete 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |服务器ID | 16 | 17 | - 返回结果 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: "ok" 24 | } 25 | ``` -------------------------------------------------------------------------------- /internal/proxy/handle_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/changba/nsqproxy/internal/model" 5 | "testing" 6 | ) 7 | 8 | func init() { 9 | model.NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 10 | } 11 | 12 | func TestNewHandler(t *testing.T) { 13 | p := NewProxy() 14 | consumeConfig := p.consumeConfigList[0] 15 | NewHandler(consumeConfig) 16 | } 17 | -------------------------------------------------------------------------------- /web/vue-admin/src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /web/vue-admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import settings from './modules/settings' 6 | 7 | Vue.use(Vuex) 8 | 9 | const store = new Vuex.Store({ 10 | modules: { 11 | app, 12 | settings 13 | }, 14 | getters 15 | }) 16 | 17 | export default store 18 | -------------------------------------------------------------------------------- /web/vue-admin/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'vue_admin_template_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /document/api/admin/api/consumeconfig/delete.md: -------------------------------------------------------------------------------- 1 | ### 删除一个消费者 2 | 3 | - 接口功能 4 | 5 | > 根据主键ID删除一个消费者 6 | 7 | ``` 8 | GET /admin/api/consumeConfig/delete 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |消费者ID | 16 | 17 | - 返回结果 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: "ok" 24 | } 25 | ``` -------------------------------------------------------------------------------- /document/api/admin/api/consumeservermap/delete.md: -------------------------------------------------------------------------------- 1 | ### 删除消费者和服务器的关联关系 2 | 3 | - 接口功能 4 | 5 | > 根据主键ID删除一个消费者和服务器的关联关系 6 | 7 | ``` 8 | GET /admin/api/consumeServerMap/delete 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |关联关系ID | 16 | 17 | - 返回结果 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: "ok" 24 | } 25 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | nsqproxy 4 | pprof/ 5 | logs/ 6 | test.go 7 | bin/ 8 | web/public 9 | pprof/ 10 | 11 | # vue 12 | node_modules/ 13 | dist/ 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | **/*.log 18 | tests/**/coverage/ 19 | tests/e2e/reports 20 | selenium-debug.log 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.local 27 | package-lock.json 28 | yarn.lock -------------------------------------------------------------------------------- /web/vue-admin/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} path 3 | * @returns {Boolean} 4 | */ 5 | export function isExternal(path) { 6 | return /^(https?:|mailto:|tel:)/.test(path) 7 | } 8 | 9 | /** 10 | * @param {string} str 11 | * @returns {Boolean} 12 | */ 13 | export function validUsername(str) { 14 | const valid_map = ['admin', 'editor'] 15 | return valid_map.indexOf(str.trim()) >= 0 16 | } 17 | -------------------------------------------------------------------------------- /document/protocol/fastcgi.md: -------------------------------------------------------------------------------- 1 | ## FastCGI 协议 2 | 3 | - 协议描述 4 | 5 | > 使用FastCGI协议 推送消息给Worker机。 6 | > 常见的Worker机为PHP-FPM 7 | 8 | ``` 9 | 消息ID:MESSAGE_ID,在FastCGI协议中发送,PHP-FPM会解析到$_SERVER['MESSAGE_ID'] 10 | 消息正文:在POST请求中。 11 | ``` 12 | 13 | ### PHP使用示例 14 | 15 | ```php 16 | //MESSAGE_ID 17 | $_SERVER['MESSAGE_ID']; 18 | 19 | //message body 下列三种方式均可 20 | $_REQUEST; 21 | $_POST; 22 | file_get_contents("php://input"); 23 | ``` -------------------------------------------------------------------------------- /internal/module/tool/tool.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | //获取本机内网IP 8 | func GetInternalIP() string { 9 | addrs, err := net.InterfaceAddrs() 10 | if err != nil { 11 | return "" 12 | } 13 | for _, addr := range addrs { 14 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 15 | if ipnet.IP.To4() != nil { 16 | return ipnet.IP.String() 17 | } 18 | } 19 | } 20 | return "" 21 | } 22 | -------------------------------------------------------------------------------- /web/vue-admin/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/vue-admin/tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /web/vue-admin/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /document/api/admin/api/consumeservermap/update.md: -------------------------------------------------------------------------------- 1 | ### 更新消费者和服务器的关联关系 2 | 3 | - 接口功能 4 | 5 | > 更新一个消费者和服务器的关联关系 6 | 7 | ``` 8 | GET /admin/admin/consumeServerMap/update 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |关联关系ID | 16 | |consumeid |true |string |消费者ID | 17 | |serverid |true |string |服务器ID | 18 | |weight |false |int |权重,默认0 | 19 | |invalid |false |int |是否有效,默认0有效,1无效 | 20 | 21 | - 返回结果 22 | 23 | ``` 24 | { 25 | code: 200, 26 | msg: "ok", 27 | result: "ok" 28 | } 29 | ``` -------------------------------------------------------------------------------- /web/vue-admin/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/vue-admin/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app 4 | '@vue/cli-plugin-babel/preset' 5 | ], 6 | 'env': { 7 | 'development': { 8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). 9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. 10 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html 11 | 'plugins': ['dynamic-import-node'] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /deployments/supervisor/nsqproxy.conf: -------------------------------------------------------------------------------- 1 | [program:nsqproxy] 2 | command=/home/nsq/nsqproxy/nsqproxy -nsqlookupdHTTP=1.1.1.1:4161,1.1.1.2:4161 -dbHost=127.0.0.1 -dbPort=3306 -dbUsername=root -dbPassword=rootpassword -dbName=dbname -logPath=/home/log/nsq/proxy.log -subLogPath=/home/log/nsq/sub.log -logLevel=info 3 | stdout_logfile=/home/log/nsq/nsqproxy.log 4 | stderr_logfile=/home/log/nsq/nsqproxy.error.log 5 | directory=/home/nsq/nsqproxy 6 | process_name=%(program_name)s_%(process_num)02d 7 | numprocs=1 8 | umask=022 9 | priority=999 10 | autostart=true 11 | autorestart=true 12 | startsecs=10 13 | startretries=10000 14 | user=www-data 15 | serverurl=AUTO -------------------------------------------------------------------------------- /document/api/admin/api/workserver/get.md: -------------------------------------------------------------------------------- 1 | ### 查询一个服务器 2 | 3 | - 接口功能 4 | 5 | > 根据主键ID查询一个服务器 6 | 7 | ``` 8 | GET /admin/api/workServer/get 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |服务器ID | 16 | 17 | - 返回结果 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: { 24 | id: 1, 25 | addr: "10.10.1.1:80", 26 | protocol: "HTTP", 27 | extra: "index.php", 28 | description: "通用机器", 29 | owner: "", 30 | invalid: 0, 31 | createdAt: "2018-10-16T14:31:08+08:00", 32 | updatedAt: "0001-01-01T00:00:00Z" 33 | } 34 | } 35 | ``` -------------------------------------------------------------------------------- /document/doc/make.md: -------------------------------------------------------------------------------- 1 | # make命令 一览 2 | 3 | > make命令可以快速执行一些封装好的命令,如build、kill、run等。 4 | 5 | * `make build` 编译为golang程序,编译后的可执行文件在bin/目录 6 | * `make build-linux` 编译为可在Linux上执行的golang程序,编译后的可执行文件在bin/目录 7 | * `make build-all` 编译为可在Linux、Windows、OSX上执行的golang程序,编译后的可执行文件在bin/目录 8 | * `make clean` 删除所有编译后的可执行文件,即清空bin/目录 9 | * `make kill` 关闭正在运行的nsqproxy进程 10 | * `make test` 执行go test 11 | * `make run` 运行 nohup ./bin/nsqproxy & 12 | * `make statik` 将静态资源文件编译成go文件。即statik -src=web/public/ -dest=internal -f 13 | * `make vue-install` 安装VUE,即npm install 14 | * `make vue-install-taobao` 同make vue-install,使用淘宝的源进行安装,防止官方源被墙 15 | * `make vue-build` 将VUE文件编译打包并复制到web/public/目录下 16 | * `make vue-dev` 将VUE文件编译打包并复制到web/public/目录下 -------------------------------------------------------------------------------- /web/vue-admin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /document/api/admin/api/consumeservermap/create.md: -------------------------------------------------------------------------------- 1 | ### 创建消费者和服务器的关联关系 2 | 3 | - 接口功能 4 | 5 | > 创建一个消费者和服务器的关联关系 6 | 7 | ``` 8 | GET /admin/api/consumeServerMap/create 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |consumeid |true |string |消费者ID | 16 | |serverid |true |string |服务器ID | 17 | |weight |false |int |权重,默认0 | 18 | |invalid |false |int |是否有效,默认0有效,1无效 | 19 | 20 | - 返回结果 21 | 22 | ``` 23 | { 24 | code: 200, 25 | msg: "ok", 26 | result: { 27 | id: 1, 28 | consumeid: 1, 29 | serverid: 1, 30 | weight: 1, 31 | invalid: 0, 32 | createdAt: "2020-11-30T10:45:29+08:00", 33 | updatedAt: "0001-01-01T00:00:00Z", 34 | } 35 | } 36 | ``` -------------------------------------------------------------------------------- /internal/worker/cbnsq_test.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "github.com/nsqio/go-nsq" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestCBNSQWorker_Send(t *testing.T) { 10 | wc := newWorkerConfig("127.0.0.1:19910", "CbNsQ", "", 1*time.Second, 1*time.Second, 1*time.Second) 11 | handler := &CBNSQWorker{} 12 | handler.new(wc) 13 | 14 | messageId := nsq.MessageID([16]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'}) 15 | body := []byte("Hello world") 16 | message := nsq.NewMessage(messageId, body) 17 | res, err := handler.Send(message) 18 | if err != nil { 19 | t.Fatalf("send error: %s", err.Error()) 20 | } 21 | if string(res) != "200 ok" { 22 | t.Fatalf("response body is not match") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/worker/http_test.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "github.com/nsqio/go-nsq" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestHTTPWorker_Send(t *testing.T) { 10 | wc := newWorkerConfig("127.0.0.1:80", "HtTp", "index.php", 1*time.Second, 1*time.Second, 1*time.Second) 11 | handler := &HTTPWorker{} 12 | handler.new(wc) 13 | 14 | messageId := nsq.MessageID([16]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'}) 15 | body := []byte("Hello world") 16 | message := nsq.NewMessage(messageId, body) 17 | res, err := handler.Send(message) 18 | if err != nil { 19 | t.Fatalf("send error: %s", err.Error()) 20 | } 21 | if string(res) != "200 ok" { 22 | t.Fatalf("response body is not match") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/vue-admin/src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | sidebarLogo: sidebarLogo 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | // eslint-disable-next-line no-prototype-builtins 14 | if (state.hasOwnProperty(key)) { 15 | state[key] = value 16 | } 17 | } 18 | } 19 | 20 | const actions = { 21 | changeSetting({ commit }, data) { 22 | commit('CHANGE_SETTING', data) 23 | } 24 | } 25 | 26 | export default { 27 | namespaced: true, 28 | state, 29 | mutations, 30 | actions 31 | } 32 | 33 | -------------------------------------------------------------------------------- /web/vue-admin/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /web/vue-admin/tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /web/vue-admin/tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /document/api/admin/api/workserver/update.md: -------------------------------------------------------------------------------- 1 | ### 更新服务器 2 | 3 | - 接口功能 4 | 5 | > 更新一个服务器配置 6 | 7 | ``` 8 | GET /admin/api/workServer/update 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |服务器ID | 16 | |addr |true |string |地址,IP:PORT | 17 | |protocol |true |string |协议,如HTTP、FastCGI、CBNSQ | 18 | |extra |false |string |扩展字段,默认空 | 19 | |description |false |string |描述,默认空 | 20 | |owner |false |string |责任人,默认空 | 21 | |invalid |false |int |是否有效,默认0有效,1无效 | 22 | 23 | - extra参数 详解 24 | - 当protocol为HTTP时,extra表示URL的路径,即http://addr/extra 25 | - 当protocol为FastCGI时,extra表示要执行的文件名路径,即/home/wwwroot/app/index.php 26 | 27 | - 返回结果 28 | 29 | ``` 30 | { 31 | code: 200, 32 | msg: "ok", 33 | result: "ok" 34 | } 35 | ``` -------------------------------------------------------------------------------- /internal/worker/fastcgi_test.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "github.com/nsqio/go-nsq" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestFastCGIWorker_Send(t *testing.T) { 10 | wc := newWorkerConfig("127.0.0.1:9000", "FaStCgI", "/var/www/index.php", 1*time.Second, 1*time.Second, 1*time.Second) 11 | handler := &FastCGIWorker{} 12 | handler.new(wc) 13 | 14 | messageId := nsq.MessageID([16]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'}) 15 | body := []byte("Hello world") 16 | message := nsq.NewMessage(messageId, body) 17 | res, err := handler.Send(message) 18 | if err != nil { 19 | t.Fatalf("send error: %s", err.Error()) 20 | } 21 | if string(res) != "200 ok" { 22 | t.Fatalf("response body is not match") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /document/protocol/http.md: -------------------------------------------------------------------------------- 1 | ## HTTP 协议 2 | 3 | - 协议描述 4 | 5 | > 使用HTTP POST协议 推送消息给Worker机。Content-Type为application/x-www-form-urlencoded 6 | 7 | ``` 8 | 消息ID:MESSAGE_ID,在HTTP Header中。 9 | 消息正文:在POST请求中。 10 | ``` 11 | 12 | ### PHP使用示例 13 | 14 | ```php 15 | //MESSAGE_ID 16 | $_SERVER['MESSAGE_ID']; 17 | 18 | //message body 下列三种方式均可 19 | $_REQUEST; 20 | $_POST; 21 | file_get_contents("php://input"); 22 | ``` 23 | 24 | ### Golang使用示例 25 | 26 | ```golang 27 | //MESSAGE_ID 28 | messageId := r.Header.Get("MESSAGE_ID") 29 | 30 | //方法一 - []byte类型的消息体 31 | data, _ := ioutil.ReadAll(r.Body) 32 | 33 | //方法二 - KV格式 34 | value := r.PostFormValue("key") 35 | //或 36 | value = r.FormValue("key") 37 | 38 | //方法三 - KV格式 39 | r.ParseForm() 40 | value = r.Form.Get("key") 41 | //或 42 | value = r.Form["key"][0] 43 | ``` -------------------------------------------------------------------------------- /web/vue-admin/tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /internal/module/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | //以下功能依赖systemconfig中的配置 4 | var logger *Logger 5 | 6 | func Init(filename, level string) { 7 | logger = NewLogger(filename, "", level) 8 | } 9 | 10 | func Debugf(format string, v ...interface{}) { 11 | logger.WithLevelf(LOG_DEBUG, format, v...) 12 | } 13 | 14 | func Infof(format string, v ...interface{}) { 15 | logger.WithLevelf(LOG_INFO, format, v...) 16 | } 17 | 18 | func Warningf(format string, v ...interface{}) { 19 | logger.WithLevelf(LOG_WARNING, format, v...) 20 | } 21 | 22 | func Errorf(format string, v ...interface{}) { 23 | logger.WithLevelf(LOG_ERROR, format, v...) 24 | } 25 | 26 | func Fatalf(format string, v ...interface{}) { 27 | logger.WithLevelf(LOG_FATAL, format, v...) 28 | } 29 | 30 | func Close() { 31 | logger.Close() 32 | } 33 | -------------------------------------------------------------------------------- /internal/httper/http.go: -------------------------------------------------------------------------------- 1 | package httper 2 | 3 | import ( 4 | _ "github.com/changba/nsqproxy/internal/statik" 5 | "github.com/rakyll/statik/fs" 6 | "net/http" 7 | _ "net/http/pprof" 8 | ) 9 | 10 | type Httper struct { 11 | addr string 12 | server *http.Server 13 | statikFS http.FileSystem 14 | } 15 | 16 | func NewHttper(addr string) *Httper { 17 | statikFS, err := fs.New() 18 | if err != nil { 19 | panic("NewHttper statikFS error: " + err.Error()) 20 | } 21 | return &Httper{ 22 | addr: addr, 23 | server: &http.Server{Addr: addr, Handler: nil}, 24 | statikFS: statikFS, 25 | } 26 | } 27 | 28 | // 启动HTTP 29 | func (h *Httper) Run() { 30 | h.router() 31 | go func() { 32 | err := h.server.ListenAndServe() 33 | if err != nil { 34 | panic("ListenAndServe error: " + err.Error()) 35 | } 36 | }() 37 | } -------------------------------------------------------------------------------- /web/vue-admin/src/api/consume.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: './api/consumeConfig/page', 6 | method: 'get', 7 | data: { ...params } 8 | }) 9 | } 10 | 11 | export function create(params) { 12 | return request({ 13 | url: './api/consumeConfig/create', 14 | method: 'get', 15 | data: { 16 | ...params 17 | } 18 | }) 19 | } 20 | 21 | export function update(params) { 22 | return request({ 23 | url: './api/consumeConfig/update', 24 | method: 'get', 25 | data: { 26 | ...params 27 | } 28 | }) 29 | } 30 | 31 | export function deleteAction(params) { 32 | return request({ 33 | url: './api/consumeConfig/delete', 34 | method: 'get', 35 | data: { 36 | ...params 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /web/vue-admin/src/api/consumeWorkServerMap.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: './api/consumeConfig/workList', 6 | method: 'get', 7 | data: { ...params } 8 | }) 9 | } 10 | 11 | export function create(params) { 12 | return request({ 13 | url: './api/consumeServerMap/create', 14 | method: 'get', 15 | data: { 16 | ...params 17 | } 18 | }) 19 | } 20 | 21 | export function update(params) { 22 | return request({ 23 | url: './api/consumeServerMap/update', 24 | method: 'get', 25 | data: { 26 | ...params 27 | } 28 | }) 29 | } 30 | 31 | export function deleteAction(params) { 32 | return request({ 33 | url: './api/consumeServerMap/delete', 34 | method: 'get', 35 | data: { 36 | ...params 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /document/api/admin/api/consumeconfig/get.md: -------------------------------------------------------------------------------- 1 | ### 查询一个消费者 2 | 3 | - 接口功能 4 | 5 | > 根据主键ID查询一个消费者 6 | 7 | ``` 8 | GET /admin/api/consumeConfig/get 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |消费者ID | 16 | 17 | - 返回结果 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: { 24 | id: 1, 25 | topic: "test_topic_1", 26 | channel: "one", 27 | description: "描述", 28 | owner: "责任人", 29 | monitorThreshold: 0, 30 | handleNum: 6, 31 | maxInFlight: 6, 32 | isRequeue: false, 33 | timeoutDial: 3590, 34 | timeoutRead: 3590, 35 | timeoutWrite: 3590, 36 | invalid: 0, 37 | createdAt: "2020-08-20T15:08:39+08:00", 38 | updatedAt: "0001-01-01T00:00:00Z", 39 | serverList: null 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /web/vue-admin/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /document/api/admin/api/consumeconfig/update.md: -------------------------------------------------------------------------------- 1 | ### 更新消费者 2 | 3 | - 接口功能 4 | 5 | > 更新一个消费者 6 | 7 | ``` 8 | GET /admin/api/consumeConfig/update 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |消费者ID | 16 | |topic |true |string |Topic名 | 17 | |channel |true |string |Channel名 | 18 | |description |false |string |描述,默认空 | 19 | |owner |false |string |责任人,默认空 | 20 | |monitorThreshold |false |int |积压报警阈值,默认50000 | 21 | |handleNum |false |int |该队列的并发量,默认2 | 22 | |maxInFlight |false |int |NSQD最多同时推送多少个消息,默认2 | 23 | |isRequeue |false |bool |失败,超时等情况是否重新入队,默认false | 24 | |timeoutDial |false |int |超时时间,默认3590秒 | 25 | |timeoutRead |false |int |读超时时间,默认3590秒 | 26 | |timeoutWrite |false |int |写超时时间,默认3590秒 | 27 | |invalid |false |int |是否有效,默认0有效,1无效 | 28 | 29 | - 返回结果 30 | 31 | ``` 32 | { 33 | code: 200, 34 | msg: "ok", 35 | result: "ok" 36 | } 37 | ``` -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /web/vue-admin/src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | 46 | // to fix el-date-picker css style 47 | .el-range-separator { 48 | box-sizing: content-box; 49 | } 50 | -------------------------------------------------------------------------------- /web/vue-admin/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /web/vue-admin/src/api/workServer.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getAll(params) { 4 | return request({ 5 | url: './api/workServer/all', 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function getList(params) { 11 | return request({ 12 | url: './api/workServer/page', 13 | method: 'get', 14 | data: { 15 | ...params 16 | } 17 | }) 18 | } 19 | 20 | export function create(params) { 21 | return request({ 22 | url: './api/workServer/create', 23 | method: 'get', 24 | data: { 25 | ...params 26 | } 27 | }) 28 | } 29 | 30 | export function update(params) { 31 | return request({ 32 | url: './api/workServer/update', 33 | method: 'get', 34 | data: { 35 | ...params 36 | } 37 | }) 38 | } 39 | 40 | export function deleteAction(params) { 41 | return request({ 42 | url: './api/workServer/delete', 43 | method: 'get', 44 | data: { 45 | ...params 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /document/api/admin/api/consumeservermap/getWork.md: -------------------------------------------------------------------------------- 1 | ### 查询一个消费者和服务器的关联关系 2 | 3 | - 接口功能 4 | 5 | > 根据主键ID查询一个消费者和服务器的关联关系 6 | 7 | ``` 8 | GET /admin/admin/consumeServerMap/getWork 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |关联关系ID | 16 | 17 | - 返回结果 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: { 24 | id: 1, 25 | consumeid: 1, 26 | serverid: 1, 27 | weight: 1, 28 | invalid: 0, 29 | createdAt: "2020-11-30T10:45:29+08:00", 30 | updatedAt: "0001-01-01T00:00:00Z", 31 | workServer: { 32 | id: 1, 33 | addr: "0.0.0.0:80", 34 | protocol: "HTTP", 35 | extra: "test.php", 36 | description: "", 37 | owner: "", 38 | invalid: 0, 39 | createdAt: "2020-11-27T11:04:31+08:00", 40 | updatedAt: "0001-01-01T00:00:00Z" 41 | } 42 | } 43 | } 44 | ``` -------------------------------------------------------------------------------- /document/api/admin/api/workserver/all.md: -------------------------------------------------------------------------------- 1 | ### 批量查询服务器 2 | 3 | - 接口功能 4 | 5 | > 批量查询服务器 6 | 7 | ``` 8 | GET /admin/api/workServer/all 9 | ``` 10 | 11 | - 请求参数 12 | 13 | > 无 14 | 15 | - 返回结果 16 | 17 | ``` 18 | { 19 | code: 200, 20 | msg: "ok", 21 | result: [ 22 | { 23 | id: 1, 24 | addr: "10.10.1.1:80", 25 | protocol: "HTTP", 26 | extra: "index.php", 27 | description: "通用机器", 28 | owner: "", 29 | invalid: 0, 30 | createdAt: "2018-10-16T14:31:08+08:00", 31 | updatedAt: "0001-01-01T00:00:00Z" 32 | }, 33 | { 34 | id: 2, 35 | addr: "10.10.1.1:9000", 36 | protocol: "FASTCGI", 37 | extra: "/home/wwwroot/test.php", 38 | description: "通用机器", 39 | owner: "", 40 | invalid: 0, 41 | createdAt: "2018-10-16T14:31:08+08:00", 42 | updatedAt: "0001-01-01T00:00:00Z" 43 | } 44 | ] 45 | } 46 | ``` -------------------------------------------------------------------------------- /internal/model/common_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestNewDb(t *testing.T) { 9 | NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 10 | type version struct { 11 | Version string 12 | } 13 | v := version{} 14 | result := db.Raw("SELECT @@version AS version").Scan(&v) 15 | if result.Error != nil { 16 | t.Fatalf("error: %s", result.Error.Error()) 17 | } 18 | if result.RowsAffected != 1 { 19 | t.Fatalf("RowsAffected != 1") 20 | } 21 | if len(v.Version) <= 0 { 22 | t.Fatalf("v.Version is empty") 23 | } 24 | t.Log(v.Version) 25 | } 26 | 27 | func TestGetAvailableConsumeList(t *testing.T) { 28 | NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 29 | consumeConfigList, err := GetAvailableConsumeList() 30 | if err != nil { 31 | t.Fatalf("error: %s", err.Error()) 32 | } 33 | j, err := json.Marshal(consumeConfigList) 34 | if err != nil { 35 | t.Fatalf("error: %s", err.Error()) 36 | } 37 | if len(j) < 100 { 38 | t.Fatalf("json is short") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /document/api/admin/api/workserver/create.md: -------------------------------------------------------------------------------- 1 | ### 创建服务器 2 | 3 | - 接口功能 4 | 5 | > 创建一个新的服务器 6 | 7 | ``` 8 | GET /admin/api/workServer/create 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |addr |true |string |地址,IP:PORT | 16 | |protocol |true |string |协议,如HTTP、FastCGI、CBNSQ | 17 | |extra |false |string |扩展字段,默认空 | 18 | |description |false |string |描述,默认空 | 19 | |owner |false |string |责任人,默认空 | 20 | |invalid |false |int |是否有效,默认0有效,1无效 | 21 | 22 | - extra参数 详解 23 | - 当protocol为HTTP时,extra表示URL的路径,即http://addr/extra 24 | - 当protocol为FastCGI时,extra表示要执行的文件名路径,即/home/wwwroot/app/index.php 25 | 26 | - 返回结果 27 | 28 | ``` 29 | { 30 | code: 200, 31 | msg: "ok", 32 | result: { 33 | id: 1, 34 | addr: "10.10.1.1:80", 35 | protocol: "HTTP", 36 | extra: "index.php", 37 | description: "通用机器", 38 | owner: "", 39 | invalid: 0, 40 | createdAt: "2018-10-16T14:31:08+08:00", 41 | updatedAt: "0001-01-01T00:00:00Z" 42 | } 43 | } 44 | ``` -------------------------------------------------------------------------------- /web/vue-admin/build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /document/doc/flag.md: -------------------------------------------------------------------------------- 1 | # 启动参数 一览 2 | 3 | > 启动时命令行可以传入的参数,所有的参数都有默认值。 4 | 5 | > `./bin/nsqproxy -h` 也可查看 6 | 7 | * NSQProxy相关部分 8 | * `-httpAddr string` 监听的HTTP端口 (default "0.0.0.0:19421") 9 | * `-masterAddr string` 主库IP端口,为空则本机为主机,不为空则本机为备机。本机为备机时,会定期给masterAddr发PING,连续5次未收到PONG则认定主机异常,该备机启动。(default "") 10 | * `-logLevel string` 日志等级,可选有debug、info、warning、error、fatal (default "info") 11 | * `-logPath string` 系统日志路径 (default "logs/proxy.log") 12 | * `-subLogPath string` 消费log,消费详情由于量大成功消费log仅在日志等级为debug时启用 (default "logs/sub.log") 13 | * `-updateConfigInterval int` 定时向MySQL更新消费者配置的间隔时间,单位秒 (default 60)。MySQL中记录变更后,不会主动推送给NSQProxy,而是NSQProxy启动定时器定时同步。 14 | * NSQ相关部分 15 | * `-nsqlookupdHTTP string` nsqLookupd的HTTP地址,多个用逗号分割如"127.0.0.1:4161,127.0.0.1:4163" (default "127.0.0.1:4161") 16 | * MySQL相关部分 17 | * `-dbHost string` MySQL的IP (default "127.0.0.1") 18 | * `-dbPort string` MySQL的端口 (default "3306") 19 | * `-dbPassword string` MySQL的密码 (default "") 20 | * `-dbUsername string` MySQL的账号 (default "root") 21 | * `-dbName string` MySQL的库名 (default "nsqproxy") -------------------------------------------------------------------------------- /internal/httper/http_test.go: -------------------------------------------------------------------------------- 1 | package httper 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestHttper_Run(t *testing.T) { 12 | addr := "0.0.0.0:19421" 13 | httper := NewHttper(addr) 14 | httper.Run() 15 | time.Sleep(100 * time.Microsecond) 16 | url := "http://" + addr + "/status" 17 | resp, err := http.Get(url) 18 | if err != nil { 19 | t.Fatal("请求错误: " + err.Error()) 20 | } 21 | body, err := ioutil.ReadAll(resp.Body) 22 | if err != nil { 23 | t.Fatalf("ReadAll error: %s", err.Error()) 24 | } 25 | _ = resp.Body.Close() 26 | 27 | type response struct { 28 | Code int `json:"code"` 29 | Message string `json:"msg"` 30 | Result interface{} `json:"result"` 31 | } 32 | r := response{} 33 | err = json.Unmarshal(body, &r) 34 | if err != nil { 35 | t.Fatalf("json decode failed. err: %s", err.Error()) 36 | } 37 | if r.Code != 200 || r.Result != "ok" { 38 | t.Fatalf("response failed. action code:%d expect code:%d action result:%s expect result:%s", r.Code, 200, body, "ok") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /document/api/admin/api/workserver/page.md: -------------------------------------------------------------------------------- 1 | ### 批量查询服务器 2 | 3 | - 接口功能 4 | 5 | > 批量查询服务器 6 | 7 | ``` 8 | GET /admin/api/workServer/page 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |page |false |int |分页,默认1,小于等于零时表示第一页,一页20条 | 16 | 17 | - 返回结果 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: [ 24 | { 25 | id: 1, 26 | addr: "10.10.1.1:80", 27 | protocol: "HTTP", 28 | extra: "index.php", 29 | description: "通用机器", 30 | owner: "", 31 | invalid: 0, 32 | createdAt: "2018-10-16T14:31:08+08:00", 33 | updatedAt: "0001-01-01T00:00:00Z" 34 | }, 35 | { 36 | id: 2, 37 | addr: "10.10.1.1:9000", 38 | protocol: "FASTCGI", 39 | extra: "/home/wwwroot/test.php", 40 | description: "通用机器", 41 | owner: "", 42 | invalid: 0, 43 | createdAt: "2018-10-16T14:31:08+08:00", 44 | updatedAt: "0001-01-01T00:00:00Z" 45 | } 46 | ] 47 | } 48 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 changba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/vue-admin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/vue-admin/tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /web/vue-admin/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import locale from 'element-ui/lib/locale/lang/en' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import store from './store' 13 | import router from './router' 14 | 15 | import '@/icons' // icon 16 | import '@/permission' // permission control 17 | 18 | /** 19 | * If you don't want to use mock-server 20 | * you want to use MockJs for mock api 21 | * you can execute: mockXHR() 22 | * 23 | * Currently MockJs will be used in the production environment, 24 | * please remove it before going online ! ! ! 25 | */ 26 | if (process.env.NODE_ENV === 'production') { 27 | // const { mockXHR } = require('../mock') 28 | // mockXHR() 29 | } 30 | 31 | // set ElementUI lang to EN 32 | Vue.use(ElementUI, { locale }) 33 | // 如果想要中文版 element-ui,按如下方式声明 34 | // Vue.use(ElementUI) 35 | 36 | Vue.config.productionTip = false 37 | 38 | new Vue({ 39 | el: '#app', 40 | router, 41 | store, 42 | render: h => h(App) 43 | }) 44 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/API.md: -------------------------------------------------------------------------------- 1 | 消费者配置: 2 | 3 | 新增:http://10.10.248.110:19421/api/consumeconfig/create?topic=test_api1 4 | 获取一条:http://10.10.248.110:19421/api/consumeconfig/get?id=2 5 | 更新:http://10.10.248.110:19421/api/consumeconfig/update?id=86&topic=test_api2 6 | 删除:http://10.10.248.110:19421/api/consumeconfig/delete?id=86 7 | 获取全部不分页:http://10.10.248.110:19421/api/consumeconfig/all?topic=hjk 8 | 获取第一页:http://10.10.248.110:19421/api/consumeconfig/all?page=1 9 | 10 | type ConsumeConfig struct { 11 | //主键ID 12 | Id int `gorm:"primaryKey"` 13 | //队列名 14 | Topic string 15 | //通道名 16 | Channel string 17 | //描述 18 | Description string 19 | //责任人 20 | Owner string 21 | //积压报警阈值 22 | MonitorThreshold int 23 | //该队列的并发量 24 | HandleNum int 25 | //NSQD最多同时推送多少个消息 26 | MaxInFlight int 27 | //失败,超时等情况是否重新入队 28 | IsRequeue bool 29 | //超时时间 30 | TimeoutDial time.Duration 31 | //读超时时间 32 | TimeoutRead time.Duration 33 | //写超时时间 34 | TimeoutWrite time.Duration 35 | //是否暂停 36 | Invalid int 37 | //是否暂停 38 | Pause int 39 | //创建时间 40 | CreatedAt time.Time 41 | //更新时间 42 | UpdatedAt time.Time 43 | } 44 | 45 | 46 | 分页 total page 47 | list 字段与创建修改不对应 48 | boolean类型默认值 -------------------------------------------------------------------------------- /internal/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/changba/nsqproxy/config" 5 | "github.com/changba/nsqproxy/internal/model" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func init() { 11 | testing.Init() 12 | config.NewSystemConfig() 13 | model.NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 14 | } 15 | 16 | func TestNewProxy(t *testing.T) { 17 | p := NewProxy() 18 | if p.consumeConfigList[0].Id <= 0 { 19 | t.Fatal("初始化proxy-consumeConfig失败") 20 | } 21 | if p.IsStop() { 22 | t.Fatal("初始化proxy-exitFlag失败") 23 | } 24 | } 25 | 26 | func TestRun(t *testing.T) { 27 | p := NewProxy() 28 | p.Run() 29 | time.Sleep(2 * time.Second) 30 | 31 | for _, consumeConfig := range p.consumeConfigList { 32 | if !consumeConfig.StatusIsSuccess() { 33 | t.Fatal(consumeConfig.Topic + " consumeConfig.status is not success") 34 | } 35 | if consumeConfig.Consumer == nil { 36 | t.Fatal(consumeConfig.Topic + " consumeConfig.Consumer is nil") 37 | } 38 | } 39 | p.Stop() 40 | for _, consumeConfig := range p.consumeConfigList { 41 | if !consumeConfig.StatusIsClose() { 42 | t.Fatal(consumeConfig.Topic + " consume status is not closed") 43 | } 44 | } 45 | time.Sleep(2 * time.Second) 46 | } 47 | -------------------------------------------------------------------------------- /web/vue-admin/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /web/vue-admin/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /internal/httper/response.go: -------------------------------------------------------------------------------- 1 | package httper 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/changba/nsqproxy/internal/module/logger" 6 | "net/http" 7 | ) 8 | 9 | const HttpCodeOK = http.StatusOK 10 | const HttpCodeBadRequest = http.StatusBadRequest 11 | const HttpCodeHttpCodeForbidden = http.StatusUnauthorized 12 | const HttpCodeForbidden = 403 13 | const HttpCodeNotFound = 404 14 | const HttpCodeInternalServerError = 500 15 | const HttpCodeNotImplemented = 501 16 | const HttpCodeBadGateway = 502 17 | const HttpCodeServiceUnavailable = 503 18 | 19 | type resp struct { 20 | w http.ResponseWriter 21 | Code int `json:"code"` 22 | Message string `json:"msg"` 23 | Result interface{} `json:"result"` 24 | } 25 | 26 | func Success(w http.ResponseWriter, result interface{}) { 27 | response(w, HttpCodeOK, "ok", result) 28 | } 29 | 30 | func Failed(w http.ResponseWriter, code int, message string) { 31 | response(w, code, message, nil) 32 | } 33 | 34 | func response(w http.ResponseWriter, code int, message string, result interface{}) { 35 | r := resp{ 36 | Code: code, 37 | Message: message, 38 | Result: result, 39 | } 40 | j, err := json.Marshal(r) 41 | if err != nil { 42 | logger.Errorf("response json error: %s", err.Error()) 43 | } 44 | _, _ = w.Write(j) 45 | } 46 | -------------------------------------------------------------------------------- /web/vue-admin/tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('timestamp string', () => { 9 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('ten digits timestamp', () => { 12 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('new Date', () => { 15 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 16 | }) 17 | it('format', () => { 18 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 19 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 20 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 24 | }) 25 | it('get the day of the week', () => { 26 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 27 | }) 28 | it('empty argument', () => { 29 | expect(parseTime()).toBeNull() 30 | }) 31 | 32 | it('null', () => { 33 | expect(parseTime(null)).toBeNull() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /web/vue-admin/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /document/api/admin/api/consumeconfig/create.md: -------------------------------------------------------------------------------- 1 | ### 创建消费者 2 | 3 | - 接口功能 4 | 5 | > 创建一个新的消费者 6 | 7 | ``` 8 | GET /admin/api/consumeConfig/create 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |topic |true |string |Topic名 | 16 | |channel |true |string |Channel名 | 17 | |description |false |string |描述,默认空 | 18 | |owner |false |string |责任人,默认空 | 19 | |monitorThreshold |false |int |积压报警阈值,默认50000 | 20 | |handleNum |false |int |该队列的并发量,默认2 | 21 | |maxInFlight |false |int |NSQD最多同时推送多少个消息,默认2 | 22 | |isRequeue |false |bool |失败,超时等情况是否重新入队,默认false | 23 | |timeoutDial |false |int |超时时间,默认3590秒 | 24 | |timeoutRead |false |int |读超时时间,默认3590秒 | 25 | |timeoutWrite |false |int |写超时时间,默认3590秒 | 26 | |invalid |false |int |是否有效,默认0有效,1无效 | 27 | 28 | - 返回结果 29 | 30 | ``` 31 | { 32 | code: 200, 33 | msg: "ok", 34 | result: { 35 | id: 1, 36 | topic: "test_topic_1", 37 | channel: "one", 38 | description: "描述", 39 | owner: "责任人", 40 | monitorThreshold: 0, 41 | handleNum: 6, 42 | maxInFlight: 6, 43 | isRequeue: false, 44 | timeoutDial: 3590, 45 | timeoutRead: 3590, 46 | timeoutWrite: 3590, 47 | invalid: 0, 48 | createdAt: "2020-08-20T15:08:39+08:00", 49 | updatedAt: "0001-01-01T00:00:00Z", 50 | serverList: null, 51 | } 52 | } 53 | ``` -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 2 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 3 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 4 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 5 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 6 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 7 | github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= 8 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 9 | github.com/nsqio/go-nsq v1.0.8 h1:3L2F8tNLlwXXlp2slDUrUWSBn2O3nMh8R1/KEDFTHPk= 10 | github.com/nsqio/go-nsq v1.0.8/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= 11 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= 12 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 13 | gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw= 14 | gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= 15 | gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 16 | gorm.io/gorm v1.20.1 h1:+hOwlHDqvqmBIMflemMVPLJH7tZYK4RxFDBHEfJTup0= 17 | gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 18 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/worker/fastcgi.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "bytes" 5 | "github.com/changba/nsqproxy/config" 6 | "github.com/changba/nsqproxy/internal/module/fastcgi" 7 | "github.com/nsqio/go-nsq" 8 | "io/ioutil" 9 | ) 10 | 11 | type FastCGIWorker struct { 12 | workerConfig workerConfig 13 | } 14 | 15 | func (w *FastCGIWorker) new(workerConfig workerConfig) { 16 | w.workerConfig = workerConfig 17 | } 18 | 19 | func (w *FastCGIWorker) Send(message *nsq.Message) ([]byte, error) { 20 | //连接到work 21 | fc, err := fastcgi.DialTimeout("tcp", w.workerConfig.addr, w.workerConfig.timeoutDial, w.workerConfig.timeoutWrite, w.workerConfig.timeoutRead) 22 | if err != nil { 23 | return nil, newWorkerErrorConnect(err) 24 | } 25 | defer fc.Close() 26 | //fpm参数,php可以用$_SERVER获取 27 | server := w.getServer(w.workerConfig, message) 28 | //给work发送数据 29 | rd := bytes.NewReader(message.Body) 30 | resp, err := fc.Post(server, "", rd, rd.Len()) 31 | if err != nil { 32 | return nil, newWorkerErrorWrite(err) 33 | } 34 | content, err := ioutil.ReadAll(resp.Body) 35 | if err != nil { 36 | return nil, newWorkerErrorRead(err) 37 | } 38 | return content, nil 39 | } 40 | 41 | func (w *FastCGIWorker) getServer(wc workerConfig, message *nsq.Message) map[string]string { 42 | //fpm参数,php可以用$_SERVER获取 43 | server := make(map[string]string) 44 | server["SCRIPT_FILENAME"] = wc.extra 45 | server["REMOTE_ADDR"] = config.SystemConfig.InternalIP 46 | server["MESSAGE_ID"] = string(message.ID[:]) 47 | return server 48 | } 49 | -------------------------------------------------------------------------------- /internal/module/tool/httpsyncpool.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/http" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | //用sync.pool管理http client 12 | //用sync.Pool和不用,QPS提升10%,CPU下降50% 13 | type HttpClientPool struct { 14 | pool sync.Pool 15 | } 16 | 17 | func NewHttpClientPool() *HttpClientPool { 18 | return &HttpClientPool{ 19 | pool: sync.Pool{ 20 | New: func() interface{} { 21 | return NewHttpClient() 22 | }, 23 | }, 24 | } 25 | } 26 | 27 | func (h *HttpClientPool) GetClient() *http.Client { 28 | return h.pool.Get().(*http.Client) 29 | } 30 | 31 | func (h *HttpClientPool) PutClient(client *http.Client) { 32 | h.pool.Put(client) 33 | } 34 | 35 | func (h *HttpClientPool) Dial(req *http.Request) (*http.Response, error) { 36 | client := h.GetClient() 37 | if client == nil { 38 | return nil, errors.New("HttpClientPool.GetClient is nil") 39 | } 40 | defer h.PutClient(client) 41 | return client.Do(req) 42 | } 43 | 44 | //创建一个http client 45 | func NewHttpClient() *http.Client { 46 | //参数是复制的DefaultClient,只是改了MaxIdleConns和MaxIdleConnsPerHost 47 | return &http.Client{ 48 | Transport: &http.Transport{ 49 | Proxy: http.ProxyFromEnvironment, 50 | DialContext: (&net.Dialer{ 51 | Timeout: 30 * time.Second, 52 | KeepAlive: 30 * time.Second, 53 | DualStack: true, 54 | }).DialContext, 55 | MaxIdleConns: 500, 56 | IdleConnTimeout: 90 * time.Second, 57 | TLSHandshakeTimeout: 10 * time.Second, 58 | ExpectContinueTimeout: 1 * time.Second, 59 | MaxIdleConnsPerHost: 500, 60 | }, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/worker/http.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "errors" 5 | "github.com/changba/nsqproxy/internal/module/tool" 6 | "github.com/nsqio/go-nsq" 7 | "io/ioutil" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | type HTTPWorker struct { 13 | workerConfig workerConfig 14 | clientPool *tool.HttpClientPool 15 | } 16 | 17 | func (w *HTTPWorker) new(wc workerConfig) { 18 | w.workerConfig = wc 19 | w.clientPool = tool.NewHttpClientPool() 20 | } 21 | 22 | //给HTTP发消息 23 | func (w *HTTPWorker) Send(message *nsq.Message) ([]byte, error) { 24 | //构造HTTP请求 25 | //values := url.Values{} 26 | //values.Set("param", string(message.Body)) 27 | req, err := http.NewRequest("POST", "http://"+w.workerConfig.addr+"/"+w.workerConfig.extra, strings.NewReader(string(message.Body))) 28 | if err != nil { 29 | return nil, err 30 | } 31 | //含下划线会被nginx抛弃,横线会被转为下划线。 32 | req.Header.Set("MESSAGE_ID", string(message.ID[:])) 33 | req.Header.Set("MESSAGE-ID", string(message.ID[:])) 34 | req.Header.Set("CONTENT-TYPE", "application/x-www-form-urlencoded") 35 | //获取http.Client 36 | client := w.clientPool.GetClient() 37 | if client == nil { 38 | return nil, errors.New("HttpClientPool.GetClient is nil") 39 | } 40 | defer w.clientPool.PutClient(client) 41 | client.Timeout = w.workerConfig.timeoutDial 42 | //发送请求 43 | resp, err := client.Do(req) 44 | if err != nil { 45 | return nil, newWorkerErrorWrite(err) 46 | } 47 | defer resp.Body.Close() 48 | content, err := ioutil.ReadAll(resp.Body) 49 | if err != nil { 50 | return nil, newWorkerErrorRead(err) 51 | } 52 | return content, nil 53 | } 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO111MODULE=on 2 | 3 | filename = nsqproxy 4 | nowDate = $(shell date +"%Y%m%d%H%M%S") 5 | 6 | .PHONY: build 7 | build: 8 | @echo "Build..." 9 | mkdir -p bin 10 | CGO_ENABLED=0 go build -o bin/$(filename) cmd/nsqproxy.go 11 | 12 | .PHONY: build-linux 13 | build-linux: 14 | @echo "Build for linux..." 15 | mkdir -p bin 16 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(filename)-linux-$(nowDate) cmd/nsqproxy.go 17 | 18 | .PHONY: build-all 19 | build-all: 20 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(filename)-linux-amd64-$(nowDate) cmd/nsqproxy.go 21 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(filename)-darwin-amd64-$(nowDate) cmd/nsqproxy.go 22 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/$(filename)-windows-amd64-$(nowDate) cmd/nsqproxy.go 23 | 24 | .PHONY: clean 25 | clean: 26 | rm -rf bin/nsqproxy* 27 | 28 | .PHONY: kill 29 | kill: 30 | -killall nsqproxy 31 | 32 | .PHONY: test 33 | test: 34 | go test ./... 35 | 36 | .PHONY: run 37 | run: build kill 38 | nohup ./bin/nsqproxy & 39 | 40 | .PHONY: statik 41 | statik: 42 | go get github.com/rakyll/statik 43 | go generate ./... 44 | 45 | .PHONY: vue-install 46 | vue-install: 47 | cd web/vue-admin && npm install 48 | 49 | .PHONY: vue-install-taobao 50 | vue-install-taobao: 51 | cd web/vue-admin && npm install --registry=https://registry.npm.taobao.org 52 | 53 | .PHONY: vue-build 54 | vue-build: 55 | cd web/vue-admin && npm run build:prod 56 | mkdir -p web/public && cp -r web/vue-admin/dist/* web/public/ 57 | 58 | .PHONY: vue-dev 59 | vue-dev: 60 | cd web/vue-admin && yarn run dev -------------------------------------------------------------------------------- /internal/worker/error.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | //连接错误号 4 | const errorConnect = 1 5 | 6 | //写入错误号 7 | const errorWrite = 2 8 | 9 | //读取 10 | const errorRead = 3 11 | 12 | //其他 13 | const errorOther = 0 14 | 15 | //连接错误 16 | type workerErrorConnect struct { 17 | err error 18 | } 19 | 20 | func newWorkerErrorConnect(err error) error { 21 | return workerErrorConnect{ 22 | err: err, 23 | } 24 | } 25 | 26 | func (e workerErrorConnect) Error() string { 27 | return e.err.Error() 28 | } 29 | 30 | //写错误 31 | type workerErrorWrite struct { 32 | err error 33 | } 34 | 35 | func newWorkerErrorWrite(err error) error { 36 | return workerErrorWrite{ 37 | err: err, 38 | } 39 | } 40 | 41 | func (e workerErrorWrite) Error() string { 42 | return e.err.Error() 43 | } 44 | 45 | //读错误 46 | type workerErrorRead struct { 47 | Err error 48 | } 49 | 50 | func newWorkerErrorRead(err error) error { 51 | return workerErrorRead{ 52 | Err: err, 53 | } 54 | } 55 | 56 | func (e workerErrorRead) Error() string { 57 | return e.Err.Error() 58 | } 59 | 60 | func getWorkerType(err error) int { 61 | switch err.(type) { 62 | case workerErrorConnect: 63 | return errorConnect 64 | case workerErrorWrite: 65 | return errorWrite 66 | case workerErrorRead: 67 | return errorRead 68 | } 69 | return errorOther 70 | } 71 | 72 | func IsErrorConnect(err error) bool { 73 | return getWorkerType(err) == errorConnect 74 | } 75 | 76 | func IsErrorWrite(err error) bool { 77 | return getWorkerType(err) == errorWrite 78 | } 79 | 80 | func IsErrorRead(err error) bool { 81 | return getWorkerType(err) == errorRead 82 | } 83 | -------------------------------------------------------------------------------- /internal/worker/worker.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "errors" 5 | "github.com/nsqio/go-nsq" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | //和Worker的通信协议 11 | //HTTP协议 12 | const protocolHttp = "http" 13 | 14 | //FastCGI协议 15 | const protocolFastCGI = "fastcgi" 16 | 17 | //自定义CBNSQ协议(唱吧NSQ) 18 | const protocolCBNSQ = "cbnsq" 19 | 20 | type workerConfig struct { 21 | addr string 22 | protocol string //小写 23 | extra string 24 | timeoutDial time.Duration 25 | timeoutWrite time.Duration 26 | timeoutRead time.Duration 27 | } 28 | 29 | func newWorkerConfig(addr, protocol, extra string, timeoutDial, timeoutWrite, timeoutRead time.Duration) workerConfig { 30 | return workerConfig{ 31 | addr: addr, 32 | protocol: strings.ToLower(protocol), 33 | extra: extra, 34 | timeoutDial: timeoutDial, 35 | timeoutWrite: timeoutWrite, 36 | timeoutRead: timeoutRead, 37 | } 38 | } 39 | 40 | //worker接口 41 | type Worker interface { 42 | new(workerConfig) 43 | Send(*nsq.Message) ([]byte, error) 44 | } 45 | 46 | func NewWorker(addr, protocol, extra string, timeoutDial, timeoutWrite, timeoutRead time.Duration) (Worker, error) { 47 | wc := newWorkerConfig(addr, protocol, extra, timeoutDial, timeoutWrite, timeoutRead) 48 | var handler Worker 49 | switch wc.protocol { 50 | case protocolHttp: 51 | handler = &HTTPWorker{} 52 | case protocolFastCGI: 53 | handler = &FastCGIWorker{} 54 | case protocolCBNSQ: 55 | handler = &CBNSQWorker{} 56 | default: 57 | return nil, errors.New("worker invalid protocol") 58 | } 59 | handler.new(wc) 60 | return handler, nil 61 | } 62 | -------------------------------------------------------------------------------- /web/vue-admin/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /document/api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## 通用响应值 4 | 5 | |返回字段|字段类型|说明| 6 | |:----- |:------|:-----------------------------| 7 | |code |int |成功为200,其他为失败| 8 | |msg |string |成功为ok,失败时为原因| 9 | |result |string |响应正文| 10 | 11 | ## 服务相关 12 | 13 | * 查看状态:[service/status](service/status.md) 14 | * 查看角色:[service/getrole](service/getrole.md) 15 | * pprof:[debug/pprof](debug/pprof.md) 16 | 17 | ## 后台相关 18 | 19 | ### 服务器管理 20 | 21 | * 新增:[admin/api/workServer/create](admin/api/workserver/create.md) 22 | * 修改:[admin/api/workServer/update](admin/api/workserver/update.md) 23 | * 删除一个:[admin/api/workServer/delete](admin/api/workserver/delete.md) 24 | * 查询一个:[admin/api/workServer/get](admin/api/workserver/get.md) 25 | * 查询所有:[admin/api/workServer/page](admin/api/workserver/page.md) 26 | 27 | ### 消费者管理 28 | 29 | * 新增:[admin/api/consumeConfig/create](admin/api/consumeconfig/create.md) 30 | * 修改:[admin/api/consumeConfig/update](admin/api/consumeconfig/update.md) 31 | * 删除一个:[admin/api/consumeConfig/delete](admin/api/consumeconfig/delete.md) 32 | * 查询一个:[admin/api/consumeConfig/get](admin/api/consumeconfig/get.md) 33 | * 查询所有:[admin/api/consumeConfig/page](admin/api/consumeconfig/page.md) 34 | * 查询一个消费者关联服务器:[admin/api/consumeConfig/workList](admin/api/consumeConfig/worklist.md) 35 | 36 | ### 消费者和服务器关联关系管理 37 | 38 | * 新增:[admin/api/consumeServerMap/create](admin/api/consumeservermap/create.md) 39 | * 修改:[admin/api/consumeServerMap/update](admin/api/consumeservermap/update.md) 40 | * 删除一个:[admin/api/consumeServerMap/delete](admin/api/consumeservermap/delete.md) 41 | * 查询一个(含关联服务器信息):[admin/api/consumeServerMap/getWork](admin/api/consumeservermap/getWork.md) -------------------------------------------------------------------------------- /document/api/admin/api/consumeconfig/page.md: -------------------------------------------------------------------------------- 1 | ### 批量查询消费者 2 | 3 | - 接口功能 4 | 5 | > 批量查询消费者 6 | 7 | ``` 8 | GET /admin/api/consumeConfig/page 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |topic |false |string |Topic名,模糊查询,%topic%的方式查询 | 16 | |page |false |int |分页,默认1,小于等于零时表示第一页,一页20条 | 17 | 18 | - 返回结果 19 | 20 | ``` 21 | { 22 | code: 200, 23 | msg: "ok", 24 | result: [ 25 | { 26 | id: 1, 27 | topic: "test_topic_1", 28 | channel: "one", 29 | description: "描述", 30 | owner: "责任人", 31 | monitorThreshold: 0, 32 | handleNum: 6, 33 | maxInFlight: 6, 34 | isRequeue: false, 35 | timeoutDial: 3590, 36 | timeoutRead: 3590, 37 | timeoutWrite: 3590, 38 | invalid: 0, 39 | createdAt: "2020-08-20T15:08:39+08:00", 40 | updatedAt: "0001-01-01T00:00:00Z", 41 | serverList: null 42 | }, 43 | { 44 | id: 2, 45 | topic: "test_topic_2", 46 | channel: "one", 47 | description: "描述", 48 | owner: "责任人", 49 | monitorThreshold: 0, 50 | handleNum: 2, 51 | maxInFlight: 2, 52 | isRequeue: false, 53 | timeoutDial: 3590, 54 | timeoutRead: 3590, 55 | timeoutWrite: 3590, 56 | invalid: 0, 57 | createdAt: "2020-08-20T15:08:39+08:00", 58 | updatedAt: "0001-01-01T00:00:00Z", 59 | serverList: null 60 | } 61 | ] 62 | } 63 | ``` -------------------------------------------------------------------------------- /web/vue-admin/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Message } from 'element-ui' 3 | import qs from 'qs' 4 | 5 | // 创建axios实例 6 | const service = axios.create({ 7 | timeout: 30000, 8 | headers: { 9 | 'X-Requested-With': 'XMLHttpRequest' 10 | }, 11 | transformRequest: [function(data, headers) { 12 | if (data instanceof FormData) { 13 | return data 14 | } 15 | 16 | if (data?.__json__ === true) { 17 | headers['Content-Type'] = 'application/json; charset=utf-8' 18 | data.__json__ = undefined 19 | 20 | return JSON.stringify(data) 21 | } 22 | return qs.stringify(data) 23 | }], 24 | data: { 25 | _timestamp: new Date() 26 | } 27 | }) 28 | 29 | service.interceptors.request.use(config => { 30 | if (config.method === 'get') { 31 | config.params = config.data 32 | } 33 | 34 | return config 35 | }, error => { 36 | // Do something with request error 37 | console.log(error) // for debug 38 | Promise.reject(error) 39 | }) 40 | 41 | service.interceptors.response.use( 42 | response => { 43 | const res = response.data 44 | if (res.code !== 200) { 45 | Message({ 46 | message: res.msg || 'Error', 47 | type: 'error', 48 | duration: 3 * 1000 49 | }) 50 | return Promise.reject(new Error(res.msg || 'Error')) 51 | } else { 52 | return res 53 | } 54 | }, 55 | error => { 56 | console.log('err' + error) // for debug 57 | Message({ 58 | message: error.message, 59 | type: 'error', 60 | duration: 5 * 1000 61 | }) 62 | return Promise.reject(error) 63 | } 64 | ) 65 | 66 | export default service 67 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/tree/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 78 | 79 | -------------------------------------------------------------------------------- /internal/httper/router.go: -------------------------------------------------------------------------------- 1 | package httper 2 | 3 | import ( 4 | "github.com/changba/nsqproxy/config" 5 | "net/http" 6 | ) 7 | 8 | // 启动HTTP 9 | func (h *Httper) router() { 10 | //获取状态 11 | http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { 12 | Success(w, "ok") 13 | }) 14 | //获取角色 15 | http.HandleFunc("/getRole", func(w http.ResponseWriter, r *http.Request) { 16 | Success(w, config.SystemConfig.Role) 17 | }) 18 | 19 | //后台 20 | http.Handle("/admin/", http.StripPrefix("/admin/", http.FileServer(h.statikFS))) 21 | //后台接口 22 | // 消费者管理 23 | consumeConfig := ConsumeConfig{} 24 | http.HandleFunc("/admin/api/consumeConfig/create", consumeConfig.Create) 25 | http.HandleFunc("/admin/api/consumeConfig/update", consumeConfig.Update) 26 | http.HandleFunc("/admin/api/consumeConfig/delete", consumeConfig.Delete) 27 | http.HandleFunc("/admin/api/consumeConfig/page", consumeConfig.Page) 28 | http.HandleFunc("/admin/api/consumeConfig/get", consumeConfig.Get) 29 | http.HandleFunc("/admin/api/consumeConfig/workList", consumeConfig.WorkList) 30 | //Worker机管理 31 | workServer := WorkServer{} 32 | http.HandleFunc("/admin/api/workServer/create", workServer.Create) 33 | http.HandleFunc("/admin/api/workServer/update", workServer.Update) 34 | http.HandleFunc("/admin/api/workServer/delete", workServer.Delete) 35 | http.HandleFunc("/admin/api/workServer/page", workServer.Page) 36 | http.HandleFunc("/admin/api/workServer/all", workServer.All) 37 | http.HandleFunc("/admin/api/workServer/get", workServer.Get) 38 | //消费者和Worker机关联关系管理 39 | consumeServerMap := ConsumeServerMap{} 40 | http.HandleFunc("/admin/api/consumeServerMap/create", consumeServerMap.Create) 41 | http.HandleFunc("/admin/api/consumeServerMap/update", consumeServerMap.Update) 42 | http.HandleFunc("/admin/api/consumeServerMap/delete", consumeServerMap.Delete) 43 | http.HandleFunc("/admin/api/consumeServerMap/getWork", consumeServerMap.GetWork) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/nsqproxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate echo "statik -src=../web/public/ -dest=../internal -f" 4 | //go:generate statik -src=../web/public/ -dest=../internal -f 5 | 6 | import ( 7 | "github.com/changba/nsqproxy/config" 8 | "github.com/changba/nsqproxy/internal/backup" 9 | "github.com/changba/nsqproxy/internal/httper" 10 | "github.com/changba/nsqproxy/internal/model" 11 | "github.com/changba/nsqproxy/internal/module/logger" 12 | "github.com/changba/nsqproxy/internal/module/tool" 13 | "github.com/changba/nsqproxy/internal/proxy" 14 | "os" 15 | "os/signal" 16 | "syscall" 17 | "time" 18 | ) 19 | 20 | // 主函数 21 | func main() { 22 | //初始化系统配置 23 | config.NewSystemConfig() 24 | model.NewDB(config.SystemConfig.DbHost, config.SystemConfig.DbPort, config.SystemConfig.DbUsername, config.SystemConfig.DbPassword, config.SystemConfig.DbName) 25 | //创建一个proxy实例 26 | p := proxy.NewProxy() 27 | //异常捕获 28 | defer func() { 29 | tool.PanicHandlerForLog() 30 | logger.Fatalf("nsqproxy will exit") 31 | os.Exit(2) 32 | }() 33 | //开启HTTP 34 | httper.NewHttper(config.SystemConfig.HttpAddr).Run() 35 | //灾备 36 | backup.Backup(config.SystemConfig.MasterAddr) 37 | //启动一个proxy实例 38 | logger.Infof("nsqproxy is starting") 39 | p.Run() 40 | //监听信号 41 | listenSignal(p) 42 | logger.Infof("nsqproxy end success") 43 | } 44 | 45 | // 监听信号 46 | func listenSignal(p *proxy.Proxy) { 47 | sigChannel := make(chan os.Signal) 48 | signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGTRAP) 49 | for { 50 | sig := <-sigChannel 51 | logger.Infof("nsqproxy receive signal: %s", sig.String()) 52 | if sig == syscall.SIGTRAP { 53 | continue 54 | } 55 | logger.Infof("nsqproxy is closing consumes...") 56 | p.SetExitFlag() 57 | p.Stop() 58 | //等待10秒 59 | logger.Infof("nsqproxy will be closed master process ten seconds later.") 60 | 61 | time.Sleep(10) 62 | //time.Sleep(10 * time.Second) 63 | break 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /web/vue-admin/src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /web/vue-admin/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | import Layout from '@/layout' 7 | 8 | export const constantRoutes = [ 9 | { 10 | path: '/login', 11 | component: () => import('@/views/login/index'), 12 | hidden: true 13 | }, 14 | 15 | { 16 | path: '/404', 17 | component: () => import('@/views/404'), 18 | hidden: true 19 | }, 20 | 21 | { 22 | path: '/', 23 | component: Layout, 24 | redirect: '/consumeconfig', 25 | name: 'NSQ消费者配置', 26 | meta: { title: 'NSQ消费者配置', icon: 'form' }, 27 | children: [ 28 | { 29 | path: '/consumeconfig', 30 | name: 'NSQ消费者配置', 31 | component: () => import('@/views/consumeconfig/index'), 32 | meta: { title: 'NSQ消费者配置', icon: 'form' } 33 | }, 34 | { 35 | path: '/consumeServerMap/:id', 36 | name: '消费者的work机权重', 37 | component: () => import('@/views/consumeconfig/weight'), 38 | hidden: true, 39 | meta: { title: '消费者的work机权重', icon: 'user' } 40 | } 41 | ] 42 | }, 43 | 44 | { 45 | path: '/workerserver', 46 | component: Layout, 47 | children: [{ 48 | path: '/workerserver', 49 | name: 'NSQ消费者服务器列表', 50 | component: () => import('@/views/workerserver/index'), 51 | meta: { title: 'NSQ消费者服务器列表', icon: 'table' } 52 | }] 53 | }, 54 | // 404 page must be placed at the end !!! 55 | { path: '*', redirect: '/404', hidden: true } 56 | ] 57 | 58 | const createRouter = () => new Router({ 59 | // mode: 'history', // require service support 60 | scrollBehavior: () => ({ y: 0 }), 61 | routes: constantRoutes 62 | }) 63 | 64 | const router = createRouter() 65 | 66 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 67 | export function resetRouter() { 68 | const newRouter = createRouter() 69 | router.matcher = newRouter.matcher // reset router 70 | } 71 | 72 | export default router 73 | -------------------------------------------------------------------------------- /internal/worker/worker_test.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "errors" 5 | "github.com/nsqio/go-nsq" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewWorker_IsError(t *testing.T) { 11 | if !IsErrorConnect(newWorkerErrorConnect(errors.New("hello world"))) { 12 | t.Fatal("IsErrorConnect failed") 13 | } 14 | if !IsErrorWrite(newWorkerErrorWrite(errors.New("hello world"))) { 15 | t.Fatal("IsErrorWrite failed") 16 | } 17 | if !IsErrorRead(newWorkerErrorRead(errors.New("hello world"))) { 18 | t.Fatal("IsErrorRead failed") 19 | } 20 | } 21 | 22 | func TestNewWorker(t *testing.T) { 23 | //构造消息 24 | messageId := nsq.MessageID([16]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'}) 25 | body := []byte("Hello world") 26 | message := nsq.NewMessage(messageId, body) 27 | 28 | //HTTP 29 | handler, err := NewWorker("127.0.0.1:80", "HtTp", "index.php", 1*time.Second, 1*time.Second, 1*time.Second) 30 | if err != nil { 31 | t.Fatalf("http NewWorker error: %s", err.Error()) 32 | } 33 | res, err := handler.Send(message) 34 | if err != nil { 35 | t.Fatalf("http send error: %s", err.Error()) 36 | } 37 | if string(res) != "200 ok" { 38 | t.Fatalf("http response body is not match") 39 | } 40 | 41 | //FastCGI 42 | handler, err = NewWorker("127.0.0.1:9000", "FaStCgI", "/var/www/index.php", 1*time.Second, 1*time.Second, 1*time.Second) 43 | if err != nil { 44 | t.Fatalf("fastcgi NewWorker error: %s", err.Error()) 45 | } 46 | res, err = handler.Send(message) 47 | if err != nil { 48 | t.Fatalf("fastcgi send error: %s", err.Error()) 49 | } 50 | if string(res) != "200 ok" { 51 | t.Fatalf("fastcgi response body is not match") 52 | } 53 | 54 | //CBNSQ 55 | handler, err = NewWorker("127.0.0.1:19910", "CbNsQ", "", 1*time.Second, 1*time.Second, 1*time.Second) 56 | if err != nil { 57 | t.Fatalf("cbnsq NewWorker error: %s", err.Error()) 58 | } 59 | res, err = handler.Send(message) 60 | if err != nil { 61 | t.Fatalf("cbnsq send error: %s", err.Error()) 62 | } 63 | if string(res) != "200 ok" { 64 | t.Fatalf("cbnsq response body is not match") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /web/vue-admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-template", 3 | "version": "4.4.0", 4 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", 5 | "author": "Pan ", 6 | "scripts": { 7 | "dev": "vue-cli-service serve", 8 | "build:prod": "vue-cli-service build", 9 | "build:stage": "vue-cli-service build --mode staging", 10 | "preview": "node build/index.js --preview", 11 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit" 15 | }, 16 | "dependencies": { 17 | "axios": "0.18.1", 18 | "core-js": "3.6.5", 19 | "element-ui": "2.13.2", 20 | "js-cookie": "2.2.0", 21 | "moment": "^2.29.1", 22 | "normalize.css": "7.0.0", 23 | "nprogress": "0.2.0", 24 | "path-to-regexp": "2.4.0", 25 | "vue": "2.6.10", 26 | "vue-router": "3.0.6", 27 | "vuex": "3.1.0" 28 | }, 29 | "devDependencies": { 30 | "@vue/cli-plugin-babel": "4.4.4", 31 | "@vue/cli-plugin-eslint": "4.4.4", 32 | "@vue/cli-plugin-unit-jest": "4.4.4", 33 | "@vue/cli-service": "4.4.4", 34 | "@vue/test-utils": "1.0.0-beta.29", 35 | "autoprefixer": "9.5.1", 36 | "babel-eslint": "10.1.0", 37 | "babel-jest": "23.6.0", 38 | "babel-plugin-dynamic-import-node": "2.3.3", 39 | "chalk": "2.4.2", 40 | "connect": "3.6.6", 41 | "eslint": "6.7.2", 42 | "eslint-plugin-vue": "6.2.2", 43 | "html-webpack-plugin": "3.2.0", 44 | "mockjs": "1.0.1-beta3", 45 | "runjs": "4.3.2", 46 | "sass": "1.26.8", 47 | "sass-loader": "8.0.2", 48 | "script-ext-html-webpack-plugin": "2.1.3", 49 | "serve-static": "1.13.2", 50 | "svg-sprite-loader": "4.1.3", 51 | "svgo": "1.2.2", 52 | "vue-template-compiler": "2.6.10" 53 | }, 54 | "browserslist": [ 55 | "> 1%", 56 | "last 2 versions" 57 | ], 58 | "engines": { 59 | "node": ">=8.9", 60 | "npm": ">= 3.0.0" 61 | }, 62 | "license": "MIT" 63 | } 64 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /document/protocol/cbnsq.md: -------------------------------------------------------------------------------- 1 | ## CBNSQ 协议 2 | 3 | - 协议描述 4 | 5 | > 自定义的基于TCP的文本协议。 6 | 7 | - CBNSQ协议是自定义的非常简单的一个基于文本的TCP的协议。 8 | - 就是消息长度 + 消息ID + 消息内容。 msg的长度(8字节,0填充) + messageId(16字节) + msg正文 9 | - 如:有个用户注册的消息是:{"topic":"test","classname":"UserService","methodname":"addUser","param":["userid", "username", "password"],"addtime":"2020-11-27 14:30:34"} 10 | - 第一部分:消息长度。消息主体是个json,长度140,补齐8位,那么这部分为00000140。8位的极限是99999999/1024/1024≈95M,足够了吧。 11 | - 第二部分:消息ID,这个是NSQ的消息唯一ID,16位。如qwertyuiopasdfgh。注意:这16位是不计入第一部分的消息长度的。 12 | - 第三部分:消息主体。即刚才提到的JSON串。 13 | - 完整的消息为:00000140qwertyuiopasdfgh{"topic":"test","classname":"UserService","methodname":"addUser","param":["userid", "username", "password"],"addtime":"2020-11-27 14:30:34"} 14 | 15 | ``` 16 | 消息ID:消息体(从0开始)的第8位到第23位。 17 | 消息正文:消息体(从0开始)的第24位到[消息长度],[消息长度]为消息体的第0位到第7位。 18 | ``` 19 | 20 | ### PHP使用示例 21 | 22 | ```php 23 | childProcessCount = 10; 52 | 53 | //设置MeepoPS实例名称 54 | $cbNsq->instanceName = 'MeepoPS-CBSNQ'; 55 | 56 | //设置回调函数 - 这是所有应用的业务代码入口 57 | $cbNsq->callbackNewData = 'callbackNewData'; 58 | 59 | //启动MeepoPS 60 | \MeepoPS\runMeepoPS(); 61 | 62 | 63 | //以下为回调函数, 业务相关. 64 | //回调 - 收到新消息 65 | function callbackNewData($connect, $data) 66 | { 67 | file_put_contents('../cbnsq.log', date('Y-m-d H:i:s') . ' ' . $_SERVER['MESSAGE_ID'] . ' ' . $data ."\n", FILE_APPEND); 68 | $connect->send("200 ok"); 69 | } 70 | ``` -------------------------------------------------------------------------------- /web/vue-admin/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | // import store from './store' 3 | // import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | // import { getToken } from '@/utils/auth' // get token from cookie 7 | // import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | // const whiteList = ['/login'] // no redirect whitelist 12 | 13 | // router.beforeEach(async(to, from, next) => { 14 | // // start progress bar 15 | // NProgress.start() 16 | 17 | // // set page title 18 | // document.title = getPageTitle(to.meta.title) 19 | 20 | // // determine whether the user has logged in 21 | // const hasToken = getToken() 22 | 23 | // if (hasToken) { 24 | // if (to.path === '/login') { 25 | // // if is logged in, redirect to the home page 26 | // next({ path: '/' }) 27 | // NProgress.done() 28 | // } else { 29 | // const hasGetUserInfo = store.getters.name 30 | // if (hasGetUserInfo) { 31 | // next() 32 | // } else { 33 | // try { 34 | // // get user info 35 | // await store.dispatch('user/getInfo') 36 | 37 | // next() 38 | // } catch (error) { 39 | // // remove token and go to login page to re-login 40 | // await store.dispatch('user/resetToken') 41 | // Message.error(error || 'Has Error') 42 | // next(`/login?redirect=${to.path}`) 43 | // NProgress.done() 44 | // } 45 | // } 46 | // } 47 | // } else { 48 | // /* has no token*/ 49 | 50 | // if (whiteList.indexOf(to.path) !== -1) { 51 | // // in the free login whitelist, go directly 52 | // next() 53 | // } else { 54 | // // other pages that do not have permission to access are redirected to the login page. 55 | // next(`/login?redirect=${to.path}`) 56 | // NProgress.done() 57 | // } 58 | // } 59 | // }) 60 | 61 | router.afterEach(() => { 62 | // finish progress bar 63 | NProgress.done() 64 | }) 65 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/table/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 72 | -------------------------------------------------------------------------------- /internal/proxy/loadbalance_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/changba/nsqproxy/internal/model" 5 | "testing" 6 | ) 7 | 8 | func init() { 9 | model.NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 10 | } 11 | 12 | func TestNewLoadBalance(t *testing.T) { 13 | p := NewProxy() 14 | if len(p.consumeConfigList) <= 0 || len(p.consumeConfigList[0].ServerList) <= 0 { 15 | t.Fatal("consume workerlist为空") 16 | } 17 | loadbalance := newLoadBalance(LoadBalanceMethodLoop, p.consumeConfigList[0].ServerList) 18 | if loadbalance.method != LoadBalanceMethodLoop || loadbalance.dispatcher == nil { 19 | t.Fatal("newLoadBalance failed") 20 | } 21 | } 22 | 23 | func TestLoadBalance_pickWorker(t *testing.T) { 24 | p := NewProxy() 25 | loadbalance := newLoadBalance(LoadBalanceMethodLoop, p.consumeConfigList[0].ServerList) 26 | work, err := loadbalance.pickWorker() 27 | if err != nil { 28 | t.Fatalf("pickWorker error: %s", err.Error()) 29 | } 30 | if work.Id <= 0 || work.Weight <= 0 || len(work.WorkServer.Addr) <= 0 || len(work.WorkServer.Protocol) <= 0 { 31 | t.Fatalf("pickWorker error: no available work") 32 | } 33 | } 34 | 35 | func TestLoadBalanceLoop_new(t *testing.T) { 36 | p := NewProxy() 37 | loop := &loadBalanceLoop{} 38 | loop.new(p.consumeConfigList[0].ServerList) 39 | if len(loop.list) <= 0 { 40 | t.Fatalf("loop.list is empty") 41 | } 42 | //检验capacity是否相等 43 | capacity := 0 44 | //检验生成的list中,每台机器的占比是否相等 45 | list := make(map[int]int, 0) 46 | for _, v := range p.consumeConfigList[0].ServerList { 47 | capacity += v.Weight * 100 48 | list[v.Id] = v.Weight * 100 49 | } 50 | if loop.capacity != capacity { 51 | t.Fatalf("loop.capacity is not match. action:%d expect:%d", loop.capacity, capacity) 52 | } 53 | count := make(map[int]int, 0) 54 | for _, v := range loop.list { 55 | count[v.Id] = v.Weight * 100 56 | } 57 | if len(count) != len(list) { 58 | t.Fatalf("loop.list length does not match. action:%d expect:%d", len(count), len(list)) 59 | } 60 | for k, v := range list { 61 | if _, ok := count[k]; !ok { 62 | t.Fatalf("the id %d of loop.list not exists", k) 63 | } 64 | if v != count[k] { 65 | t.Fatalf("the id %d total of loop.list does not match. action:%d expect:%d", k, count[k], v) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/worker/cbnsq.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "github.com/nsqio/go-nsq" 8 | "net" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // CBNSQ协议是自定义的非常简单的一个基于文本的TCP的协议。 14 | // 就是消息长度 + 消息内容。 msg的长度(8字节,0填充) + messageId(16字节) + msg正文 15 | // 如:有个用户注册的消息是:{"topic":"test","classname":"UserService","methodname":"addUser","param":["userid", "username", "password"],"addtime":"2020-11-27 14:30:34"} 16 | // 第一部分:消息长度。消息主体是个json,长度140,补齐8位,那么这部分为00000140。8位的极限是99999999/1024/1024≈95M,足够了吧。 17 | // 第二部分:消息ID,这个是NSQ的消息唯一ID,16位。如qwertyuiopasdfgh。注意:这16位是不计入第一部分的消息长度的。 18 | // 第三部分:消息主体。即刚才提到的JSON串。 19 | // 完整的消息为:00000140qwertyuiopasdfgh{"topic":"test","classname":"UserService","methodname":"addUser","param":["userid", "username", "password"],"addtime":"2020-11-27 14:30:34"} 20 | 21 | type CBNSQWorker struct { 22 | workerConfig workerConfig 23 | } 24 | 25 | func (w *CBNSQWorker) new(wc workerConfig) { 26 | w.workerConfig = wc 27 | } 28 | 29 | func (w *CBNSQWorker) Send(message *nsq.Message) ([]byte, error) { 30 | //连接到worker 31 | conn, err := net.DialTimeout("tcp", w.workerConfig.addr, w.workerConfig.timeoutDial) 32 | if err != nil { 33 | return nil, newWorkerErrorConnect(err) 34 | } 35 | //设置连接的读写超时时间 36 | _ = conn.SetWriteDeadline(time.Now().Add(w.workerConfig.timeoutWrite)) 37 | _ = conn.SetReadDeadline(time.Now().Add(w.workerConfig.timeoutRead)) 38 | defer conn.Close() 39 | //给worker发送数据 40 | data := w.encode(message) 41 | n, err := conn.Write(data) 42 | if n == 0 { 43 | return nil, newWorkerErrorWrite(errors.New("n of conn.Write is 0")) 44 | } 45 | if err != nil { 46 | return nil, newWorkerErrorWrite(errors.New("conn.Write" + err.Error())) 47 | } 48 | //从worker读取响应 49 | buf := make([]byte, 128) 50 | n, err = conn.Read(buf) 51 | if err != nil { 52 | return nil, newWorkerErrorRead(errors.New("conn.Read" + err.Error())) 53 | } 54 | if n == 0 { 55 | return nil, newWorkerErrorRead(errors.New("response length is 0")) 56 | } 57 | return buf[:n], nil 58 | } 59 | 60 | // CBNSQ协议打包数据 61 | // msg的长度(8字节,0填充) + messageId(16字节) + msg正文 62 | func (w *CBNSQWorker) encode(message *nsq.Message) []byte { 63 | header := fmt.Sprintf("%08s", strconv.Itoa(len(message.Body))) 64 | return bytes.Join([]([]byte){[]byte(header), message.ID[:], message.Body}, []byte("")) 65 | } 66 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | 53 | 94 | -------------------------------------------------------------------------------- /web/vue-admin/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /internal/backup/live.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/changba/nsqproxy/internal/module/logger" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | //主库返回的响应值 12 | type masterResponse struct { 13 | Code int `json:"code"` 14 | Message string `json:"msg"` 15 | Result interface{} `json:"result"` 16 | } 17 | 18 | //向主发送心跳,主观判断主下线后,备才会启动,否则在本函数阻塞 19 | //成功:成功一次就算成功 20 | //失败:连续失败五次算失败。 21 | //即使发生分区、网络阻塞等原因主被错误的判断为失败,此时主备会同时在线,但我们认为,主备同时在线,并不会造成消息的丢失(但是极限下会加快两倍消费速度)。 22 | //考虑引入多节点的哨兵判断,上升复杂度,不如就这样简单做。 23 | func Backup(masterAddr string) { 24 | //主的空,则忽略 25 | if masterAddr == "" { 26 | return 27 | } 28 | //访问主机失败次数 29 | failedNum := 0 30 | successInterval := 15 * time.Second 31 | failedInterval := 2 * time.Second 32 | interval := successInterval 33 | for { 34 | resp, err := http.Get("http://" + masterAddr + "/status") 35 | var body []byte 36 | var r masterResponse 37 | if err != nil { 38 | interval = failedInterval 39 | failedNum++ 40 | logger.Errorf("the backup connect master error: %s", err.Error()) 41 | logger.Errorf("failed to access the master %d times", failedNum) 42 | goto SLEEP 43 | } 44 | body, err = ioutil.ReadAll(resp.Body) 45 | if err != nil { 46 | interval = failedInterval 47 | failedNum++ 48 | logger.Errorf("the backup read response of master error: %s", err.Error()) 49 | logger.Errorf("failed to access the master %d times", failedNum) 50 | goto SLEEP 51 | } 52 | err = json.Unmarshal(body, &r) 53 | if err != nil { 54 | interval = failedInterval 55 | failedNum++ 56 | logger.Errorf("the backup change response of master to json error: %s", err.Error()) 57 | logger.Errorf("failed to access the master %d times", failedNum) 58 | goto SLEEP 59 | } 60 | _ = resp.Body.Close() 61 | if r.Code != 200 || r.Result != "ok" { 62 | interval = failedInterval 63 | failedNum++ 64 | logger.Errorf("the backup read response of master failed. action:%d %s expect:%d %s", r.Code, r.Result, 200, "ok") 65 | logger.Errorf("failed to access the master %d times", failedNum) 66 | goto SLEEP 67 | } 68 | logger.Infof("the master is normal. the backup is keeping on block.") 69 | interval = successInterval 70 | failedNum = 0 71 | 72 | SLEEP: 73 | if failedNum >= 5 { 74 | logger.Errorf("the backup will run.") 75 | break 76 | } 77 | time.Sleep(interval) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /web/vue-admin/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /web/vue-admin/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/module/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | //log库 4 | 5 | import ( 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type LogLevel int 12 | 13 | const ( 14 | LOG_DEBUG LogLevel = iota 15 | LOG_INFO 16 | LOG_WARNING 17 | LOG_ERROR 18 | LOG_FATAL 19 | ) 20 | 21 | func (ll LogLevel) String() string { 22 | switch ll { 23 | case LOG_DEBUG: 24 | return "debug" 25 | case LOG_INFO: 26 | return "info" 27 | case LOG_WARNING: 28 | return "warning" 29 | case LOG_ERROR: 30 | return "error" 31 | case LOG_FATAL: 32 | return "fatal" 33 | default: 34 | return "info" 35 | } 36 | } 37 | 38 | func getLevelByString(levelString string) LogLevel { 39 | switch strings.ToLower(levelString) { 40 | case "debug": 41 | return LOG_DEBUG 42 | case "info": 43 | return LOG_INFO 44 | case "warning": 45 | return LOG_WARNING 46 | case "error": 47 | return LOG_ERROR 48 | case "fatal": 49 | return LOG_FATAL 50 | default: 51 | return LOG_INFO 52 | } 53 | } 54 | 55 | type Logger struct { 56 | *log.Logger 57 | Level LogLevel 58 | logFile *os.File 59 | } 60 | 61 | func NewLogger(fileName string, prefix string, level string) *Logger { 62 | //目录是否存在 63 | pathSliceList := strings.Split(fileName, "/") 64 | //不是当前目录 65 | if len(pathSliceList) > 1 { 66 | pathSliceList = pathSliceList[:len(pathSliceList)-1] 67 | path := strings.Join(pathSliceList, "/") 68 | _, err := os.Stat(path) 69 | //创建目录 70 | if err != nil && os.IsNotExist(err) { 71 | err := os.Mkdir(path, os.ModePerm) 72 | if err != nil { 73 | panic("create dir failed: " + err.Error()) 74 | } 75 | } 76 | } 77 | logFile, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 78 | if err != nil { 79 | panic("open log file " + fileName + " error: " + err.Error()) 80 | } 81 | logger := log.New(logFile, prefix, log.LstdFlags) 82 | logger.SetFlags(log.Ldate | log.Ltime) 83 | return &Logger{ 84 | Logger: logger, 85 | Level: getLevelByString(level), 86 | logFile: logFile, 87 | } 88 | } 89 | 90 | func (l *Logger) WithLevelf(lev LogLevel, format string, v ...interface{}) { 91 | if l == nil || lev < l.Level { 92 | return 93 | } 94 | format = "[" + lev.String() + "]" + " " + format 95 | l.logf(format, v...) 96 | } 97 | 98 | func (l *Logger) logf(format string, v ...interface{}) { 99 | l.Printf(format, v...) 100 | } 101 | 102 | func (l *Logger) Close() bool { 103 | _ = l.logFile.Close() 104 | return true 105 | } 106 | -------------------------------------------------------------------------------- /document/doc/quick_start.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ## 简介 4 | NSQProxy是 队列NSQ 和 消费者 之间的桥梁。负责将 队列中的任务 根据管理后台的配置 通过指定的协议 分发给指定的Worker机。在唱吧内部使用2年,高效稳定的处理着每日数十亿条消息。 5 | 6 | * 队列中的任务:生产者入队到NSQ的数据。 7 | * 管理后台的配置:每个Topic/Channel都可以配置在哪台机器上消费、同时消费的并发量是几个、是否重新入队、积压多少个开始报警等。 8 | * 通过指定的协议:支持HTTP、FastCGI、CBNSQ等协议将数据发送给Worker机。 9 | * Worker机:执行消费者程序的服务器。 10 | * 数据流转:生产者 -> NSQ -> NSQProxy -> 消费者 11 | 12 | > 本文档以HTTP作为通信协议,编写一个go代码作为消费者。 13 | 14 | ## 启动依赖 15 | NSQProxy是一个中间转发器,因此需要上下游依赖。 16 | 17 | #### 依赖NSQ 18 | NSQ是队列服务,因此NSQProxy的上游是NSQ。NSQ会将任务下发给NSQProxy,站在NSQ的视角中,NSQProxy是它的消费者。 19 | 20 | * 启动NSQLookupd `nsqlookupd -broadcast-address="0.0.0.0" -http-address="0.0.0.0:4161" -tcp-address="0.0.0.0:4160"` 21 | * 启动NSQD `nsqd -broadcast-address="0.0.0.0" -lookupd-tcp-address="0.0.0.0:4160" -tcp-address="0.0.0.0:4150" -http-address="0.0.0.0:4151"` 22 | 23 | #### 依赖MySQL 24 | MySQL中存储着各Topic/Channel的配置信息,因此NSQProxy依赖于MySQL。 25 | 26 | 启动MySQL的方式多种多样,如`mysqld` 和 `service mysql start`等等。 27 | 28 | ## 下载安装 29 | 下载并启动NSQProxy。 30 | 31 | * 下载最新版本的压缩包 https://github.com/changba/nsqproxy/releases 32 | * 解压 33 | * 启动(注意替换为自己的MySQL信息) `./nsqproxy -dbHost=127.0.0.1 -dbPort=3306 -dbUsername=root -dbPassword=rootpsd -dbName=nsqproxy -logLevel=debug -nsqlookupdHTTP=127.0.0.1:4161` 34 | * 命令行 `curl http://0.0.0.0:19421/status` 输出ok 35 | * 浏览器打开 http://0.0.0.0:19421/admin 36 | 37 | ## 部署消费者 38 | 本文编写一个go代码作为消费者,这个go代码使用HTTP协议监听8888端口。 39 | ```golang 40 | package main 41 | 42 | import ( 43 | "fmt" 44 | "io/ioutil" 45 | "net/http" 46 | ) 47 | 48 | // 启动HTTP 49 | func main() { 50 | http.HandleFunc("/nsqTask", func(w http.ResponseWriter, r *http.Request) { 51 | fmt.Println("MessageID:" + r.Header.Get("MESSAGE_ID")) 52 | data, _ := ioutil.ReadAll(r.Body) 53 | fmt.Println("MessageBody:" + string(data)) 54 | _, _ = w.Write([]byte("200 ok")) 55 | }) 56 | if err := http.ListenAndServe("0.0.0.0:8888", nil); err != nil { 57 | panic("ListenAndServe error: " + err.Error()) 58 | } 59 | } 60 | ``` 61 | 62 | 运行 `go run test.go` 63 | 64 | ## 后台操作 65 | 把Topic和消费者关联起来。浏览器打开 http://0.0.0.0:19421/admin 66 | 67 | 1、添加Worker机 68 | 69 | ![添加Worker机](../../assets/images/quick_start_add_work_server.png) 70 | 71 | 2、添加新消费者配置 72 | 73 | ![添加新消费者配置](../../assets/images/quick_start_add_consume_config.png) 74 | 75 | 3、把消费者和Worker机关联起来 76 | 77 | ![把消费者和Worker机关联起来](../../assets/images/quick_start_add_consume_server_map.png) 78 | 79 | ## 具体示例 80 | 81 | 此时,我们给NSQ入队,NSQ就会把消息推给NSQProxy,NSQProxy根据刚才的配置,就会把消息推送给0.0.0.0:8888的Golang程序。 82 | 83 | 1、入队给NSQ `curl -d 'name=xiaoming&sex=male&age=18' 'http://0.0.0.0:4151/pub?topic=test_topic'` 84 | 85 | 2、查看刚才编写的Golang程序的输出,可以拿到消息的唯一ID和消息的内容 86 | 87 | 具体示例 -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 35 | 36 | 114 | -------------------------------------------------------------------------------- /web/vue-admin/src/views/form/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 79 | 80 | 85 | 86 | -------------------------------------------------------------------------------- /internal/module/tool/guid.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | // the core algorithm here was borrowed from: 4 | // Blake Mizerany's `noeqd` https://github.com/bmizerany/noeqd 5 | // and indirectly: 6 | // Twitter's `snowflake` https://github.com/twitter/snowflake 7 | 8 | // only minor cleanup and changes to introduce a type, combine the concept 9 | // of workerID + datacenterId into a single identifier, and modify the 10 | // behavior when sequences rollover for our specific implementation needs 11 | 12 | import ( 13 | "encoding/hex" 14 | "errors" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | func GenerateUniqueId(node int64) [16]byte { 20 | retry: 21 | id, err := NewGUIDFactory(node).NewGUID() 22 | if err != nil { 23 | time.Sleep(time.Millisecond) 24 | goto retry 25 | } 26 | return id.Hex() 27 | } 28 | 29 | const ( 30 | nodeIDBits = uint64(10) 31 | sequenceBits = uint64(12) 32 | nodeIDShift = sequenceBits 33 | timestampShift = sequenceBits + nodeIDBits 34 | sequenceMask = int64(-1) ^ (int64(-1) << sequenceBits) 35 | 36 | // ( 2012-10-28 16:23:42 UTC ).UnixNano() >> 20 37 | twepoch = int64(1288834974288) 38 | ) 39 | 40 | var ErrTimeBackwards = errors.New("time has gone backwards") 41 | var ErrSequenceExpired = errors.New("sequence expired") 42 | var ErrIDBackwards = errors.New("ID went backward") 43 | 44 | type guid int64 45 | 46 | type guidFactory struct { 47 | sync.Mutex 48 | 49 | nodeID int64 50 | sequence int64 51 | lastTimestamp int64 52 | lastID guid 53 | } 54 | 55 | func NewGUIDFactory(nodeID int64) *guidFactory { 56 | return &guidFactory{ 57 | nodeID: nodeID, 58 | } 59 | } 60 | 61 | func (f *guidFactory) NewGUID() (guid, error) { 62 | f.Lock() 63 | 64 | // divide by 1048576, giving pseudo-milliseconds 65 | ts := time.Now().UnixNano() >> 20 66 | 67 | if ts < f.lastTimestamp { 68 | f.Unlock() 69 | return 0, ErrTimeBackwards 70 | } 71 | 72 | if f.lastTimestamp == ts { 73 | f.sequence = (f.sequence + 1) & sequenceMask 74 | if f.sequence == 0 { 75 | f.Unlock() 76 | return 0, ErrSequenceExpired 77 | } 78 | } else { 79 | f.sequence = 0 80 | } 81 | 82 | f.lastTimestamp = ts 83 | 84 | id := guid(((ts - twepoch) << timestampShift) | 85 | (f.nodeID << nodeIDShift) | 86 | f.sequence) 87 | 88 | if id <= f.lastID { 89 | f.Unlock() 90 | return 0, ErrIDBackwards 91 | } 92 | 93 | f.lastID = id 94 | 95 | f.Unlock() 96 | 97 | return id, nil 98 | } 99 | 100 | func (g guid) Hex() [16]byte { 101 | var h [16]byte 102 | var b [8]byte 103 | 104 | b[0] = byte(g >> 56) 105 | b[1] = byte(g >> 48) 106 | b[2] = byte(g >> 40) 107 | b[3] = byte(g >> 32) 108 | b[4] = byte(g >> 24) 109 | b[5] = byte(g >> 16) 110 | b[6] = byte(g >> 8) 111 | b[7] = byte(g) 112 | 113 | hex.Encode(h[:], b[:]) 114 | return h 115 | } 116 | -------------------------------------------------------------------------------- /web/vue-admin/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /web/vue-admin/tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /internal/model/consumeservermap.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | type ConsumeServerMap struct { 9 | Id int `json:"id" gorm:"primaryKey"` 10 | Consumeid int `json:"consumeid"` 11 | Serverid int `json:"serverid"` 12 | Weight int `json:"weight"` 13 | Invalid int `json:"invalid"` 14 | //创建时间 15 | CreatedAt time.Time `json:"createdAt"` 16 | //更新时间 17 | UpdatedAt time.Time `json:"updatedAt"` 18 | 19 | WorkServer WorkServer `json:"workServer" gorm:"-"` 20 | } 21 | 22 | func (ConsumeServerMap) TableName() string { 23 | return "nsqproxy_consume_server_map" 24 | } 25 | 26 | func (ConsumeServerMap) CreateTable() error { 27 | sql := "CREATE TABLE IF NOT EXISTS `nsqproxy_consume_server_map` (" + 28 | "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," + 29 | "`consumeid` int(11) NOT NULL COMMENT '消费者id'," + 30 | "`serverid` int(11) NOT NULL COMMENT '服务器id'," + 31 | "`weight` int(11) DEFAULT '0' COMMENT '权重'," + 32 | "`invalid` tinyint(4) DEFAULT '0' COMMENT '是否有效,0是有效'," + 33 | "`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间'," + 34 | "`updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间'," + 35 | "PRIMARY KEY (`id`)," + 36 | "UNIQUE KEY `index_uq_cid_sid` (`consumeid`,`serverid`)" + 37 | ") ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='消费者和可消费的服务器之间的关联关系';" 38 | return db.Exec(sql).Error 39 | } 40 | 41 | //两份配置是否相等 42 | func (m ConsumeServerMap) IsEqual(newMap ConsumeServerMap) bool { 43 | if m.Id != newMap.Id || m.Consumeid != newMap.Consumeid || m.Serverid != newMap.Serverid || m.Weight != newMap.Weight || m.Invalid != newMap.Invalid { 44 | return false 45 | } 46 | if !m.WorkServer.IsEqual(newMap.WorkServer) { 47 | return false 48 | } 49 | return true 50 | } 51 | 52 | func (m *ConsumeServerMap) Create() (int, error) { 53 | result := db.Create(m) 54 | if result.Error != nil { 55 | return 0, result.Error 56 | } else if result.RowsAffected <= 0 { 57 | return 0, errors.New("RowsAffected is zero") 58 | } else if m.Id <= 0 { 59 | return 0, errors.New("primaryKey is zero") 60 | } 61 | return m.Id, nil 62 | } 63 | 64 | func (m *ConsumeServerMap) Delete() (int64, error) { 65 | if m.Id <= 0 { 66 | return 0, errors.New("primaryKey is zero") 67 | } 68 | result := db.Delete(m, m.Id) 69 | return result.RowsAffected, result.Error 70 | } 71 | 72 | func (m *ConsumeServerMap) Update() (int64, error) { 73 | if m.Id <= 0 { 74 | return 0, errors.New("primaryKey is zero") 75 | } 76 | result := db.Select("Id", "Consumeid", "Serverid", "Weight", "Invalid", "UpdatedAt").Updates(m) 77 | if result.Error != nil { 78 | return 0, result.Error 79 | } 80 | return result.RowsAffected, nil 81 | } 82 | 83 | func (m *ConsumeServerMap) Get() (int64, error) { 84 | if m.Id <= 0 { 85 | return 0, errors.New("primaryKey is zero") 86 | } 87 | result := db.First(m) 88 | return result.RowsAffected, result.Error 89 | } 90 | 91 | func (m *ConsumeServerMap) AllByConsumeid(consumeid int) ([]ConsumeServerMap, error) { 92 | mList := make([]ConsumeServerMap, 0) 93 | result := db.Where("consumeid = ?", consumeid).Find(&mList) 94 | return mList, result.Error 95 | } 96 | -------------------------------------------------------------------------------- /web/vue-admin/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string | null} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0 || !time) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string')) { 21 | if ((/^[0-9]+$/.test(time))) { 22 | // support "1548221490638" 23 | time = parseInt(time) 24 | } else { 25 | // support safari 26 | // https://stackoverflow.com/questions/4310953/invalid-date-in-safari 27 | time = time.replace(new RegExp(/-/gm), '/') 28 | } 29 | } 30 | 31 | if ((typeof time === 'number') && (time.toString().length === 10)) { 32 | time = time * 1000 33 | } 34 | date = new Date(time) 35 | } 36 | const formatObj = { 37 | y: date.getFullYear(), 38 | m: date.getMonth() + 1, 39 | d: date.getDate(), 40 | h: date.getHours(), 41 | i: date.getMinutes(), 42 | s: date.getSeconds(), 43 | a: date.getDay() 44 | } 45 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { 46 | const value = formatObj[key] 47 | // Note: getDay() returns 0 on Sunday 48 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 49 | return value.toString().padStart(2, '0') 50 | }) 51 | return time_str 52 | } 53 | 54 | /** 55 | * @param {number} time 56 | * @param {string} option 57 | * @returns {string} 58 | */ 59 | export function formatTime(time, option) { 60 | if (('' + time).length === 10) { 61 | time = parseInt(time) * 1000 62 | } else { 63 | time = +time 64 | } 65 | const d = new Date(time) 66 | const now = Date.now() 67 | 68 | const diff = (now - d) / 1000 69 | 70 | if (diff < 30) { 71 | return '刚刚' 72 | } else if (diff < 3600) { 73 | // less 1 hour 74 | return Math.ceil(diff / 60) + '分钟前' 75 | } else if (diff < 3600 * 24) { 76 | return Math.ceil(diff / 3600) + '小时前' 77 | } else if (diff < 3600 * 24 * 2) { 78 | return '1天前' 79 | } 80 | if (option) { 81 | return parseTime(time, option) 82 | } else { 83 | return ( 84 | d.getMonth() + 85 | 1 + 86 | '月' + 87 | d.getDate() + 88 | '日' + 89 | d.getHours() + 90 | '时' + 91 | d.getMinutes() + 92 | '分' 93 | ) 94 | } 95 | } 96 | 97 | /** 98 | * @param {string} url 99 | * @returns {Object} 100 | */ 101 | export function param2Obj(url) { 102 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 103 | if (!search) { 104 | return {} 105 | } 106 | const obj = {} 107 | const searchArr = search.split('&') 108 | searchArr.forEach(v => { 109 | const index = v.indexOf('=') 110 | if (index !== -1) { 111 | const name = v.substring(0, index) 112 | const val = v.substring(index + 1, v.length) 113 | obj[name] = val 114 | } 115 | }) 116 | return obj 117 | } 118 | -------------------------------------------------------------------------------- /document/api/admin/api/consumeconfig/worklist.md: -------------------------------------------------------------------------------- 1 | ### 查询一个消费者关联服务器 2 | 3 | - 接口功能 4 | 5 | > 根据消费者ID,查询消费者所关联的所有服务器列表 6 | 7 | ``` 8 | GET /admin/api/consumeConfig/workList 9 | ``` 10 | 11 | - 请求参数 12 | 13 | |参数|必选|类型|说明| 14 | |:----- |:-------|:-----|----- | 15 | |id |true |int |消费者ID | 16 | 17 | - 返回结果 18 | 19 | ``` 20 | { 21 | code: 200, 22 | msg: "ok", 23 | result: { 24 | id: 1, 25 | topic: "test_topic", 26 | channel: "one", 27 | description: "描述", 28 | owner: "责任人", 29 | monitorThreshold: 0, 30 | handleNum: 6, 31 | maxInFlight: 6, 32 | isRequeue: false, 33 | timeoutDial: 3590, 34 | timeoutRead: 3590, 35 | timeoutWrite: 3590, 36 | invalid: 0, 37 | createdAt: "2020-08-20T15:08:39+08:00", 38 | updatedAt: "0001-01-01T00:00:00Z", 39 | serverList: [ 40 | { 41 | id: 1, 42 | consumeid: 1, 43 | serverid: 1, 44 | weight: 1, 45 | invalid: 0, 46 | createdAt: "2018-10-16T14:36:33+08:00", 47 | updatedAt: "0001-01-01T00:00:00Z", 48 | workServer: { 49 | id: 1, 50 | addr: "1.1.1.1:80, 51 | protocol: "HTTP", 52 | extra: "", 53 | description: "通用机器1", 54 | owner: "", 55 | invalid: 0, 56 | createdAt: "2018-10-16T14:30:58+08:00", 57 | updatedAt: "0001-01-01T00:00:00Z" 58 | } 59 | }, 60 | { 61 | id: 2, 62 | consumeid: 1, 63 | serverid: 2, 64 | weight: 1, 65 | invalid: 0, 66 | createdAt: "2018-10-16T14:36:33+08:00", 67 | updatedAt: "0001-01-01T00:00:00Z", 68 | workServer: { 69 | id: 2, 70 | addr: "1.1.1.2:80, 71 | protocol: "HTTP", 72 | extra: "", 73 | description: "通用机器2", 74 | owner: "", 75 | invalid: 0, 76 | createdAt: "2018-10-16T14:30:58+08:00", 77 | updatedAt: "0001-01-01T00:00:00Z" 78 | } 79 | }, 80 | { 81 | id: 3, 82 | consumeid: 1, 83 | serverid: 3, 84 | weight: 1, 85 | invalid: 0, 86 | createdAt: "2018-10-16T14:36:33+08:00", 87 | updatedAt: "0001-01-01T00:00:00Z", 88 | workServer: { 89 | id: 3, 90 | addr: "1.1.1.3:80, 91 | protocol: "HTTP", 92 | extra: "", 93 | description: "通用机器3", 94 | owner: "", 95 | invalid: 0, 96 | createdAt: "2018-10-16T14:30:58+08:00", 97 | updatedAt: "0001-01-01T00:00:00Z" 98 | } 99 | } 100 | ] 101 | } 102 | } 103 | ``` -------------------------------------------------------------------------------- /internal/model/consumeservermap_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConsumeServerMap_CreateTable(t *testing.T) { 8 | NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 9 | m := ConsumeServerMap{} 10 | err := m.CreateTable() 11 | if err != nil { 12 | t.Fatalf("create table error: %s", err.Error()) 13 | } 14 | db, err := db.DB() 15 | if err != nil { 16 | t.Fatalf("get db error: %s", err.Error()) 17 | } 18 | err = db.Close() 19 | if err != nil { 20 | t.Fatalf("close db error: %s", err.Error()) 21 | } 22 | } 23 | 24 | func TestConsumeServerMap_CURD(t *testing.T) { 25 | NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 26 | m := ConsumeServerMap{ 27 | Consumeid: 1, 28 | Serverid: 2, 29 | Weight: 1, 30 | } 31 | //新增 32 | result := db.Table("nsqproxy_consume_server_map").Create(&m) 33 | id := m.Id 34 | if result.Error != nil { 35 | t.Fatalf("[insert]error: %s", result.Error.Error()) 36 | } 37 | if result.RowsAffected != 1 { 38 | t.Fatalf("[insert]RowsAffected is not 1. result: %d", result.RowsAffected) 39 | } 40 | if id <= 0 { 41 | t.Fatalf("[insert]id is 0") 42 | } 43 | 44 | //查询 45 | find := ConsumeServerMap{} 46 | result = db.Table("nsqproxy_consume_server_map").First(&find, id) 47 | if result.Error != nil { 48 | t.Fatalf("[select]error: %s", result.Error.Error()) 49 | } 50 | if result.RowsAffected != 1 { 51 | t.Fatalf("[select]RowsAffected is not 1. result: %d", result.RowsAffected) 52 | } 53 | 54 | //修改 55 | m.Invalid = 10 56 | m.Weight = 11 57 | result = db.Save(&m) 58 | if result.Error != nil { 59 | t.Fatalf("[update]error: %s", result.Error.Error()) 60 | } 61 | if result.RowsAffected != 1 { 62 | t.Fatalf("[update]RowsAffected is not 1. result: %d", result.RowsAffected) 63 | } 64 | if m.Id != id { 65 | t.Fatalf("[update]id has changed") 66 | } 67 | 68 | //查询 69 | find = ConsumeServerMap{} 70 | result = db.First(&find, id) 71 | if result.Error != nil { 72 | t.Fatalf("[select2]error: %s", result.Error.Error()) 73 | } 74 | if find.Invalid != 10 || find.Weight != 11 { 75 | t.Fatalf("[select2]update failed") 76 | } 77 | if result.RowsAffected != 1 { 78 | t.Fatalf("[select2]RowsAffected is not 1. result: %d", result.RowsAffected) 79 | } 80 | if !m.IsEqual(find) { 81 | t.Fatalf("[select]Query result failed") 82 | } 83 | 84 | //删除 85 | result = db.Delete(&m) 86 | if result.Error != nil { 87 | t.Fatalf("[delete]error: %s", result.Error.Error()) 88 | } 89 | if result.RowsAffected != 1 { 90 | t.Fatalf("[delete]RowsAffected is not 1. result: %d", result.RowsAffected) 91 | } 92 | 93 | //查询 94 | find = ConsumeServerMap{} 95 | result = db.First(&find, id) 96 | if find.Serverid != 0 { 97 | t.Fatalf("[select3]id is not 0") 98 | } 99 | if result.Error == nil { 100 | t.Fatalf("[select3]no error") 101 | } 102 | if result.RowsAffected != 0 { 103 | t.Fatalf("[select3]RowsAffected is not 1. result: %d", result.RowsAffected) 104 | } 105 | 106 | //关闭 107 | db, err := db.DB() 108 | if err != nil { 109 | t.Fatalf("get db error: %s", err.Error()) 110 | } 111 | err = db.Close() 112 | if err != nil { 113 | t.Fatalf("close db error: %s", err.Error()) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /internal/model/workserver_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWorkServer_CreateTable(t *testing.T) { 8 | NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 9 | w := WorkServer{} 10 | err := w.CreateTable() 11 | if err != nil { 12 | t.Fatalf("create table error: %s", err.Error()) 13 | } 14 | db, err := db.DB() 15 | if err != nil { 16 | t.Fatalf("get db error: %s", err.Error()) 17 | } 18 | err = db.Close() 19 | if err != nil { 20 | t.Fatalf("close db error: %s", err.Error()) 21 | } 22 | } 23 | 24 | func TestWorkServer_CURD(t *testing.T) { 25 | NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 26 | w := WorkServer{ 27 | Addr: "0.0.0.0:80", 28 | Protocol: "HTTP", 29 | } 30 | //新增 31 | result := db.Table("nsqproxy_work_server").Create(&w) 32 | id := w.Id 33 | if result.Error != nil { 34 | t.Fatalf("[insert]error: %s", result.Error.Error()) 35 | } 36 | if result.RowsAffected != 1 { 37 | t.Fatalf("[insert]RowsAffected is not 1. result: %d", result.RowsAffected) 38 | } 39 | if id <= 0 { 40 | t.Fatalf("[insert]serverid is 0") 41 | } 42 | 43 | //查询 44 | find := WorkServer{} 45 | result = db.Table("nsqproxy_work_server").First(&find, id) 46 | if result.Error != nil { 47 | t.Fatalf("[select]error: %s", result.Error.Error()) 48 | } 49 | if result.RowsAffected != 1 { 50 | t.Fatalf("[select]RowsAffected is not 1. result: %d", result.RowsAffected) 51 | } 52 | if !w.IsEqual(find) { 53 | t.Fatalf("[select]Query result failed") 54 | } 55 | 56 | //修改 57 | w.Addr = "0.0.0.0:81" 58 | w.Protocol = "CBNSQ" 59 | result = db.Save(&w) 60 | if result.Error != nil { 61 | t.Fatalf("[update]error: %s", result.Error.Error()) 62 | } 63 | if result.RowsAffected != 1 { 64 | t.Fatalf("[update]RowsAffected is not 1. result: %d", result.RowsAffected) 65 | } 66 | if w.Id != id { 67 | t.Fatalf("[update]id has changed") 68 | } 69 | 70 | //查询 71 | find = WorkServer{} 72 | result = db.First(&find, id) 73 | if result.Error != nil { 74 | t.Fatalf("[select2]error: %s", result.Error.Error()) 75 | } 76 | if find.Addr != "0.0.0.0:81" || find.Protocol != "CBNSQ" { 77 | t.Fatalf("[select2]update failed") 78 | } 79 | if result.RowsAffected != 1 { 80 | t.Fatalf("[select2]RowsAffected is not 1. result: %d", result.RowsAffected) 81 | } 82 | if !w.IsEqual(find) { 83 | t.Fatalf("[select2]Query result failed") 84 | } 85 | //删除 86 | result = db.Delete(&w) 87 | if result.Error != nil { 88 | t.Fatalf("[delete]error: %s", result.Error.Error()) 89 | } 90 | if result.RowsAffected != 1 { 91 | t.Fatalf("[delete]RowsAffected is not 1. result: %d", result.RowsAffected) 92 | } 93 | 94 | //查询 95 | find = WorkServer{} 96 | result = db.First(&find, id) 97 | if find.Id != 0 { 98 | t.Fatalf("[select3]id is not 0") 99 | } 100 | if result.Error == nil { 101 | t.Fatalf("[select3]no error") 102 | } 103 | if result.RowsAffected != 0 { 104 | t.Fatalf("[select3]RowsAffected is not 1. result: %d", result.RowsAffected) 105 | } 106 | 107 | //关闭 108 | db, err := db.DB() 109 | if err != nil { 110 | t.Fatalf("get db error: %s", err.Error()) 111 | } 112 | err = db.Close() 113 | if err != nil { 114 | t.Fatalf("close db error: %s", err.Error()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSQProxy 2 | NSQProxy是Golang开发的NSQ和Worker之间的中间件,根据数据库配置,负责消息转发。NSQProxy启动后,接受NSQD队列内容,然后通过HTTP/FastCGI/CBNSQ等协议转发给Worker机执行。在唱吧内部使用2年,高效稳定的处理着每日数十亿条消息。 3 | 4 | [![go report card](https://goreportcard.com/badge/github.com/changba/nsqproxy "go report card")](https://goreportcard.com/report/github.com/changba/nsqproxy) 5 | [![MIT license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/changba/nsqproxy/blob/master/LICENSE) 6 | [![Downloads](https://img.shields.io/github/downloads/changba/nsqproxy/total.svg)](https://github.com/changba/nsqproxy/releases) 7 | [![Release](https://img.shields.io/github/release/changba/nsqproxy.svg?label=Release)](https://github.com/changba/nsqproxy/releases) 8 | 9 | ## 解决的问题 10 | 11 | * 各Topic执行机器可配 12 | * 各Topic消费速度可配 13 | * 各Worker机协议可配 14 | * HTTP:将消息发送给配好的URL。 15 | * FastCGI:将消息发送给配置的服务端,如PHP-FPM。 16 | * CBNSQ:自定义的基于TCP的文本协议。 17 | * 可视化界面管理 18 | * 队列积压超出阈值报警 19 | * 散乱在各处的消费者集中化管理 20 | * 通过网络分发,无需安装.so等扩展库,因此无需修改线上环境 21 | 22 | 23 | ## 有图有真相 24 | 25 | ![流程图](assets/images/nsqproxy_flow_chart.png) 26 | 27 | ![消费者管理](assets/images/admin_consume_config.png) 28 | 29 | ![worker机管理](assets/images/admin_work_server.png) 30 | 31 | ## 使用 32 | 请先部署好NSQLookupd、NSQd、MySQL 33 | 34 | > 启动NSQLookupd `nsqlookupd -broadcast-address="0.0.0.0" -http-address="0.0.0.0:4161" -tcp-address="0.0.0.0:4160"` 35 | 36 | > 启动NSQD `nsqd -broadcast-address="0.0.0.0" -lookupd-tcp-address="0.0.0.0:4160" -tcp-address="0.0.0.0:4150" -http-address="0.0.0.0:4151"` 37 | 38 | > 启动MySQL 39 | 40 | ### 安装 41 | 42 | #### 二进制安装 43 | 44 | * 下载最新版本的压缩包 https://github.com/changba/nsqproxy/releases 45 | * 解压 46 | * 启动(注意替换为自己的MySQL信息) `./nsqproxy -dbHost=127.0.0.1 -dbPort=3306 -dbUsername=root -dbPassword=rootpsd -dbName=nsqproxy -logLevel=debug -nsqlookupdHTTP=127.0.0.1:4161` 47 | * 命令行 `curl http://0.0.0.0:19421/status` 输出ok 48 | * 浏览器打开 http://0.0.0.0:19421/admin 49 | 50 | #### 源码安装 51 | 52 | * 要求Go1.13及以上 53 | * 下载本项目 `go get github.com/changba/nsqproxy` 54 | * `cd nsqproxy` 55 | * `export GO111MODULE=on` 56 | * 编译 `make build` 57 | * 启动(注意替换为自己的MySQL信息) `./bin/nsqproxy -dbHost=127.0.0.1 -dbPort=3306 -dbUsername=root -dbPassword=rootpsd -dbName=nsqproxy -logLevel=debug -nsqlookupdHTTP=127.0.0.1:4161` 58 | * 命令行 `curl http://0.0.0.0:19421/status` 输出ok 59 | * 浏览器打开 http://0.0.0.0:19421/admin 60 | 61 | ### 快速开始 62 | 63 | * [快速体验](document/doc/quick_start.md) 64 | * [启动参数](document/doc/flag.md) 65 | * [make命令](document/doc/make.md) 66 | * [文档](document/doc/README.md) 67 | 68 | ## 二次开发 69 | 70 | ### 前端 71 | 使用VUE开发,所有源码均在/web/vue-admin目录中,开发完成后需要编译,编译后的文件存放在/web/public/目录中。使用开源项目statik将静态文件/web/public/变成一个go文件internal/statik/statik.go,这样前端的静态文件也会被我们编译到同一个二进制文件中了。 72 | 73 | * 启动go服务 `make run` 74 | * 安装VUE `make vue-install`(如果国内被墙可以使用淘宝的源进行安装:make vue-install-taobao) 75 | * 开启VUE开发环境 `make vue-dev` 76 | * 浏览器打开 http://0.0.0.0:9528/admin 77 | * 开发前端相关功能 78 | * 编译VUE `make vue-build` 79 | * 前段文件转换为一个go文件 `make statik` 80 | * 编译go服务 `make build` 81 | * 浏览器打开 http://0.0.0.0:19421/admin 82 | 83 | ### 接口文档 84 | * 通过接口对数据库增删改查:[查看接口文档](document/api/README.md) 85 | 86 | ## TODO LIST 87 | 88 | * 协议增加protobuf 89 | * 后台增加用户权限管理 90 | * 报警HOOK 91 | * 日志按天分割 92 | 93 | ## License 94 | 95 | © [Changba.com](https://changba.com), 2020~time.Now 96 | 97 | Released under the [MIT License](https://github.com/changba/nsqproxy/blob/main/LICENSE) -------------------------------------------------------------------------------- /internal/proxy/loadbalance.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | "github.com/changba/nsqproxy/internal/model" 6 | ) 7 | 8 | const LoadBalanceMethodLoop = "loop" 9 | 10 | //负载均衡分发器 11 | //目前只支持轮询 12 | type loadBalanceDispatcher interface { 13 | //初始化方法 14 | new([]model.ConsumeServerMap) 15 | //选择 16 | pick() (model.ConsumeServerMap, error) 17 | } 18 | 19 | type loadBalance struct { 20 | method string //负载均衡的方法 21 | dispatcher loadBalanceDispatcher //负载均衡分发器,具体的算法执行 者 22 | } 23 | 24 | func newLoadBalance(method string, consumeServerMapList []model.ConsumeServerMap) *loadBalance { 25 | //过滤一下,选出可用的机器 26 | list := make([]model.ConsumeServerMap, 0) 27 | for _, v := range consumeServerMapList { 28 | if v.Id <= 0 || v.Weight <= 0 || len(v.WorkServer.Addr) <= 0 || len(v.WorkServer.Protocol) <= 0 { 29 | continue 30 | } 31 | list = append(list, v) 32 | } 33 | 34 | //构造分发器,目前只支持轮询 35 | //轮询 36 | var dispatcher loadBalanceDispatcher 37 | if method == LoadBalanceMethodLoop { 38 | dispatcher = &loadBalanceLoop{} 39 | } else { 40 | method = LoadBalanceMethodLoop 41 | dispatcher = &loadBalanceLoop{} 42 | } 43 | dispatcher.new(list) 44 | 45 | return &loadBalance{ 46 | method: method, 47 | dispatcher: dispatcher, 48 | } 49 | } 50 | 51 | //选择一个work机器 - 轮询的方式根据权重选择 52 | func (l *loadBalance) pickWorker() (model.ConsumeServerMap, error) { 53 | work, err := l.dispatcher.pick() 54 | //失败了就再取一次 55 | if err != nil || work.Id <= 0 || work.Weight <= 0 || len(work.WorkServer.Addr) <= 0 || len(work.WorkServer.Protocol) <= 0 { 56 | work, err = l.dispatcher.pick() 57 | if err != nil || work.Id <= 0 || work.Weight <= 0 || len(work.WorkServer.Addr) <= 0 || len(work.WorkServer.Protocol) <= 0 { 58 | return work, errors.New("no available work") 59 | } 60 | } 61 | return work, nil 62 | } 63 | 64 | type loadBalanceLoop struct { 65 | list []model.ConsumeServerMap //机器列表 66 | capacity int //权重总分 67 | seek int 68 | } 69 | 70 | func (l *loadBalanceLoop) new(consumeServerMapList []model.ConsumeServerMap) { 71 | //所有机器的权重和 72 | capacity := 0 73 | for _, v := range consumeServerMapList { 74 | capacity += v.Weight 75 | } 76 | //生成Worker列表,按顺序使用,比rand节省CPU 77 | multiple := 100 78 | capacity *= multiple 79 | wl := make([]model.ConsumeServerMap, capacity) 80 | //填充 81 | index := 0 82 | for i := 0; i < multiple; i++ { 83 | for _, consumeServerMap := range consumeServerMapList { 84 | for j := 0; j < consumeServerMap.Weight; j++ { 85 | wl[index] = consumeServerMap 86 | index++ 87 | } 88 | } 89 | } 90 | l.list = wl 91 | l.capacity = capacity 92 | l.seek = 0 93 | } 94 | 95 | //选择一个work机器 - 轮询的方式根据权重选择 96 | func (l *loadBalanceLoop) pick() (model.ConsumeServerMap, error) { 97 | //这里无需加锁,并发问题最多导致分布不是绝对的均衡。你多执行一个我少执行一个无伤大雅。 98 | seek := l.seek 99 | if seek >= l.capacity { 100 | l.seek = 0 101 | seek = 0 102 | } 103 | work := l.list[seek] 104 | //失败了就取索引为的0,补取一次 105 | if work.Id <= 0 || work.Weight <= 0 || len(work.WorkServer.Addr) <= 0 || len(work.WorkServer.Protocol) <= 0 { 106 | l.seek = 0 107 | seek = 0 108 | work = l.list[seek] 109 | if work.Id <= 0 || work.Weight <= 0 || len(work.WorkServer.Addr) <= 0 || len(work.WorkServer.Protocol) <= 0 { 110 | return work, errors.New("no available work") 111 | } 112 | } 113 | l.seek++ 114 | return work, nil 115 | } 116 | -------------------------------------------------------------------------------- /internal/model/consumeconfig_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConsumeConfig_CreateTable(t *testing.T) { 8 | NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 9 | c := ConsumeConfig{} 10 | err := c.CreateTable() 11 | if err != nil { 12 | t.Fatalf("create table error: %s", err.Error()) 13 | } 14 | db, err := db.DB() 15 | if err != nil { 16 | t.Fatalf("get db error: %s", err.Error()) 17 | } 18 | err = db.Close() 19 | if err != nil { 20 | t.Fatalf("close db error: %s", err.Error()) 21 | } 22 | } 23 | 24 | func TestConsumeConfig_CURD(t *testing.T) { 25 | NewDB("0.0.0.0", "3306", "root", "", "nsqproxy") 26 | c := ConsumeConfig{ 27 | Topic: "golang_test_topic", 28 | Channel: "golang_test_channel", 29 | Description: "go test", 30 | Owner: "go", 31 | } 32 | //新增 33 | result := db.Table("nsqproxy_consume_config").Create(&c) 34 | id := c.Id 35 | if result.Error != nil { 36 | t.Fatalf("[insert]error: %s", result.Error.Error()) 37 | } 38 | if result.RowsAffected != 1 { 39 | t.Fatalf("[insert]RowsAffected is not 1. result: %d", result.RowsAffected) 40 | } 41 | if id <= 0 { 42 | t.Fatalf("[insert]consumeid is 0") 43 | } 44 | 45 | //查询 46 | find := ConsumeConfig{} 47 | result = db.Table("nsqproxy_consume_config").First(&find, id) 48 | if result.Error != nil { 49 | t.Fatalf("[select]error: %s", result.Error.Error()) 50 | } 51 | if result.RowsAffected != 1 { 52 | t.Fatalf("[select]RowsAffected is not 1. result: %d", result.RowsAffected) 53 | } 54 | if !c.IsEqual(find) { 55 | t.Fatalf("[select]Query result failed") 56 | } 57 | 58 | //修改 59 | c.MonitorThreshold = 100 60 | c.HandleNum = 10 61 | c.MaxInFlight = 20 62 | result = db.Save(&c) 63 | if result.Error != nil { 64 | t.Fatalf("[update]error: %s", result.Error.Error()) 65 | } 66 | if result.RowsAffected != 1 { 67 | t.Fatalf("[update]RowsAffected is not 1. result: %d", result.RowsAffected) 68 | } 69 | if c.Id != id { 70 | t.Fatalf("[update]id has changed") 71 | } 72 | 73 | //查询 74 | find = ConsumeConfig{} 75 | result = db.First(&find, id) 76 | if result.Error != nil { 77 | t.Fatalf("[select2]error: %s", result.Error.Error()) 78 | } 79 | if find.MonitorThreshold != 100 || find.HandleNum != 10 || find.MaxInFlight != 20 { 80 | t.Fatalf("[select2]update failed") 81 | } 82 | if result.RowsAffected != 1 { 83 | t.Fatalf("[select2]RowsAffected is not 1. result: %d", result.RowsAffected) 84 | } 85 | if !c.IsEqual(find) { 86 | t.Fatalf("[select2]Query result failed") 87 | } 88 | 89 | //删除 90 | result = db.Delete(&c) 91 | if result.Error != nil { 92 | t.Fatalf("[delete]error: %s", result.Error.Error()) 93 | } 94 | if result.RowsAffected != 1 { 95 | t.Fatalf("[delete]RowsAffected is not 1. result: %d", result.RowsAffected) 96 | } 97 | 98 | //查询 99 | find = ConsumeConfig{} 100 | result = db.First(&find, id) 101 | if find.Id != 0 { 102 | t.Fatalf("[select3]id is not 0") 103 | } 104 | if result.Error == nil { 105 | t.Fatalf("[select3]no error") 106 | } 107 | if result.RowsAffected != 0 { 108 | t.Fatalf("[select3]RowsAffected is not 1. result: %d", result.RowsAffected) 109 | } 110 | 111 | //关闭 112 | db, err := db.DB() 113 | if err != nil { 114 | t.Fatalf("get db error: %s", err.Error()) 115 | } 116 | err = db.Close() 117 | if err != nil { 118 | t.Fatalf("close db error: %s", err.Error()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /config/systemconfig.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "github.com/changba/nsqproxy/internal/module/logger" 6 | "github.com/changba/nsqproxy/internal/module/tool" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const NsqproxyVersion = "1.0.0" 12 | 13 | const RoleMaster = "master" 14 | const RoleBackup = "backup" 15 | 16 | var SystemConfig = &systemConfig{} 17 | 18 | type systemConfig struct { 19 | //-----本项目相关----- 20 | //本机监听的端口 21 | HttpAddr string 22 | //主库IP端口,本机为主库时请留空。 23 | MasterAddr string 24 | 25 | //-----NSQ相关----- 26 | NsqlookupdHttpAddrList []string 27 | 28 | //-----日志相关----- 29 | //订阅的消息日志logger 30 | SubLogger *logger.Logger 31 | 32 | //-----消费者相关----- 33 | UpdateConfigInterval time.Duration 34 | 35 | //-----数据库相关----- 36 | DbHost string 37 | DbPort string 38 | DbUsername string 39 | DbPassword string 40 | DbName string 41 | 42 | //-----本机相关----- 43 | //本机IP 44 | InternalIP string 45 | 46 | //全局配置,无需自定义 47 | Role string 48 | } 49 | 50 | func NewSystemConfig() { 51 | //参数 52 | var httpAddr = flag.String("httpAddr", "0.0.0.0:19421", "监听的HTTP端口") 53 | var masterAddr = flag.String("masterAddr", "", "主库IP端口,为空则本机为主机") 54 | //nsq相关 55 | var nsqlookupdHTTP = flag.String("nsqlookupdHTTP", "127.0.0.1:4161", "nsqLookupd的HTTP地址,多个用逗号分割如'127.0.0.1:4161,127.0.0.1:4163'") 56 | //log相关 57 | var logLevel = flag.String("logLevel", "info", "日志等级,可选有debug、info、warning、error、fatal") 58 | var logPath = flag.String("logPath", "logs/proxy.log", "系统日志路径") 59 | var subLogPath = flag.String("subLogPath", "logs/sub.log", "消费log,由于量大成功消费log仅在日志等级为debug时启用") 60 | //MySQL 61 | var dbHost = flag.String("dbHost", "127.0.0.1", "MySQL的IP") 62 | var dbPort = flag.String("dbPort", "3306", "MySQL的端口") 63 | var dbUsername = flag.String("dbUsername", "root", "MySQL的账号") 64 | var dbPassword = flag.String("dbPassword", "", "MySQL的密码") 65 | var dbName = flag.String("dbName", "nsqproxy", "MySQL的库名") 66 | //消费者相关 67 | var updateConfigInterval = flag.Int64("updateConfigInterval", 60, "更新配置间隔") 68 | 69 | flag.Parse() 70 | //本机相关 71 | SystemConfig.HttpAddr = *httpAddr 72 | if len(SystemConfig.HttpAddr) <= 0 { 73 | panic("httpAddr参数缺失") 74 | } 75 | SystemConfig.MasterAddr = *masterAddr 76 | //nsqlookupd相关 77 | SystemConfig.NsqlookupdHttpAddrList = strings.Split(*nsqlookupdHTTP, ",") 78 | if len(SystemConfig.NsqlookupdHttpAddrList) <= 0 { 79 | panic("nsqlookupdHTTP 缺失") 80 | } 81 | //日志相关 82 | logLevelLower := strings.ToLower(*logLevel) 83 | logLevelList := map[string]struct{}{"debug": struct{}{}, "info": struct{}{}, "warning": struct{}{}, "error": struct{}{}, "fatal": struct{}{}} 84 | if _, ok := logLevelList[logLevelLower]; !ok { 85 | panic("logLevel可选值为debug、info、warning、error、fatal") 86 | } 87 | logger.Init(*logPath, logLevelLower) 88 | SystemConfig.SubLogger = logger.NewLogger(*subLogPath, "", logLevelLower) 89 | //数据库 90 | SystemConfig.DbHost = *dbHost 91 | SystemConfig.DbPort = *dbPort 92 | SystemConfig.DbUsername = *dbUsername 93 | SystemConfig.DbPassword = *dbPassword 94 | SystemConfig.DbName = *dbName 95 | //消费者相关 96 | SystemConfig.UpdateConfigInterval = time.Duration(*updateConfigInterval) * time.Second 97 | //版本 98 | SystemConfig.InternalIP = tool.GetInternalIP() 99 | 100 | //全局配置,无需自定义 101 | SystemConfig.Role = RoleBackup 102 | } 103 | 104 | func (s *systemConfig) Close() bool { 105 | logger.Close() 106 | s.SubLogger.Close() 107 | s = nil 108 | return true 109 | } 110 | -------------------------------------------------------------------------------- /internal/model/common.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const ( 12 | InvalidAvailable = 0 //可用 13 | InvalidUnavailable = 1 //不可用 14 | ) 15 | 16 | type PageResult struct { 17 | Total int64 `json:"total"` 18 | Page int `json:"page"` 19 | Result interface{} `json:"result"` 20 | } 21 | 22 | var db *gorm.DB 23 | 24 | func NewDB(host, port, username, password, dbname string) { 25 | var err error 26 | dsn := username + ":" + password + "@tcp(" + host + ":" + port + ")/" + dbname + "?charset=utf8&parseTime=true&loc=Local" 27 | db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) 28 | if err != nil { 29 | panic("open mysql error: " + err.Error()) 30 | } 31 | err = ConsumeConfig{}.CreateTable() 32 | if err != nil { 33 | panic("create table error: " + err.Error()) 34 | } 35 | err = ConsumeServerMap{}.CreateTable() 36 | if err != nil { 37 | panic("create table error: " + err.Error()) 38 | } 39 | err = WorkServer{}.CreateTable() 40 | if err != nil { 41 | panic("create table error: " + err.Error()) 42 | } 43 | } 44 | 45 | func IsErrRecordNotFound(err error) bool { 46 | if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { 47 | return true 48 | } 49 | return false 50 | } 51 | 52 | //获取可用的消费者列表 53 | func GetAvailableConsumeList() ([]ConsumeConfig, error) { 54 | //执行sql 55 | consumeConfigList := make([]ConsumeConfig, 0) 56 | consumeConfigListDB := make([]ConsumeConfig, 0) 57 | if db == nil { 58 | return consumeConfigList, errors.New("db is nil") 59 | } 60 | result := db.Where("invalid = ?", InvalidAvailable).Find(&consumeConfigListDB) 61 | if result.Error != nil { 62 | if IsErrRecordNotFound(result.Error) { 63 | return consumeConfigList, nil 64 | } 65 | //数据库出错,直接跳过本次查库 66 | return consumeConfigList, result.Error 67 | } 68 | if result.RowsAffected <= 0 { 69 | return consumeConfigList, nil 70 | } 71 | for _, consumeConfig := range consumeConfigListDB { 72 | consumeConfig.Consumer = nil 73 | consumeConfig.TimeoutWrite = consumeConfig.TimeoutWrite * time.Second 74 | consumeConfig.TimeoutRead = consumeConfig.TimeoutRead * time.Second 75 | consumeConfig.TimeoutDial = consumeConfig.TimeoutDial * time.Second 76 | consumeConfig.SetStatusWait() 77 | 78 | //获取消费者配置和worker机关联关系 79 | consumeServerMapList := make([]ConsumeServerMap, 0) 80 | consumeServerMapListDB := make([]ConsumeServerMap, 0) 81 | result = db.Where("consumeid = ? AND weight > 0 AND invalid = ?", consumeConfig.Id, InvalidAvailable).Find(&consumeServerMapListDB) 82 | if result.Error != nil || result.RowsAffected <= 0 || len(consumeServerMapListDB) <= 0 { 83 | continue 84 | } 85 | 86 | //获取每个消费者对应的work机器列表 87 | for _, consumeServerMap := range consumeServerMapListDB { 88 | workServer := WorkServer{} 89 | result = db.Where("id = ? AND invalid = ?", consumeServerMap.Serverid, InvalidAvailable).First(&workServer) 90 | if result.Error != nil || result.RowsAffected <= 0 || workServer.Id <= 0 { 91 | continue 92 | } 93 | workServer.Protocol = strings.ToLower(workServer.Protocol) 94 | workServer.SetStatusAvailable() 95 | consumeServerMap.WorkServer = workServer 96 | consumeServerMapList = append(consumeServerMapList, consumeServerMap) 97 | } 98 | if len(consumeServerMapList) <= 0 { 99 | continue 100 | } 101 | consumeConfig.ServerList = consumeServerMapList 102 | consumeConfigList = append(consumeConfigList, consumeConfig) 103 | } 104 | return consumeConfigList, nil 105 | } 106 | -------------------------------------------------------------------------------- /internal/httper/workserver.go: -------------------------------------------------------------------------------- 1 | package httper 2 | 3 | import ( 4 | "github.com/changba/nsqproxy/internal/model" 5 | "net/http" 6 | "strconv" 7 | ) 8 | 9 | type WorkServer struct { 10 | } 11 | 12 | func (WorkServer) Create(w http.ResponseWriter, r *http.Request) { 13 | work := &model.WorkServer{} 14 | work.Addr = r.FormValue("addr") 15 | work.Protocol = r.FormValue("protocol") 16 | work.Extra = r.FormValue("extra") 17 | work.Description = r.FormValue("description") 18 | work.Owner = r.FormValue("owner") 19 | work.Invalid = model.InvalidAvailable 20 | invalid, err := strconv.Atoi(r.FormValue("invalid")) 21 | if err != nil || invalid <= 0 || (invalid != model.InvalidAvailable && invalid != model.InvalidUnavailable) { 22 | invalid = model.InvalidAvailable 23 | } 24 | work.Invalid = invalid 25 | id, err := work.Create() 26 | if err != nil { 27 | Failed(w, HttpCodeBadRequest, "create failed. err: "+err.Error()) 28 | return 29 | } 30 | if id <= 0 { 31 | Failed(w, HttpCodeBadRequest, "id is zero") 32 | return 33 | } 34 | Success(w, work) 35 | } 36 | 37 | func (WorkServer) Delete(w http.ResponseWriter, r *http.Request) { 38 | work := &model.WorkServer{} 39 | var err error 40 | work.Id, err = strconv.Atoi(r.FormValue("id")) 41 | if err != nil || work.Id <= 0 { 42 | Failed(w, HttpCodeBadRequest, "param error") 43 | return 44 | } 45 | _, err = work.Delete() 46 | if err != nil { 47 | Failed(w, HttpCodeBadRequest, "delete failed. err: "+err.Error()) 48 | return 49 | } 50 | Success(w, "ok") 51 | } 52 | 53 | func (WorkServer) Update(w http.ResponseWriter, r *http.Request) { 54 | work := &model.WorkServer{} 55 | var err error 56 | work.Id, err = strconv.Atoi(r.FormValue("id")) 57 | if err != nil || work.Id <= 0 { 58 | Failed(w, HttpCodeBadRequest, "param error") 59 | return 60 | } 61 | work.Addr = r.FormValue("addr") 62 | work.Protocol = r.FormValue("protocol") 63 | work.Extra = r.FormValue("extra") 64 | work.Description = r.FormValue("description") 65 | work.Owner = r.FormValue("owner") 66 | invalid, err := strconv.Atoi(r.FormValue("invalid")) 67 | if err != nil || invalid <= 0 || (invalid != model.InvalidAvailable && invalid != model.InvalidUnavailable) { 68 | invalid = model.InvalidAvailable 69 | } 70 | work.Invalid = invalid 71 | 72 | _, err = work.Update() 73 | if err != nil { 74 | Failed(w, HttpCodeBadRequest, "upload failed. err: "+err.Error()) 75 | return 76 | } 77 | Success(w, "ok") 78 | } 79 | 80 | func (WorkServer) Get(w http.ResponseWriter, r *http.Request) { 81 | work := &model.WorkServer{} 82 | var err error 83 | work.Id, err = strconv.Atoi(r.FormValue("id")) 84 | if err != nil || work.Id <= 0 { 85 | Failed(w, HttpCodeBadRequest, "param error") 86 | return 87 | } 88 | n, err := work.Get() 89 | if err != nil || n == 0 || work.Id <= 0 { 90 | Failed(w, HttpCodeNotFound, "not found") 91 | return 92 | } 93 | Success(w, work) 94 | } 95 | 96 | func (WorkServer) Page(w http.ResponseWriter, r *http.Request) { 97 | work := &model.WorkServer{} 98 | page, err := strconv.Atoi(r.FormValue("page")) 99 | if err != nil || page <= 0 { 100 | page = 1 101 | } 102 | pageResult, err := work.Page(page) 103 | if err != nil { 104 | Failed(w, HttpCodeInternalServerError, "please try again. err: "+err.Error()) 105 | return 106 | } 107 | Success(w, pageResult) 108 | } 109 | 110 | func (WorkServer) All(w http.ResponseWriter, r *http.Request) { 111 | work := &model.WorkServer{} 112 | wList, err := work.All() 113 | if err != nil { 114 | Failed(w, HttpCodeInternalServerError, "please try again. err: "+err.Error()) 115 | return 116 | } 117 | Success(w, wList) 118 | } 119 | -------------------------------------------------------------------------------- /internal/proxy/handle.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | "github.com/changba/nsqproxy/config" 6 | "github.com/changba/nsqproxy/internal/model" 7 | "github.com/changba/nsqproxy/internal/module/logger" 8 | "github.com/changba/nsqproxy/internal/worker" 9 | "github.com/nsqio/go-nsq" 10 | "strconv" 11 | ) 12 | 13 | //响应成功 14 | const WorkResponseSuccess = "200" 15 | 16 | type Handler struct { 17 | consumeConfig model.ConsumeConfig 18 | workerList *loadBalance 19 | } 20 | 21 | func NewHandler(consumeConfig model.ConsumeConfig) Handler { 22 | return Handler{ 23 | consumeConfig: consumeConfig, 24 | workerList: newLoadBalance(LoadBalanceMethodLoop, consumeConfig.ServerList), 25 | } 26 | } 27 | 28 | //处理收到的订阅的消息 29 | //返回error就会重新入队,返回nil则不会 30 | func (h *Handler) HandleMessage(message *nsq.Message) error { 31 | //根据权重选一个work 32 | workServer, err := h.workerList.pickWorker() 33 | if err != nil { 34 | h.recordSubLog(logger.LOG_ERROR, message.ID, message.Body, workServer.WorkServer.Addr, "pick work error. "+err.Error()) 35 | return err 36 | } 37 | h.recordSubLog(logger.LOG_DEBUG, message.ID, message.Body, workServer.WorkServer.Addr, "") 38 | //初始化Worker 39 | w, err := worker.NewWorker(workServer.WorkServer.Addr, workServer.WorkServer.Protocol, workServer.WorkServer.Extra, h.consumeConfig.TimeoutDial, h.consumeConfig.TimeoutWrite, h.consumeConfig.TimeoutRead) 40 | if err != nil { 41 | h.recordSubLog(logger.LOG_ERROR, message.ID, message.Body, workServer.WorkServer.Addr, "new worker error. "+err.Error()) 42 | return err 43 | } 44 | //向Worker发送数据 45 | response, err := w.Send(message) 46 | if err != nil { 47 | h.recordSubLog(logger.LOG_ERROR, message.ID, message.Body, workServer.WorkServer.Addr, "send worker error. "+err.Error()) 48 | if h.isRequeueByError(err) { 49 | return err 50 | } 51 | } 52 | //返回值长度判断 53 | if len(response) < 3 { 54 | //约定返回值前3位为状态码,如:200 success 55 | errMsg := "response length less 3. response[" + strconv.Itoa(len(response)) + "]: " + string(response) 56 | h.recordSubLog(logger.LOG_WARNING, message.ID, message.Body, workServer.WorkServer.Addr, errMsg) 57 | //是否需要重新入队 58 | if h.consumeConfig.IsRequeue { 59 | return errors.New(errMsg) 60 | } else { 61 | return nil 62 | } 63 | } 64 | codeString := string(response[0:3]) 65 | //返回值判断 66 | if codeString == WorkResponseSuccess { 67 | return nil 68 | } 69 | errMsg := "response code error. code: " + codeString + ", content: " + string(response) 70 | h.recordSubLog(logger.LOG_WARNING, message.ID, message.Body, workServer.WorkServer.Addr, errMsg) 71 | //是否需要重新入队 72 | if !h.consumeConfig.IsRequeue { 73 | return nil 74 | } 75 | return errors.New(errMsg) 76 | } 77 | 78 | //是否重新入队,根据error类型和配置决定 79 | //连不上worker直接重新入队,write失败直接重新入队,read失败根据配置决定是否重新入队 80 | func (h *Handler) isRequeueByError(err error) bool { 81 | if worker.IsErrorConnect(err) { 82 | return true 83 | } else if worker.IsErrorWrite(err) { 84 | return true 85 | } else if worker.IsErrorRead(err) && h.consumeConfig.IsRequeue { 86 | return true 87 | } 88 | return false 89 | } 90 | 91 | // 处理失败时会调用此方法 92 | // 连续失败五次次,第六次时在go-nsq客户端giving up时会执行 93 | func (h *Handler) LogFailedMessage(message nsq.Message) { 94 | h.recordSubLog(logger.LOG_FATAL, message.ID, message.Body, "", "LogFailedMessage giving up") 95 | } 96 | 97 | //封装写log方法,上面代码太啰嗦 98 | func (h *Handler) recordSubLog(logLevel logger.LogLevel, messageId nsq.MessageID, messageBody []byte, workAddr, errMsg string) { 99 | if len(workAddr) <= 0 { 100 | workAddr = "null" 101 | } 102 | if len(errMsg) <= 0 { 103 | errMsg = "nil" 104 | } 105 | config.SystemConfig.SubLogger.WithLevelf(logLevel, "[%s/%s] nsqproxy %s %s messageid:%s messagebody:%s", 106 | h.consumeConfig.Topic, h.consumeConfig.Channel, 107 | workAddr, errMsg, string(messageId[:]), string(messageBody), 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /web/vue-admin/vue.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const defaultSettings = require('./src/settings.js') 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, dir) 7 | } 8 | 9 | const name = defaultSettings.title || 'vue Admin Template' // page title 10 | 11 | // If your port is set to 80, 12 | // use administrator privileges to execute the command line. 13 | // For example, Mac: sudo npm run 14 | // You can change the port by the following methods: 15 | // port = 9528 npm run dev OR npm run dev --port = 9528 16 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port 17 | 18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/ 19 | module.exports = { 20 | publicPath: '/admin', 21 | outputDir: 'dist', 22 | assetsDir: 'static', 23 | lintOnSave: process.env.NODE_ENV === 'development', 24 | productionSourceMap: false, 25 | devServer: { 26 | port: port, 27 | open: true, 28 | overlay: { 29 | warnings: false, 30 | errors: true 31 | }, 32 | proxy: { 33 | '/admin/api': { 34 | target: 'http://localhost:19421', 35 | changeOrigin: true 36 | } 37 | } 38 | }, 39 | configureWebpack: { 40 | name: name, 41 | resolve: { 42 | alias: { 43 | '@': resolve('src') 44 | } 45 | } 46 | }, 47 | chainWebpack(config) { 48 | config.plugin('preload').tap(() => [ 49 | { 50 | rel: 'preload', 51 | fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], 52 | include: 'initial' 53 | } 54 | ]) 55 | 56 | config.plugins.delete('prefetch') 57 | 58 | config.module 59 | .rule('svg') 60 | .exclude.add(resolve('src/icons')) 61 | .end() 62 | config.module 63 | .rule('icons') 64 | .test(/\.svg$/) 65 | .include.add(resolve('src/icons')) 66 | .end() 67 | .use('svg-sprite-loader') 68 | .loader('svg-sprite-loader') 69 | .options({ 70 | symbolId: 'icon-[name]' 71 | }) 72 | .end() 73 | 74 | config 75 | .when(process.env.NODE_ENV !== 'development', 76 | config => { 77 | config 78 | .plugin('ScriptExtHtmlWebpackPlugin') 79 | .after('html') 80 | .use('script-ext-html-webpack-plugin', [{ 81 | // `runtime` must same as runtimeChunk name. default is `runtime` 82 | inline: /runtime\..*\.js$/ 83 | }]) 84 | .end() 85 | config 86 | .optimization.splitChunks({ 87 | chunks: 'all', 88 | cacheGroups: { 89 | libs: { 90 | name: 'chunk-libs', 91 | test: /[\\/]node_modules[\\/]/, 92 | priority: 10, 93 | chunks: 'initial' // only package third parties that are initially dependent 94 | }, 95 | elementUI: { 96 | name: 'chunk-elementUI', // split elementUI into a single package 97 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 98 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 99 | }, 100 | commons: { 101 | name: 'chunk-commons', 102 | test: resolve('src/components'), // can customize your rules 103 | minChunks: 3, // minimum common number 104 | priority: 5, 105 | reuseExistingChunk: true 106 | } 107 | } 108 | }) 109 | // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk 110 | config.optimization.runtimeChunk('single') 111 | } 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/model/workserver.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | //worker当前可用 10 | const workserverStatusAvailable = 1 11 | 12 | //worker当前不可用 13 | const workserverStatusUnavailable = 0 14 | 15 | type WorkServer struct { 16 | Id int `json:"id" gorm:"primaryKey"` 17 | //地址,IP:PORT 18 | Addr string `json:"addr"` 19 | //协议,如HTTP、FastCGI、CBNSQ 20 | Protocol string `json:"protocol"` 21 | //扩展字段 22 | Extra string `json:"extra"` 23 | //描述 24 | Description string `json:"description"` 25 | //责任人 26 | Owner string `json:"owner"` 27 | //是否有效 28 | Invalid int `json:"invalid"` 29 | //创建时间 30 | CreatedAt time.Time `json:"createdAt"` 31 | //更新时间 32 | UpdatedAt time.Time `json:"updatedAt"` 33 | 34 | status int32 `gorm:"-"` 35 | } 36 | 37 | func (WorkServer) TableName() string { 38 | return "nsqproxy_work_server" 39 | } 40 | 41 | func (WorkServer) CreateTable() error { 42 | sql := "CREATE TABLE IF NOT EXISTS `nsqproxy_work_server` (" + 43 | "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," + 44 | "`addr` varchar(255) NOT NULL DEFAULT '' COMMENT '地址'," + 45 | "`protocol` varchar(11) DEFAULT 'CBNSQ' COMMENT '使用的协议,支持HTTP、FastCGI、CBNSQ'," + 46 | "`extra` varchar(1000) DEFAULT NULL COMMENT '扩展字段,比如协议是FastCGI时,需要传入PHP-FPM的执行的PHP文件的路径'," + 47 | "`description` varchar(1000) DEFAULT '' COMMENT '描述'," + 48 | "`owner` varchar(12) DEFAULT NULL COMMENT '责任人'," + 49 | "`invalid` tinyint(4) DEFAULT '0' COMMENT '是否有效,0是有效'," + 50 | "`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间'," + 51 | "`updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间'," + 52 | "PRIMARY KEY (`id`)," + 53 | "UNIQUE KEY `index_uq_addr` (`addr`)" + 54 | ") ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='所有的可以消费的服务器列表';" 55 | return db.Exec(sql).Error 56 | } 57 | 58 | //两份配置是否相等 59 | func (w WorkServer) IsEqual(newWork WorkServer) bool { 60 | if w.Id != newWork.Id || w.Addr != newWork.Addr || w.Protocol != newWork.Protocol || w.Extra != newWork.Extra || w.Description != newWork.Description { 61 | return false 62 | } 63 | if w.Owner != newWork.Owner || w.Invalid != newWork.Invalid { 64 | return false 65 | } 66 | return true 67 | } 68 | 69 | func (w WorkServer) GetStatus() { 70 | atomic.LoadInt32(&w.status) 71 | } 72 | 73 | func (w *WorkServer) SetStatusAvailable() { 74 | atomic.StoreInt32(&w.status, workserverStatusAvailable) 75 | } 76 | 77 | func (w *WorkServer) SetStatusUnAvailable() { 78 | atomic.StoreInt32(&w.status, workserverStatusUnavailable) 79 | } 80 | 81 | func (w *WorkServer) Create() (int, error) { 82 | result := db.Create(w) 83 | if result.Error != nil { 84 | return 0, result.Error 85 | } else if result.RowsAffected <= 0 { 86 | return 0, errors.New("RowsAffected is zero") 87 | } else if w.Id <= 0 { 88 | return 0, errors.New("primaryKey is zero") 89 | } 90 | return w.Id, nil 91 | } 92 | 93 | func (w *WorkServer) Delete() (int64, error) { 94 | if w.Id <= 0 { 95 | return 0, errors.New("primaryKey is zero") 96 | } 97 | result := db.Delete(w, w.Id) 98 | return result.RowsAffected, result.Error 99 | } 100 | 101 | func (w *WorkServer) Update() (int64, error) { 102 | if w.Id <= 0 { 103 | return 0, errors.New("primaryKey is zero") 104 | } 105 | result := db.Select("Id", "Addr", "Protocol", "Extra", "Description", "Owner", "Invalid", "UpdatedAt").Updates(w) 106 | if result.Error != nil { 107 | return 0, result.Error 108 | } 109 | return result.RowsAffected, nil 110 | } 111 | 112 | func (w *WorkServer) Get() (int64, error) { 113 | if w.Id <= 0 { 114 | return 0, errors.New("primaryKey is zero") 115 | } 116 | result := db.First(w) 117 | return result.RowsAffected, result.Error 118 | } 119 | 120 | func (w *WorkServer) Page(page int) (PageResult, error) { 121 | var wList []WorkServer 122 | d := db.Table(w.TableName()) 123 | //count部分 124 | var total int64 125 | result := d.Count(&total) 126 | if result.Error != nil || result.RowsAffected != 1 { 127 | total = 0 128 | } 129 | //page部分 130 | if page <= 0 { 131 | page = 1 132 | } 133 | result = d.Offset((page - 1) * 20).Limit(20).Find(&wList) 134 | pageRet := PageResult{ 135 | Total: total, 136 | Page: page, 137 | Result: wList, 138 | } 139 | return pageRet, result.Error 140 | } 141 | 142 | func (w *WorkServer) All() ([]WorkServer, error) { 143 | var wList []WorkServer 144 | result := db.Find(&wList) 145 | return wList, result.Error 146 | } 147 | -------------------------------------------------------------------------------- /internal/httper/consumeservermap.go: -------------------------------------------------------------------------------- 1 | package httper 2 | 3 | import ( 4 | "github.com/changba/nsqproxy/internal/model" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type ConsumeServerMap struct { 11 | } 12 | 13 | func (ConsumeServerMap) Create(w http.ResponseWriter, r *http.Request) { 14 | consumeid, err := strconv.Atoi(r.FormValue("consumeid")) 15 | if err != nil || consumeid <= 0 { 16 | Failed(w, HttpCodeBadRequest, "param error. consumeid is required") 17 | return 18 | } 19 | weight, err := strconv.Atoi(r.FormValue("weight")) 20 | if err != nil || weight <= 0 { 21 | Failed(w, HttpCodeBadRequest, "param error. weight is required") 22 | return 23 | } 24 | invalid, err := strconv.Atoi(r.FormValue("invalid")) 25 | if err != nil || invalid <= 0 || (invalid != model.InvalidAvailable && invalid != model.InvalidUnavailable) { 26 | invalid = model.InvalidAvailable 27 | } 28 | //支持多选,逗号分割 29 | if len(r.FormValue("serverid")) < 0 { 30 | Failed(w, HttpCodeBadRequest, "param error. serverid is required") 31 | return 32 | } 33 | serveridStrList := strings.Split(r.FormValue("serverid"), ",") 34 | csMapList := make([]*model.ConsumeServerMap, 0) 35 | for _, serveridStr := range serveridStrList { 36 | serverid, err := strconv.Atoi(serveridStr) 37 | if err != nil || serverid <= 0 { 38 | Failed(w, HttpCodeBadRequest, "param error. serverid must be int") 39 | return 40 | } 41 | csMap := &model.ConsumeServerMap{ 42 | Consumeid: consumeid, 43 | Serverid: serverid, 44 | Weight: weight, 45 | Invalid: invalid, 46 | } 47 | id, err := csMap.Create() 48 | if err != nil { 49 | Failed(w, HttpCodeBadRequest, "create failed. err: "+err.Error()) 50 | return 51 | } 52 | if id <= 0 { 53 | Failed(w, HttpCodeBadRequest, "id is zero") 54 | return 55 | } 56 | csMapList = append(csMapList, csMap) 57 | } 58 | Success(w, csMapList) 59 | } 60 | 61 | func (ConsumeServerMap) Delete(w http.ResponseWriter, r *http.Request) { 62 | csMap := &model.ConsumeServerMap{} 63 | var err error 64 | csMap.Id, err = strconv.Atoi(r.FormValue("id")) 65 | if err != nil || csMap.Id <= 0 { 66 | Failed(w, HttpCodeBadRequest, "param error") 67 | return 68 | } 69 | _, err = csMap.Delete() 70 | if err != nil { 71 | Failed(w, HttpCodeBadRequest, "delete failed. err: "+err.Error()) 72 | return 73 | } 74 | Success(w, "ok") 75 | } 76 | 77 | func (ConsumeServerMap) Update(w http.ResponseWriter, r *http.Request) { 78 | csMap := &model.ConsumeServerMap{} 79 | var err error 80 | csMap.Id, err = strconv.Atoi(r.FormValue("id")) 81 | if err != nil || csMap.Id <= 0 { 82 | Failed(w, HttpCodeBadRequest, "param error") 83 | return 84 | } 85 | csMap.Consumeid, err = strconv.Atoi(r.FormValue("consumeid")) 86 | if err != nil || csMap.Consumeid <= 0 { 87 | Failed(w, HttpCodeBadRequest, "param error. consumeid is required") 88 | return 89 | } 90 | csMap.Weight, err = strconv.Atoi(r.FormValue("weight")) 91 | if err != nil || csMap.Weight <= 0 { 92 | Failed(w, HttpCodeBadRequest, "param error. weight is required") 93 | return 94 | } 95 | csMap.Serverid, err = strconv.Atoi(r.FormValue("serverid")) 96 | if err != nil || csMap.Serverid <= 0 { 97 | Failed(w, HttpCodeBadRequest, "param error. serverid is required") 98 | return 99 | } 100 | invalid, err := strconv.Atoi(r.FormValue("invalid")) 101 | if err != nil || invalid <= 0 || (invalid != model.InvalidAvailable && invalid != model.InvalidUnavailable) { 102 | invalid = model.InvalidAvailable 103 | } 104 | csMap.Invalid = invalid 105 | 106 | _, err = csMap.Update() 107 | if err != nil { 108 | Failed(w, HttpCodeBadRequest, "upload failed. err: "+err.Error()) 109 | return 110 | } 111 | Success(w, "ok") 112 | } 113 | 114 | func (ConsumeServerMap) GetWork(w http.ResponseWriter, r *http.Request) { 115 | csMap := &model.ConsumeServerMap{} 116 | var err error 117 | csMap.Id, err = strconv.Atoi(r.FormValue("id")) 118 | if err != nil || csMap.Id <= 0 { 119 | Failed(w, HttpCodeBadRequest, "param error") 120 | return 121 | } 122 | n, err := csMap.Get() 123 | if n == 0 || csMap.Id <= 0 { 124 | Failed(w, HttpCodeNotFound, "not found") 125 | return 126 | } 127 | if csMap.Serverid <= 0 { 128 | Failed(w, HttpCodeBadRequest, "work.Id is empty") 129 | return 130 | } 131 | work := model.WorkServer{} 132 | work.Id = csMap.Serverid 133 | n, err = work.Get() 134 | if err != nil || n == 0 || work.Id <= 0 { 135 | Failed(w, HttpCodeNotFound, "work not found") 136 | return 137 | } 138 | csMap.WorkServer = work 139 | Success(w, csMap) 140 | } 141 | -------------------------------------------------------------------------------- /web/vue-admin/src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: $sideBarWidth; 7 | position: relative; 8 | } 9 | 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: $sideBarWidth !important; 13 | background-color: $menuBg; 14 | height: 100%; 15 | position: fixed; 16 | font-size: 0px; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 1001; 21 | overflow: hidden; 22 | 23 | // reset element-ui css 24 | .horizontal-collapse-transition { 25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 26 | } 27 | 28 | .scrollbar-wrapper { 29 | overflow-x: hidden !important; 30 | } 31 | 32 | .el-scrollbar__bar.is-vertical { 33 | right: 0px; 34 | } 35 | 36 | .el-scrollbar { 37 | height: 100%; 38 | } 39 | 40 | &.has-logo { 41 | .el-scrollbar { 42 | height: calc(100% - 50px); 43 | } 44 | } 45 | 46 | .is-horizontal { 47 | display: none; 48 | } 49 | 50 | a { 51 | display: inline-block; 52 | width: 100%; 53 | overflow: hidden; 54 | } 55 | 56 | .svg-icon { 57 | margin-right: 16px; 58 | } 59 | 60 | .sub-el-icon { 61 | margin-right: 12px; 62 | margin-left: -2px; 63 | } 64 | 65 | .el-menu { 66 | border: none; 67 | height: 100%; 68 | width: 100% !important; 69 | } 70 | 71 | // menu hover 72 | .submenu-title-noDropdown, 73 | .el-submenu__title { 74 | &:hover { 75 | background-color: $menuHover !important; 76 | } 77 | } 78 | 79 | .is-active>.el-submenu__title { 80 | color: $subMenuActiveText !important; 81 | } 82 | 83 | & .nest-menu .el-submenu>.el-submenu__title, 84 | & .el-submenu .el-menu-item { 85 | min-width: $sideBarWidth !important; 86 | background-color: $subMenuBg !important; 87 | 88 | &:hover { 89 | background-color: $subMenuHover !important; 90 | } 91 | } 92 | } 93 | 94 | .hideSidebar { 95 | .sidebar-container { 96 | width: 54px !important; 97 | } 98 | 99 | .main-container { 100 | margin-left: 54px; 101 | } 102 | 103 | .submenu-title-noDropdown { 104 | padding: 0 !important; 105 | position: relative; 106 | 107 | .el-tooltip { 108 | padding: 0 !important; 109 | 110 | .svg-icon { 111 | margin-left: 20px; 112 | } 113 | 114 | .sub-el-icon { 115 | margin-left: 19px; 116 | } 117 | } 118 | } 119 | 120 | .el-submenu { 121 | overflow: hidden; 122 | 123 | &>.el-submenu__title { 124 | padding: 0 !important; 125 | 126 | .svg-icon { 127 | margin-left: 20px; 128 | } 129 | 130 | .sub-el-icon { 131 | margin-left: 19px; 132 | } 133 | 134 | .el-submenu__icon-arrow { 135 | display: none; 136 | } 137 | } 138 | } 139 | 140 | .el-menu--collapse { 141 | .el-submenu { 142 | &>.el-submenu__title { 143 | &>span { 144 | height: 0; 145 | width: 0; 146 | overflow: hidden; 147 | visibility: hidden; 148 | display: inline-block; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | .el-menu--collapse .el-menu .el-submenu { 156 | min-width: $sideBarWidth !important; 157 | } 158 | 159 | // mobile responsive 160 | .mobile { 161 | .main-container { 162 | margin-left: 0px; 163 | } 164 | 165 | .sidebar-container { 166 | transition: transform .28s; 167 | width: $sideBarWidth !important; 168 | } 169 | 170 | &.hideSidebar { 171 | .sidebar-container { 172 | pointer-events: none; 173 | transition-duration: 0.3s; 174 | transform: translate3d(-$sideBarWidth, 0, 0); 175 | } 176 | } 177 | } 178 | 179 | .withoutAnimation { 180 | 181 | .main-container, 182 | .sidebar-container { 183 | transition: none; 184 | } 185 | } 186 | } 187 | 188 | // when menu collapsed 189 | .el-menu--vertical { 190 | &>.el-menu { 191 | .svg-icon { 192 | margin-right: 16px; 193 | } 194 | .sub-el-icon { 195 | margin-right: 12px; 196 | margin-left: -2px; 197 | } 198 | } 199 | 200 | .nest-menu .el-submenu>.el-submenu__title, 201 | .el-menu-item { 202 | &:hover { 203 | // you can use $subMenuHover 204 | background-color: $menuHover !important; 205 | } 206 | } 207 | 208 | // the scroll bar appears when the subMenu is too long 209 | >.el-menu--popup { 210 | max-height: 100vh; 211 | overflow-y: auto; 212 | 213 | &::-webkit-scrollbar-track-piece { 214 | background: #d3dce6; 215 | } 216 | 217 | &::-webkit-scrollbar { 218 | width: 6px; 219 | } 220 | 221 | &::-webkit-scrollbar-thumb { 222 | background: #99a9bf; 223 | border-radius: 20px; 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /web/vue-admin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | "vue/max-attributes-per-line": [2, { 18 | "singleline": 10, 19 | "multiline": { 20 | "max": 1, 21 | "allowFirstLine": false 22 | } 23 | }], 24 | "vue/singleline-html-element-content-newline": "off", 25 | "vue/multiline-html-element-content-newline":"off", 26 | "vue/name-property-casing": ["error", "PascalCase"], 27 | "vue/no-v-html": "off", 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': ["error", "always", {"null": "ignore"}], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 0, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | 'quotes': [2, 'single', { 166 | 'avoidEscape': true, 167 | 'allowTemplateLiterals': true 168 | }], 169 | 'semi': [2, 'never'], 170 | 'semi-spacing': [2, { 171 | 'before': false, 172 | 'after': true 173 | }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 2, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | --------------------------------------------------------------------------------