├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── assets.go ├── bootstrap │ ├── css │ │ ├── animate.min.css │ │ ├── bootstrap.min.css │ │ ├── materialdesignicons.min.css │ │ └── style.min.css │ ├── favicon.ico │ ├── fonts │ │ ├── materialdesignicons-webfont.eot │ │ ├── materialdesignicons-webfont.ttf │ │ ├── materialdesignicons-webfont.woff │ │ └── materialdesignicons-webfont.woff2 │ ├── images │ │ ├── logo-sidebar.png │ │ ├── qr-code.png │ │ └── users │ │ │ └── avatar.png │ └── js │ │ ├── authorization │ │ ├── crypto-js.min.js │ │ ├── enc-base64.min.js │ │ ├── hmac-sha256.js │ │ ├── ksort.js │ │ └── md5.min.js │ │ ├── bootstrap-maxlength │ │ └── bootstrap-maxlength.min.js │ │ ├── bootstrap-multitabs │ │ ├── multitabs.js │ │ ├── multitabs.min.css │ │ └── multitabs.min.js │ │ ├── bootstrap-notify.min.js │ │ ├── bootstrap-notify │ │ ├── bootstrap-notify.min.js │ │ └── notify.js │ │ ├── bootstrap-select │ │ ├── bootstrap-select.css │ │ ├── bootstrap-select.min.css │ │ ├── bootstrap-select.min.js │ │ └── i18n │ │ │ ├── defaults-zh_CN.min.js │ │ │ └── defaults-zh_TW.min.js │ │ ├── bootstrap-table │ │ ├── bootstrap-table.css │ │ ├── bootstrap-table.js │ │ ├── bootstrap-table.min.css │ │ ├── bootstrap-table.min.js │ │ ├── extensions │ │ │ ├── accent-neutralise │ │ │ │ └── bootstrap-table-accent-neutralise.min.js │ │ │ ├── addrbar │ │ │ │ └── bootstrap-table-addrbar.min.js │ │ │ ├── auto-refresh │ │ │ │ └── bootstrap-table-auto-refresh.min.js │ │ │ ├── cell-input │ │ │ │ ├── bootstrap-table-cell-input.min.css │ │ │ │ └── bootstrap-table-cell-input.min.js │ │ │ ├── cookie │ │ │ │ └── bootstrap-table-cookie.min.js │ │ │ ├── copy-rows │ │ │ │ └── bootstrap-table-copy-rows.min.js │ │ │ ├── defer-url │ │ │ │ └── bootstrap-table-defer-url.min.js │ │ │ ├── editable │ │ │ │ └── bootstrap-table-editable.min.js │ │ │ ├── export │ │ │ │ └── bootstrap-table-export.min.js │ │ │ ├── filter-control │ │ │ │ ├── bootstrap-table-filter-control.min.css │ │ │ │ └── bootstrap-table-filter-control.min.js │ │ │ ├── fixed-columns │ │ │ │ ├── bootstrap-table-fixed-columns.js │ │ │ │ └── bootstrap-table-fixed-columns.scss │ │ │ ├── group-by-v2 │ │ │ │ ├── bootstrap-table-group-by.js │ │ │ │ ├── bootstrap-table-group-by.scss │ │ │ │ └── extension.json │ │ │ ├── i18n-enhance │ │ │ │ ├── bootstrap-table-i18n-enhance.js │ │ │ │ └── extension.json │ │ │ ├── key-events │ │ │ │ ├── bootstrap-table-key-events.js │ │ │ │ └── extension.json │ │ │ ├── mobile │ │ │ │ ├── bootstrap-table-mobile.js │ │ │ │ └── extension.json │ │ │ ├── multiple-sort │ │ │ │ ├── bootstrap-table-multiple-sort.js │ │ │ │ └── extension.json │ │ │ ├── page-jump-to │ │ │ │ ├── bootstrap-table-page-jump-to.js │ │ │ │ └── bootstrap-table-page-jump-to.scss │ │ │ ├── pipeline │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── bootstrap-table-pipeline.js │ │ │ │ └── extension.json │ │ │ ├── print │ │ │ │ └── bootstrap-table-print.js │ │ │ ├── reorder-columns │ │ │ │ ├── bootstrap-table-reorder-columns.js │ │ │ │ └── extension.json │ │ │ ├── reorder-rows │ │ │ │ ├── bootstrap-table-reorder-rows.js │ │ │ │ ├── bootstrap-table-reorder-rows.scss │ │ │ │ └── extension.json │ │ │ ├── resizable │ │ │ │ ├── bootstrap-table-resizable.js │ │ │ │ └── extension.json │ │ │ ├── sticky-header │ │ │ │ ├── bootstrap-table-sticky-header.js │ │ │ │ ├── bootstrap-table-sticky-header.scss │ │ │ │ └── extension.json │ │ │ ├── toolbar │ │ │ │ ├── bootstrap-table-toolbar.js │ │ │ │ └── extension.json │ │ │ └── treegrid │ │ │ │ └── bootstrap-table-treegrid.min.js │ │ └── locale │ │ │ ├── bootstrap-table-zh-CN.min.js │ │ │ └── bootstrap-table-zh-TW.min.js │ │ ├── bootstrap.min.js │ │ ├── httpclient │ │ └── httpclient.js │ │ ├── index.min.js │ │ ├── jquery-confirm │ │ ├── jquery-confirm.min.css │ │ └── jquery-confirm.min.js │ │ ├── jquery-treegrid │ │ ├── jquery.treegrid.min.css │ │ └── jquery.treegrid.min.js │ │ ├── jquery.bootstrap.wizard.min.js │ │ ├── jquery.cookie.min.js │ │ ├── jquery.min.js │ │ ├── jquery.pagination.js │ │ ├── main.min.js │ │ ├── perfect-scrollbar.min.js │ │ ├── popper.min.js │ │ └── vkbeautify.js └── templates │ ├── admin │ ├── admin_add.html │ ├── admin_list.html │ ├── admin_login.html │ ├── admin_menu.html │ ├── admin_modify_info.html │ └── admin_modify_password.html │ ├── authorized │ ├── authorized_add.html │ ├── authorized_api.html │ ├── authorized_demo.html │ └── authorized_list.html │ ├── configinfo │ ├── config_code.html │ └── config_email.html │ ├── cron_task │ ├── cron_task_add.html │ ├── cron_task_edit.html │ └── cron_task_list.html │ ├── dashboard │ └── dashboard.html │ ├── generator │ ├── generator_gorm.html │ └── generator_handler.html │ ├── index │ └── index.html │ ├── install │ ├── install_view.html │ └── upgrade_view.html │ ├── menu │ ├── menu_action.html │ └── menu_view.html │ └── tool │ ├── tool_cache.html │ ├── tool_data.html │ ├── tool_hashids.html │ ├── tool_logs.html │ └── tool_websocket.html ├── cmd ├── gormgen │ ├── README.MD │ ├── main.go │ └── pkg │ │ ├── generator.go │ │ ├── parser.go │ │ ├── template.go │ │ └── utils.go ├── handlergen │ └── main.go ├── mfmt │ └── main.go └── mysqlmd │ ├── main.go │ └── mysql │ └── mysql.go ├── configs ├── configs.go ├── constants.go ├── dev_configs.toml ├── fat_configs.toml ├── pro_configs.toml └── uat_configs.toml ├── deployments ├── loki │ ├── loki.yaml │ └── promtail.yaml └── prometheus │ └── prometheus.yml ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── en.md ├── go.mod ├── go.sum ├── gqlgen.yml ├── internal ├── alert │ ├── alert.go │ └── email_template.go ├── api │ ├── admin │ │ ├── func_create.go │ │ ├── func_createadminmenu.go │ │ ├── func_delete.go │ │ ├── func_detail.go │ │ ├── func_list.go │ │ ├── func_listadminmenu.go │ │ ├── func_login.go │ │ ├── func_logout.go │ │ ├── func_modifypassword.go │ │ ├── func_modifypersonalinfo.go │ │ ├── func_offline.go │ │ ├── func_resetpassword.go │ │ ├── func_updateused.go │ │ └── handler.go │ ├── authorized │ │ ├── func_create.go │ │ ├── func_createapi.go │ │ ├── func_delete.go │ │ ├── func_deleteapi.go │ │ ├── func_list.go │ │ ├── func_listapi.go │ │ ├── func_updateused.go │ │ └── handler.go │ ├── config │ │ ├── func_email.go │ │ └── handler.go │ ├── cron │ │ ├── func_create.go │ │ ├── func_detail.go │ │ ├── func_execute.go │ │ ├── func_list.go │ │ ├── func_modify.go │ │ ├── func_updateused.go │ │ └── handler.go │ ├── helper │ │ ├── func_md5.go │ │ ├── func_sign.go │ │ └── handler.go │ ├── menu │ │ ├── func_create.go │ │ ├── func_createaction.go │ │ ├── func_delete.go │ │ ├── func_deleteaction.go │ │ ├── func_detail.go │ │ ├── func_list.go │ │ ├── func_listaction.go │ │ ├── func_updatesort.go │ │ ├── func_updateused.go │ │ └── handler.go │ └── tool │ │ ├── func_clearcache.go │ │ ├── func_dbs.go │ │ ├── func_hashidsdecode.go │ │ ├── func_hashidsencode.go │ │ ├── func_searchcache.go │ │ ├── func_searchmysql.go │ │ ├── func_sendmessage.go │ │ ├── func_tables.go │ │ └── handler.go ├── code │ ├── README.md │ ├── code.go │ ├── en-us.go │ └── zh-cn.go ├── graph │ ├── README.md │ ├── generated │ │ └── generated.go │ ├── handler │ │ └── handler.go │ ├── model │ │ └── generated.go │ ├── resolvers │ │ ├── generated │ │ │ └── generated.go │ │ ├── resolvers.go │ │ └── user.go │ └── schemas │ │ └── user.graphql ├── metrics │ ├── metrics.go │ └── prometheus.go ├── pkg │ ├── core │ │ ├── context.go │ │ ├── core.go │ │ └── error.go │ ├── password │ │ └── password.go │ └── validation │ │ └── validation.go ├── proposal │ ├── alert.go │ ├── metrics.go │ ├── session.go │ └── tablesqls │ │ ├── table_admin.go │ │ ├── table_admin_menu.go │ │ ├── table_authorized.go │ │ ├── table_authorized_api.go │ │ ├── table_cron_task.go │ │ ├── table_menu.go │ │ └── table_menu_action.go ├── render │ ├── admin │ │ └── admin.go │ ├── authorized │ │ └── authorized.go │ ├── config │ │ └── config.go │ ├── cron │ │ └── cron.go │ ├── dashboard │ │ └── dashboard.go │ ├── generator │ │ ├── generator.go │ │ ├── gorm_execute.go │ │ ├── gorm_view.go │ │ ├── handler_execute.go │ │ └── handler_view.go │ ├── index │ │ └── index.go │ ├── install │ │ ├── execute.go │ │ └── install.go │ ├── tool │ │ └── tool.go │ └── upgrade │ │ ├── execute.go │ │ ├── upgrade.go │ │ └── view.go ├── repository │ ├── cron │ │ ├── cron.go │ │ ├── cron_add_job.go │ │ ├── cron_add_task.go │ │ ├── cron_remove_task.go │ │ ├── cron_start.go │ │ └── cron_stop.go │ ├── mysql │ │ ├── admin │ │ │ ├── gen_admin.go │ │ │ ├── gen_model.go │ │ │ └── gen_table.md │ │ ├── admin_menu │ │ │ ├── gen_admin_menu.go │ │ │ ├── gen_model.go │ │ │ └── gen_table.md │ │ ├── authorized │ │ │ ├── gen_authorized.go │ │ │ ├── gen_model.go │ │ │ ├── gen_table.md │ │ │ └── model.go │ │ ├── authorized_api │ │ │ ├── gen_authorized_api.go │ │ │ ├── gen_model.go │ │ │ └── gen_table.md │ │ ├── cron_task │ │ │ ├── gen_cron_task.go │ │ │ ├── gen_model.go │ │ │ ├── gen_table.md │ │ │ └── model.go │ │ ├── menu │ │ │ ├── gen_menu.go │ │ │ ├── gen_model.go │ │ │ └── gen_table.md │ │ ├── menu_action │ │ │ ├── gen_menu_action.go │ │ │ ├── gen_model.go │ │ │ └── gen_table.md │ │ ├── mysql.go │ │ └── plugin.go │ ├── redis │ │ └── redis.go │ └── socket │ │ ├── socket.go │ │ ├── socket_on_close.go │ │ ├── socket_on_message.go │ │ └── socket_on_send.go ├── router │ ├── interceptor │ │ ├── check_login.go │ │ ├── check_rbac.go │ │ ├── check_signature.go │ │ └── interceptor.go │ ├── router.go │ ├── router_api.go │ ├── router_graphql.go │ ├── router_render.go │ └── router_socket.go ├── services │ ├── admin │ │ ├── service.go │ │ ├── service_create.go │ │ ├── service_createmenu.go │ │ ├── service_delete.go │ │ ├── service_detail.go │ │ ├── service_listmenu.go │ │ ├── service_modifypassword.go │ │ ├── service_modifypersonalinfo.go │ │ ├── service_myaction.go │ │ ├── service_mymenu.go │ │ ├── service_pagelist.go │ │ ├── service_pagelistcount.go │ │ ├── service_resetpassword.go │ │ └── service_updateused.go │ ├── authorized │ │ ├── service.go │ │ ├── service_create.go │ │ ├── service_createapi.go │ │ ├── service_delete.go │ │ ├── service_deleteapi.go │ │ ├── service_detail.go │ │ ├── service_detailbykey.go │ │ ├── service_list.go │ │ ├── service_listapi.go │ │ ├── service_pagelist.go │ │ ├── service_pagelistcount.go │ │ └── service_updateused.go │ ├── cron │ │ ├── service.go │ │ ├── service_create.go │ │ ├── service_detail.go │ │ ├── service_execute.go │ │ ├── service_modify.go │ │ ├── service_pagelist.go │ │ ├── service_pagelistcount.go │ │ └── service_updateused.go │ └── menu │ │ ├── service.go │ │ ├── service_create.go │ │ ├── service_createaction.go │ │ ├── service_delete.go │ │ ├── service_deleteaction.go │ │ ├── service_detail.go │ │ ├── service_list.go │ │ ├── service_listaction.go │ │ ├── service_modify.go │ │ ├── service_updatesort.go │ │ └── service_updateused.go └── websocket │ └── sysmessage │ └── sysmessage.go ├── logs └── .gitkeep ├── main.go ├── pkg ├── aes │ ├── aes.go │ └── aes_test.go ├── browser │ └── browser.go ├── color │ ├── string_darwin.go │ ├── string_linux.go │ └── string_windows.go ├── ddm │ ├── README.md │ ├── mark.go │ ├── type.go │ └── type_test.go ├── debugs │ └── print.go ├── env │ └── env.go ├── errors │ ├── err.go │ └── err_test.go ├── file │ └── file.go ├── hash │ ├── hash.go │ ├── hash_hashids.go │ └── hash_hashids_test.go ├── httpclient │ ├── alarm.go │ ├── client.go │ ├── error.go │ ├── option.go │ ├── retry.go │ └── util.go ├── logger │ ├── logger.go │ └── logger_test.go ├── mail │ ├── mail.go │ └── mail_test.go ├── rsa │ ├── rsa.go │ └── rsa_test.go ├── shutdown │ └── shutdown.go ├── signature │ ├── signature.go │ ├── signature_generate.go │ ├── signature_test.go │ └── signature_verify.go ├── timeutil │ ├── timeutil.go │ └── timeutil_test.go ├── trace │ ├── debug.go │ ├── dialog.go │ ├── redis.go │ ├── sql.go │ └── trace.go └── urltable │ ├── urltable.go │ └── urltable_test.go └── scripts ├── gormgen.bat ├── gormgen.sh ├── gqlgen.bat ├── gqlgen.sh ├── handlergen.bat ├── handlergen.sh ├── swagger.bat └── swagger.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.idea 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # Project runtime log 19 | logs/*.log -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM 基于 golang:1.16-alpine 2 | FROM golang:1.16-alpine AS builder 3 | 4 | # ENV 设置环境变量 5 | ENV GOPATH=/opt/repo 6 | ENV GO111MODULE=on 7 | ENV GOPROXY=https://goproxy.io,direct 8 | 9 | # COPY 源路径 目标路径 10 | COPY . $GOPATH/src/github.com/xinliangnote/go-gin-api 11 | 12 | # RUN 执行 go build . 13 | RUN cd $GOPATH/src/github.com/xinliangnote/go-gin-api && go build . 14 | 15 | # FROM 基于 alpine:latest 16 | FROM alpine:latest 17 | 18 | # RUN 设置代理镜像 19 | RUN echo -e http://mirrors.ustc.edu.cn/alpine/v3.13/main/ > /etc/apk/repositories 20 | 21 | # RUN 设置 Asia/Shanghai 时区 22 | RUN apk --no-cache add tzdata && \ 23 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 24 | echo "Asia/Shanghai" > /etc/timezone 25 | 26 | # COPY 源路径 目标路径 从镜像中 COPY 27 | COPY --from=builder /opt/repo/src/github.com/xinliangnote/go-gin-api /opt 28 | 29 | # EXPOSE 设置端口映射 30 | EXPOSE 9999/tcp 31 | 32 | # WORKDIR 设置工作目录 33 | WORKDIR /opt 34 | 35 | # CMD 设置启动命令 36 | CMD ["./go-gin-api", "-env", "fat"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 新亮笔记 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 | -------------------------------------------------------------------------------- /assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import "embed" 4 | 5 | var ( 6 | //go:embed bootstrap 7 | Bootstrap embed.FS 8 | 9 | //go:embed templates 10 | Templates embed.FS 11 | ) 12 | -------------------------------------------------------------------------------- /assets/bootstrap/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/assets/bootstrap/favicon.ico -------------------------------------------------------------------------------- /assets/bootstrap/fonts/materialdesignicons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/assets/bootstrap/fonts/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /assets/bootstrap/fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/assets/bootstrap/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /assets/bootstrap/fonts/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/assets/bootstrap/fonts/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /assets/bootstrap/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/assets/bootstrap/fonts/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /assets/bootstrap/images/logo-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/assets/bootstrap/images/logo-sidebar.png -------------------------------------------------------------------------------- /assets/bootstrap/images/qr-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/assets/bootstrap/images/qr-code.png -------------------------------------------------------------------------------- /assets/bootstrap/images/users/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/assets/bootstrap/images/users/avatar.png -------------------------------------------------------------------------------- /assets/bootstrap/js/authorization/enc-base64.min.js: -------------------------------------------------------------------------------- 1 | !function(r,e){"object"==typeof exports?module.exports=exports=e(require("./core")):"function"==typeof define&&define.amd?define(["./core"],e):e(r.CryptoJS)}(this,function(r){var s;return s=r.lib.WordArray,r.enc.Base64={stringify:function(r){var e=r.words,t=r.sigBytes,a=this._map;r.clamp();for(var n=[],o=0;o>>2]>>>24-o%4*8&255)<<16|(e[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|e[o+2>>>2]>>>24-(o+2)%4*8&255,f=0;f<4&&o+.75*f>>6*(3-f)&63));var c=a.charAt(64);if(c)for(;n.length%4;)n.push(c);return n.join("")},parse:function(r){var e=r.length,t=this._map,a=this._reverseMap;if(!a){a=this._reverseMap=[];for(var n=0;n>>6-o%4*2,c=i|f;a[n>>>2]|=c<<24-n%4*8,n++}return s.create(a,n)}(r,e,a)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="},r.enc.Base64}); -------------------------------------------------------------------------------- /assets/bootstrap/js/authorization/hmac-sha256.js: -------------------------------------------------------------------------------- 1 | ;(function (root, factory, undef) { 2 | if (typeof exports === "object") { 3 | // CommonJS 4 | module.exports = exports = factory(require("./core"), require("./sha256"), require("./hmac")); 5 | } 6 | else if (typeof define === "function" && define.amd) { 7 | // AMD 8 | define(["./core", "./sha256", "./hmac"], factory); 9 | } 10 | else { 11 | // Global (browser) 12 | factory(root.CryptoJS); 13 | } 14 | }(this, function (CryptoJS) { 15 | 16 | return CryptoJS.HmacSHA256; 17 | 18 | })); -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-notify/notify.js: -------------------------------------------------------------------------------- 1 | document.write(''); 2 | 3 | function SuccessNotify(content) { 4 | $.notify({ 5 | icon: "mdi mdi-alert", 6 | title: "", 7 | message: content, 8 | url: "", 9 | target: "" 10 | }, { 11 | type: "success", 12 | allow_dismiss: true, 13 | newest_on_top: false, 14 | placement: { 15 | from: "top", 16 | align: "right", 17 | }, 18 | offset: { 19 | x: "20", 20 | y: "20" 21 | }, 22 | spacing: "10", 23 | z_index: "1031", 24 | delay: "3000", 25 | animate: { 26 | enter: "animated fadeInDown", 27 | exit: "animated fadeOutUp" 28 | }, 29 | onClosed: null, 30 | mouse_over: null 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-select/i18n/defaults-zh_CN.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap-select v1.13.17 (https://developer.snapappointments.com/bootstrap-select) 3 | * 4 | * Copyright 2012-2020 SnapAppointments, LLC 5 | * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) 6 | */ 7 | 8 | !function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"\u6ca1\u6709\u9009\u4e2d\u4efb\u4f55\u9879",noneResultsText:"\u6ca1\u6709\u627e\u5230\u5339\u914d\u9879",countSelectedText:"\u9009\u4e2d{1}\u4e2d\u7684{0}\u9879",maxOptionsText:["\u8d85\u51fa\u9650\u5236 (\u6700\u591a\u9009\u62e9{n}\u9879)","\u7ec4\u9009\u62e9\u8d85\u51fa\u9650\u5236(\u6700\u591a\u9009\u62e9{n}\u7ec4)"],multipleSeparator:", ",selectAllText:"\u5168\u9009",deselectAllText:"\u53d6\u6d88\u5168\u9009"}}); -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-select/i18n/defaults-zh_TW.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap-select v1.13.17 (https://developer.snapappointments.com/bootstrap-select) 3 | * 4 | * Copyright 2012-2020 SnapAppointments, LLC 5 | * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) 6 | */ 7 | 8 | !function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"\u6c92\u6709\u9078\u53d6\u4efb\u4f55\u9805\u76ee",noneResultsText:"\u6c92\u6709\u627e\u5230\u7b26\u5408\u7684\u7d50\u679c",countSelectedText:"\u5df2\u7d93\u9078\u53d6{0}\u500b\u9805\u76ee",maxOptionsText:["\u8d85\u904e\u9650\u5236 (\u6700\u591a\u9078\u64c7{n}\u9805)","\u8d85\u904e\u9650\u5236(\u6700\u591a\u9078\u64c7{n}\u7d44)"],selectAllText:"\u9078\u53d6\u5168\u90e8",deselectAllText:"\u5168\u90e8\u53d6\u6d88",multipleSeparator:", "}}); -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/cell-input/bootstrap-table-cell-input.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) 3 | * 4 | * @version v1.16.0 5 | * @homepage https://bootstrap-table.com 6 | * @author wenzhixin (http://wenzhixin.net.cn/) 7 | * @license MIT 8 | */ 9 | 10 | .table-cell-input{display:block!important;padding:5px!important;margin:0!important;border:0!important;width:100%!important;box-sizing:border-box!important;-moz-box-sizing:border-box!important;border-radius:0!important;line-height:1!important;white-space:nowrap} -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) 3 | * 4 | * @version v1.16.0 5 | * @homepage https://bootstrap-table.com 6 | * @author wenzhixin (http://wenzhixin.net.cn/) 7 | * @license MIT 8 | */ 9 | 10 | @charset "UTF-8";.no-filter-control{height:34px}.filter-control{margin:0 2px 2px 2px} -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/fixed-columns/bootstrap-table-fixed-columns.scss: -------------------------------------------------------------------------------- 1 | .fixed-columns, 2 | .fixed-columns-right { 3 | position: absolute; 4 | top: 0; 5 | height: 100%; 6 | background-color: #fff; 7 | box-sizing: border-box; 8 | z-index: 1; 9 | } 10 | 11 | .fixed-columns { 12 | left: 0; 13 | 14 | .fixed-table-body { 15 | overflow: hidden!important; 16 | } 17 | } 18 | 19 | .fixed-columns-right { 20 | right: 0; 21 | 22 | .fixed-table-body { 23 | overflow-x: hidden!important; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.scss: -------------------------------------------------------------------------------- 1 | .bootstrap-table .table > tbody > tr.groupBy { 2 | cursor: pointer; 3 | } 4 | 5 | .bootstrap-table .table > tbody > tr.hidden + tr.detail-view { 6 | display: none; 7 | } 8 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/group-by-v2/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Group By V2", 3 | "version": "1.0.0", 4 | "description": "Group the data by field", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by-v2", 6 | "example": "", 7 | "plugins": [], 8 | "author": { 9 | "name": "Knoxvillekm", 10 | "image": "https://avatars3.githubusercontent.com/u/11072464" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/i18n-enhance/bootstrap-table-i18n-enhance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Jewway 3 | * @update zhixin wen 4 | */ 5 | 6 | $.fn.bootstrapTable.methods.push('changeTitle') 7 | $.fn.bootstrapTable.methods.push('changeLocale') 8 | 9 | $.BootstrapTable = class extends $.BootstrapTable { 10 | 11 | changeTitle (locale) { 12 | $.each(this.options.columns, (idx, columnList) => { 13 | $.each(columnList, (idx, column) => { 14 | if (column.field) { 15 | column.title = locale[column.field] 16 | } 17 | }) 18 | }) 19 | this.initHeader() 20 | this.initBody() 21 | this.initToolbar() 22 | } 23 | 24 | changeLocale (localeId) { 25 | this.options.locale = localeId 26 | this.initLocale() 27 | this.initPagination() 28 | this.initBody() 29 | this.initToolbar() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/i18n-enhance/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "i18n Enhance", 3 | "version": "1.0.0", 4 | "description": "Plugin to add i18n API in order to change column's title and table locale.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/i18n-enhance", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/i18n-enhance.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-i18n-enhance", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/i18n-enhance" 11 | }], 12 | 13 | "author": { 14 | "name": "Jewway", 15 | "image": "https://avatars0.githubusercontent.com/u/3501899" 16 | } 17 | } -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/key-events/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Key Events", 3 | "version": "1.0.0", 4 | "description": "Plugin to support the key events in the bootstrap table.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/key-events", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/key-events.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-key-events", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/key-events" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/mobile/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mobile", 3 | "version": "1.1.0", 4 | "description": "Plugin to support the responsive feature.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/mobile", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/mobile.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-mobile", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/mobile" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/multiple-sort/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Multiple Sort", 3 | "version": "1.1.0", 4 | "description": "Plugin to support the multiple sort.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-sort", 6 | "example": "#", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-multiple-sort", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-sort" 11 | }], 12 | 13 | "author": { 14 | "name": "dimbslmh", 15 | "image": "https://avatars1.githubusercontent.com/u/745635" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/page-jump-to/bootstrap-table-page-jump-to.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Jay 3 | * @update zhixin wen 4 | */ 5 | 6 | const Utils = $.fn.bootstrapTable.utils 7 | 8 | $.extend($.fn.bootstrapTable.defaults, { 9 | showJumpTo: false 10 | }) 11 | 12 | $.extend($.fn.bootstrapTable.locales, { 13 | formatJumpTo () { 14 | return 'GO' 15 | } 16 | }) 17 | $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales) 18 | 19 | $.BootstrapTable = class extends $.BootstrapTable { 20 | initPagination (...args) { 21 | super.initPagination(...args) 22 | 23 | if (this.options.showJumpTo) { 24 | const $pageGroup = this.$pagination.find('> .pagination') 25 | let $jumpTo = $pageGroup.find('.page-jump-to') 26 | 27 | if (!$jumpTo.length) { 28 | $jumpTo = $(` 29 |
30 | 31 | 34 |
35 | `).appendTo($pageGroup) 36 | 37 | $jumpTo.on('click', 'button', (e) => { 38 | this.selectPage(+$(e.target).parent('.page-jump-to').find('input').val()) 39 | }) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/page-jump-to/bootstrap-table-page-jump-to.scss: -------------------------------------------------------------------------------- 1 | .bootstrap-table.bootstrap3 .fixed-table-pagination > .pagination ul.pagination, 2 | .bootstrap-table.bootstrap3 .fixed-table-pagination > .pagination .page-jump-to { 3 | display: inline; 4 | } 5 | 6 | .bootstrap-table .fixed-table-pagination > .pagination .page-jump-to input { 7 | width: 70px; 8 | margin-left: 5px; 9 | text-align: center; 10 | float: left; 11 | } 12 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/pipeline/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2019 doug-the-guy 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/pipeline/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pipeline", 3 | "version": "1.0.0", 4 | "description": "Plugin to support a hybrid approach to server/client side paging.", 5 | "url": "", 6 | "example": "#", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-pipeline", 10 | "url": "" 11 | }], 12 | 13 | "author": { 14 | "name": "doug-the-guy", 15 | "image": "" 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/reorder-columns/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Reorder Columns", 3 | "version": "1.1.0", 4 | "description": "Plugin to support the reordering columns feature.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-columns", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/reorder-columns.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-reorder-columns", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-columns" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/reorder-rows/bootstrap-table-reorder-rows.scss: -------------------------------------------------------------------------------- 1 | .reorder_rows_onDragClass td { 2 | background-color: #eee; 3 | -webkit-box-shadow: 11px 5px 12px 2px #333, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset; 4 | -webkit-box-shadow: 6px 3px 5px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset; 5 | -moz-box-shadow: 6px 4px 5px 1px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset; 6 | -box-shadow: 6px 4px 5px 1px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset; 7 | } 8 | 9 | .reorder_rows_onDragClass td:last-child { 10 | -webkit-box-shadow: 8px 7px 12px 0 #333, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset; 11 | -webkit-box-shadow: 1px 8px 6px -4px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset; 12 | -moz-box-shadow: 0 9px 4px -4px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset, -1px 0 0 #ccc inset; 13 | -box-shadow: 0 9px 4px -4px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset, -1px 0 0 #ccc inset; 14 | } 15 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/reorder-rows/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Reorder Rows", 3 | "version": "1.0.0", 4 | "description": "Plugin to support the reordering rows feature.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-rows", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/reorder-rows.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-reorder-rows", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-rows" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/resizable/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Resizable", 3 | "version": "1.1.0", 4 | "description": "Plugin to support the resizable feature.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/resizable", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/resizable.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-resizable", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/resizable" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @author vincent loh 3 | * @update zhixin wen 4 | */ 5 | 6 | .fix-sticky { 7 | position: fixed !important; 8 | overflow: hidden; 9 | z-index: 100; 10 | } 11 | 12 | .fix-sticky table thead { 13 | background: #fff; 14 | } 15 | 16 | .fix-sticky table thead.thead-light { 17 | background: #e9ecef; 18 | } 19 | 20 | .fix-sticky table thead.thead-dark { 21 | background: #212529; 22 | } 23 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/sticky-header/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sticky Header", 3 | "version": "1.0.0", 4 | "description": "An extension which provides a sticky header for table columns when scrolling on a long page and / or table. Works for tables with many columns and narrow width with horizontal scrollbars too.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/sticky-header.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-sticky-header", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header" 11 | }], 12 | 13 | "author": { 14 | "name": "vinzloh", 15 | "image": "https://avatars0.githubusercontent.com/u/5501845" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /assets/bootstrap/js/bootstrap-table/extensions/toolbar/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Toolbar", 3 | "version": "2.0.0", 4 | "description": "Plugin to support the advanced search.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/toolbar.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-toolbar", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } -------------------------------------------------------------------------------- /assets/bootstrap/js/jquery-treegrid/jquery.treegrid.min.css: -------------------------------------------------------------------------------- 1 | .treegrid-indent{width:16px;height:16px;display:inline-block;position:relative}.treegrid-expander{width:16px;height:16px;display:inline-block;position:relative;cursor:pointer} -------------------------------------------------------------------------------- /assets/bootstrap/js/jquery.cookie.min.js: -------------------------------------------------------------------------------- 1 | /*! jquery.cookie v1.4.1 | MIT */ 2 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?a(require("jquery")):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}}); -------------------------------------------------------------------------------- /cmd/gormgen/README.MD: -------------------------------------------------------------------------------- 1 | ## 执行命令 2 | 在根目录下执行脚本:`./scripts/gormgen.sh addr user pass name tables`; 3 | - addr:数据库地址,例如:127.0.0.1:3306 4 | - user:账号,例如:root 5 | - pass:密码,例如:root 6 | - name:数据库名称,例如:go_gin_api 7 | - tables:表名,默认为 *,多个表名可用“,”分割,例如:user_demo 8 | 9 | 例如: 10 | ``` 11 | ./scripts/gormgen.sh 127.0.0.1:3306 root root go_gin_api user_demo 12 | ``` 13 | 14 | ## 参考 15 | - https://github.com/MohamedBassem/gormgen -------------------------------------------------------------------------------- /cmd/gormgen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/xinliangnote/go-gin-api/cmd/gormgen/pkg" 10 | ) 11 | 12 | var ( 13 | input string 14 | structs []string 15 | ) 16 | 17 | func init() { 18 | flagStructs := flag.String("structs", "", "[Required] The name of schema structs to generate structs for, comma seperated\n") 19 | flagInput := flag.String("input", "", "[Required] The name of the input file dir\n") 20 | flag.Parse() 21 | 22 | if *flagStructs == "" || *flagInput == "" { 23 | flag.Usage() 24 | os.Exit(1) 25 | } 26 | 27 | structs = strings.Split(*flagStructs, ",") 28 | input = *flagInput 29 | } 30 | 31 | func main() { 32 | gen := pkg.NewGenerator(input) 33 | p := pkg.NewParser(input) 34 | if err := gen.ParserAST(p, structs).Generate().Format().Flush(); err != nil { 35 | log.Fatalln(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/gormgen/pkg/utils.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "strings" 4 | 5 | // SQLColumnToHumpStyle sql转换成驼峰模式 6 | func SQLColumnToHumpStyle(in string) (ret string) { 7 | for i := 0; i < len(in); i++ { 8 | if i > 0 && in[i-1] == '_' && in[i] != '_' { 9 | s := strings.ToUpper(string(in[i])) 10 | ret += s 11 | } else if in[i] == '_' { 12 | continue 13 | } else { 14 | ret += string(in[i]) 15 | } 16 | } 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /cmd/mysqlmd/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/xinliangnote/go-gin-api/pkg/errors" 7 | 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/schema" 11 | ) 12 | 13 | var _ Repo = (*dbRepo)(nil) 14 | 15 | type Repo interface { 16 | i() 17 | GetDb() *gorm.DB 18 | DbClose() error 19 | } 20 | 21 | type dbRepo struct { 22 | DbConn *gorm.DB 23 | } 24 | 25 | func New(dbAddr, dbUser, dbPass, dbName string) (Repo, error) { 26 | dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=%t&loc=%s", 27 | dbUser, 28 | dbPass, 29 | dbAddr, 30 | dbName, 31 | true, 32 | "Local") 33 | 34 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ 35 | NamingStrategy: schema.NamingStrategy{ 36 | SingularTable: true, 37 | }, 38 | //Logger: logger.Default.LogMode(logger.Info), // 日志配置 39 | }) 40 | 41 | if err != nil { 42 | return nil, errors.Wrap(err, fmt.Sprintf("[db connection failed] Database name: %s", dbName)) 43 | } 44 | 45 | db.Set("gorm:table_options", "CHARSET=utf8mb4") 46 | 47 | return &dbRepo{ 48 | DbConn: db, 49 | }, nil 50 | } 51 | 52 | func (d *dbRepo) i() {} 53 | 54 | func (d *dbRepo) GetDb() *gorm.DB { 55 | return d.DbConn 56 | } 57 | 58 | func (d *dbRepo) DbClose() error { 59 | sqlDB, err := d.DbConn.DB() 60 | if err != nil { 61 | return err 62 | } 63 | return sqlDB.Close() 64 | } 65 | -------------------------------------------------------------------------------- /configs/constants.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import "time" 4 | 5 | const ( 6 | // MinGoVersion 最小 Go 版本 7 | MinGoVersion = 1.16 8 | 9 | // ProjectVersion 项目版本 10 | ProjectVersion = "v1.2.8" 11 | 12 | // ProjectName 项目名称 13 | ProjectName = "go-gin-api" 14 | 15 | // ProjectDomain 项目域名 16 | ProjectDomain = "http://127.0.0.1" 17 | 18 | // ProjectPort 项目端口 19 | ProjectPort = ":9999" 20 | 21 | // ProjectAccessLogFile 项目访问日志存放文件 22 | ProjectAccessLogFile = "./logs/" + ProjectName + "-access.log" 23 | 24 | // ProjectCronLogFile 项目后台任务日志存放文件 25 | ProjectCronLogFile = "./logs/" + ProjectName + "-cron.log" 26 | 27 | // ProjectInstallMark 项目安装完成标识 28 | ProjectInstallMark = "INSTALL.lock" 29 | 30 | // HeaderLoginToken 登录验证 Token,Header 中传递的参数 31 | HeaderLoginToken = "Token" 32 | 33 | // HeaderSignToken 签名验证 Authorization,Header 中传递的参数 34 | HeaderSignToken = "Authorization" 35 | 36 | // HeaderSignTokenDate 签名验证 Date,Header 中传递的参数 37 | HeaderSignTokenDate = "Authorization-Date" 38 | 39 | // HeaderSignTokenTimeout 签名有效期为 2 分钟 40 | HeaderSignTokenTimeout = time.Minute * 2 41 | 42 | // RedisKeyPrefixLoginUser Redis Key 前缀 - 登录用户信息 43 | RedisKeyPrefixLoginUser = ProjectName + ":login-user:" 44 | 45 | // RedisKeyPrefixSignature Redis Key 前缀 - 签名验证信息 46 | RedisKeyPrefixSignature = ProjectName + ":signature:" 47 | 48 | // ZhCN 简体中文 - 中国 49 | ZhCN = "zh-cn" 50 | 51 | // EnUS 英文 - 美国 52 | EnUS = "en-us" 53 | 54 | // MaxRequestsPerSecond 每秒最大请求量 55 | MaxRequestsPerSecond = 10000 56 | 57 | // LoginSessionTTL 登录有效期为 24 小时 58 | LoginSessionTTL = time.Hour * 24 59 | ) 60 | -------------------------------------------------------------------------------- /configs/dev_configs.toml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /configs/fat_configs.toml: -------------------------------------------------------------------------------- 1 | 2 | [hashids] 3 | length = 12 4 | secret = "6ab6122836cfef95f8db" 5 | 6 | [language] 7 | local = "zh-cn" 8 | 9 | [mail] 10 | host = "smtp.163.com" 11 | pass = "" 12 | port = 465 13 | to = "" 14 | user = "" 15 | 16 | [mysql] 17 | 18 | [mysql.base] 19 | connmaxlifetime = 60 20 | maxidleconn = 60 21 | maxopenconn = 10 22 | 23 | [mysql.read] 24 | addr = "127.0.0.1:3306" 25 | name = "go_gin_api" 26 | pass = "123456789" 27 | user = "root" 28 | 29 | [mysql.write] 30 | addr = "127.0.0.1:3306" 31 | name = "go_gin_api" 32 | pass = "123456789" 33 | user = "root" 34 | 35 | [redis] 36 | addr = "127.0.0.1:6379" 37 | db = "0" 38 | maxretries = 3 39 | minidleconns = 5 40 | pass = "" 41 | poolsize = 10 42 | -------------------------------------------------------------------------------- /configs/pro_configs.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /configs/uat_configs.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /deployments/loki/loki.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | http_listen_port: 3100 5 | 6 | ingester: 7 | lifecycler: 8 | address: 127.0.0.1 9 | ring: 10 | kvstore: 11 | store: inmemory 12 | replication_factor: 1 13 | final_sleep: 0s 14 | chunk_idle_period: 5m 15 | chunk_retain_period: 30s 16 | 17 | schema_config: 18 | configs: 19 | - from: 2020-01-01 20 | store: boltdb 21 | object_store: filesystem 22 | schema: v9 23 | index: 24 | prefix: index_ 25 | period: 168h # 每张表的时间范围6天 26 | 27 | storage_config: 28 | boltdb: 29 | directory: /data/loki/index # 索引文件存储地址 30 | 31 | filesystem: 32 | directory: /data/loki/chunks # 块存储地址 33 | 34 | limits_config: 35 | enforce_metric_name: false 36 | reject_old_samples: true 37 | reject_old_samples_max_age: 168h 38 | 39 | chunk_store_config: 40 | max_look_back_period: 0s 41 | 42 | table_manager: 43 | retention_deletes_enabled: false 44 | retention_period: 0s -------------------------------------------------------------------------------- /deployments/loki/promtail.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 9080 3 | grpc_listen_port: 0 4 | 5 | # Positions 6 | positions: 7 | filename: /data/loki/positions.yaml 8 | 9 | # Loki服务器的地址 10 | clients: 11 | - url: http://127.0.0.1:3100/loki/api/v1/push 12 | 13 | scrape_configs: 14 | - job_name: go-gin-api 15 | static_configs: 16 | - targets: 17 | - localhost 18 | labels: 19 | job: accesslog 20 | __path__: /data/logs/*.log # 日志目录 -------------------------------------------------------------------------------- /deployments/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | scrape_configs: 6 | - job_name: "prometheus" 7 | static_configs: 8 | - targets: ["127.0.0.1:9090"] 9 | - job_name: "go_app_server" 10 | static_configs: 11 | - targets: 12 | - "127.0.0.1:9999" 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xinliangnote/go-gin-api 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.14.0 7 | github.com/StackExchange/wmi v1.2.1 // indirect 8 | github.com/dave/dst v0.26.2 9 | github.com/fsnotify/fsnotify v1.5.1 10 | github.com/gin-contrib/pprof v1.3.0 11 | github.com/gin-gonic/gin v1.7.4 12 | github.com/go-playground/locales v0.14.0 13 | github.com/go-playground/universal-translator v0.18.0 14 | github.com/go-playground/validator/v10 v10.9.0 15 | github.com/go-redis/redis/v7 v7.4.1 16 | github.com/gorilla/websocket v1.4.2 17 | github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5 18 | github.com/jinzhu/gorm v1.9.16 19 | github.com/pkg/errors v0.9.1 20 | github.com/prometheus/client_golang v1.11.0 21 | github.com/rs/cors v1.8.0 22 | github.com/shirou/gopsutil v3.21.10+incompatible 23 | github.com/speps/go-hashids v1.0.0 24 | github.com/spf13/cast v1.4.1 25 | github.com/spf13/viper v1.9.0 26 | github.com/swaggo/gin-swagger v1.3.3 27 | github.com/swaggo/swag v1.7.4 28 | github.com/tklauser/go-sysconf v0.3.9 // indirect 29 | github.com/vektah/gqlparser/v2 v2.2.0 30 | go.uber.org/multierr v1.7.0 31 | go.uber.org/zap v1.19.1 32 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 33 | golang.org/x/tools v0.1.7 34 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 35 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 36 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 37 | gorm.io/driver/mysql v1.2.0 38 | gorm.io/gorm v1.22.3 39 | ) 40 | -------------------------------------------------------------------------------- /internal/alert/alert.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/proposal" 6 | "github.com/xinliangnote/go-gin-api/pkg/errors" 7 | "github.com/xinliangnote/go-gin-api/pkg/mail" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | // NotifyHandler 告警通知 13 | func NotifyHandler(logger *zap.Logger) func(msg *proposal.AlertMessage) { 14 | if logger == nil { 15 | panic("logger required") 16 | } 17 | 18 | return func(msg *proposal.AlertMessage) { 19 | cfg := configs.Get().Mail 20 | if cfg.Host == "" || cfg.Port == 0 || cfg.User == "" || cfg.Pass == "" || cfg.To == "" { 21 | logger.Error("Mail config error") 22 | return 23 | } 24 | 25 | subject, body, err := newHTMLEmail( 26 | msg.Method, 27 | msg.HOST, 28 | msg.URI, 29 | msg.TraceID, 30 | msg.ErrorMessage, 31 | msg.ErrorStack, 32 | ) 33 | if err != nil { 34 | logger.Error("email template error", zap.Error(err)) 35 | return 36 | } 37 | 38 | options := &mail.Options{ 39 | MailHost: cfg.Host, 40 | MailPort: cfg.Port, 41 | MailUser: cfg.User, 42 | MailPass: cfg.Pass, 43 | MailTo: cfg.To, 44 | Subject: subject, 45 | Body: body, 46 | } 47 | if err := mail.Send(options); err != nil { 48 | logger.Error("发送告警通知邮件失败", zap.Error(errors.WithStack(err))) 49 | } 50 | 51 | return 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/api/admin/func_delete.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type deleteRequest struct { 11 | Id string `uri:"id"` // HashID 12 | } 13 | 14 | type deleteResponse struct { 15 | Id int32 `json:"id"` // 主键ID 16 | } 17 | 18 | // Delete 删除管理员 19 | // @Summary 删除管理员 20 | // @Description 删除管理员 21 | // @Tags API.admin 22 | // @Accept json 23 | // @Produce json 24 | // @Param id path string true "hashId" 25 | // @Success 200 {object} deleteResponse 26 | // @Failure 400 {object} code.Failure 27 | // @Router /api/admin/{id} [delete] 28 | // @Security LoginToken 29 | func (h *handler) Delete() core.HandlerFunc { 30 | return func(c core.Context) { 31 | req := new(deleteRequest) 32 | res := new(deleteResponse) 33 | if err := c.ShouldBindURI(req); err != nil { 34 | c.AbortWithError(core.Error( 35 | http.StatusBadRequest, 36 | code.ParamBindError, 37 | code.Text(code.ParamBindError)).WithError(err), 38 | ) 39 | return 40 | } 41 | 42 | ids, err := h.hashids.HashidsDecode(req.Id) 43 | if err != nil { 44 | c.AbortWithError(core.Error( 45 | http.StatusBadRequest, 46 | code.HashIdsDecodeError, 47 | code.Text(code.HashIdsDecodeError)).WithError(err), 48 | ) 49 | return 50 | } 51 | 52 | id := int32(ids[0]) 53 | 54 | err = h.adminService.Delete(c, id) 55 | if err != nil { 56 | c.AbortWithError(core.Error( 57 | http.StatusBadRequest, 58 | code.AdminDeleteError, 59 | code.Text(code.AdminDeleteError)).WithError(err), 60 | ) 61 | return 62 | } 63 | 64 | res.Id = id 65 | c.Payload(res) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/api/admin/func_logout.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/configs" 7 | "github.com/xinliangnote/go-gin-api/internal/code" 8 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 10 | "github.com/xinliangnote/go-gin-api/pkg/errors" 11 | ) 12 | 13 | type logoutResponse struct { 14 | Username string `json:"username"` // 用户账号 15 | } 16 | 17 | // Logout 管理员登出 18 | // @Summary 管理员登出 19 | // @Description 管理员登出 20 | // @Tags API.admin 21 | // @Accept application/x-www-form-urlencoded 22 | // @Produce json 23 | // @Success 200 {object} logoutResponse 24 | // @Failure 400 {object} code.Failure 25 | // @Router /api/admin/logout [post] 26 | // @Security LoginToken 27 | func (h *handler) Logout() core.HandlerFunc { 28 | return func(c core.Context) { 29 | res := new(logoutResponse) 30 | res.Username = c.SessionUserInfo().UserName 31 | 32 | if !h.cache.Del(configs.RedisKeyPrefixLoginUser+c.GetHeader(configs.HeaderLoginToken), redis.WithTrace(c.Trace())) { 33 | c.AbortWithError(core.Error( 34 | http.StatusBadRequest, 35 | code.AdminLogOutError, 36 | code.Text(code.AdminLogOutError)).WithError(errors.New("cache del err")), 37 | ) 38 | return 39 | } 40 | 41 | c.Payload(res) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/api/admin/func_modifypersonalinfo.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | "github.com/xinliangnote/go-gin-api/internal/services/admin" 9 | ) 10 | 11 | type modifyPersonalInfoRequest struct { 12 | Nickname string `form:"nickname"` // 昵称 13 | Mobile string `form:"mobile"` // 手机号 14 | } 15 | 16 | type modifyPersonalInfoResponse struct { 17 | Username string `json:"username"` // 用户账号 18 | } 19 | 20 | // ModifyPersonalInfo 修改个人信息 21 | // @Summary 修改个人信息 22 | // @Description 修改个人信息 23 | // @Tags API.admin 24 | // @Accept application/x-www-form-urlencoded 25 | // @Produce json 26 | // @Param nickname formData string true "昵称" 27 | // @Param mobile formData string true "手机号" 28 | // @Success 200 {object} modifyPersonalInfoResponse 29 | // @Failure 400 {object} code.Failure 30 | // @Router /api/admin/modify_personal_info [patch] 31 | // @Security LoginToken 32 | func (h *handler) ModifyPersonalInfo() core.HandlerFunc { 33 | return func(ctx core.Context) { 34 | req := new(modifyPersonalInfoRequest) 35 | res := new(modifyPersonalInfoResponse) 36 | if err := ctx.ShouldBindForm(req); err != nil { 37 | ctx.AbortWithError(core.Error( 38 | http.StatusBadRequest, 39 | code.ParamBindError, 40 | code.Text(code.ParamBindError)).WithError(err), 41 | ) 42 | return 43 | } 44 | 45 | modifyData := new(admin.ModifyData) 46 | modifyData.Nickname = req.Nickname 47 | modifyData.Mobile = req.Mobile 48 | 49 | if err := h.adminService.ModifyPersonalInfo(ctx, ctx.SessionUserInfo().UserID, modifyData); err != nil { 50 | ctx.AbortWithError(core.Error( 51 | http.StatusBadRequest, 52 | code.AdminModifyPersonalInfoError, 53 | code.Text(code.AdminModifyPersonalInfoError)).WithError(err), 54 | ) 55 | return 56 | } 57 | 58 | res.Username = ctx.SessionUserInfo().UserName 59 | ctx.Payload(res) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/api/admin/func_offline.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/configs" 7 | "github.com/xinliangnote/go-gin-api/internal/code" 8 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 9 | "github.com/xinliangnote/go-gin-api/internal/pkg/password" 10 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 11 | ) 12 | 13 | type offlineRequest struct { 14 | Id string `form:"id"` // 主键ID 15 | } 16 | 17 | type offlineResponse struct { 18 | Id int32 `json:"id"` // 主键ID 19 | } 20 | 21 | // Offline 下线管理员 22 | // @Summary 下线管理员 23 | // @Description 下线管理员 24 | // @Tags API.admin 25 | // @Accept application/x-www-form-urlencoded 26 | // @Produce json 27 | // @Param id formData string true "Hashid" 28 | // @Success 200 {object} offlineResponse 29 | // @Failure 400 {object} code.Failure 30 | // @Router /api/admin/offline [patch] 31 | // @Security LoginToken 32 | func (h *handler) Offline() core.HandlerFunc { 33 | return func(c core.Context) { 34 | req := new(offlineRequest) 35 | res := new(offlineResponse) 36 | if err := c.ShouldBindForm(req); err != nil { 37 | c.AbortWithError(core.Error( 38 | http.StatusBadRequest, 39 | code.ParamBindError, 40 | code.Text(code.ParamBindError)).WithError(err), 41 | ) 42 | return 43 | } 44 | 45 | ids, err := h.hashids.HashidsDecode(req.Id) 46 | if err != nil { 47 | c.AbortWithError(core.Error( 48 | http.StatusBadRequest, 49 | code.HashIdsDecodeError, 50 | code.Text(code.HashIdsDecodeError)).WithError(err), 51 | ) 52 | return 53 | } 54 | 55 | id := int32(ids[0]) 56 | 57 | b := h.cache.Del(configs.RedisKeyPrefixLoginUser+password.GenerateLoginToken(id), redis.WithTrace(c.Trace())) 58 | if !b { 59 | c.AbortWithError(core.Error( 60 | http.StatusBadRequest, 61 | code.AdminOfflineError, 62 | code.Text(code.AdminOfflineError)), 63 | ) 64 | return 65 | } 66 | 67 | res.Id = id 68 | c.Payload(res) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/api/admin/func_resetpassword.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type resetPasswordRequest struct { 11 | Id string `uri:"id"` // HashID 12 | } 13 | 14 | type resetPasswordResponse struct { 15 | Id int32 `json:"id"` // 主键ID 16 | } 17 | 18 | // ResetPassword 重置密码 19 | // @Summary 重置密码 20 | // @Description 重置密码 21 | // @Tags API.admin 22 | // @Accept json 23 | // @Produce json 24 | // @Param id path string true "hashId" 25 | // @Success 200 {object} resetPasswordResponse 26 | // @Failure 400 {object} code.Failure 27 | // @Router /api/admin/reset_password/{id} [patch] 28 | // @Security LoginToken 29 | func (h *handler) ResetPassword() core.HandlerFunc { 30 | return func(c core.Context) { 31 | req := new(resetPasswordRequest) 32 | res := new(resetPasswordResponse) 33 | if err := c.ShouldBindURI(req); err != nil { 34 | c.AbortWithError(core.Error( 35 | http.StatusBadRequest, 36 | code.ParamBindError, 37 | code.Text(code.ParamBindError)).WithError(err), 38 | ) 39 | return 40 | } 41 | 42 | ids, err := h.hashids.HashidsDecode(req.Id) 43 | if err != nil { 44 | c.AbortWithError(core.Error( 45 | http.StatusBadRequest, 46 | code.HashIdsDecodeError, 47 | code.Text(code.HashIdsDecodeError)).WithError(err), 48 | ) 49 | return 50 | } 51 | 52 | id := int32(ids[0]) 53 | 54 | err = h.adminService.ResetPassword(c, id) 55 | if err != nil { 56 | c.AbortWithError(core.Error( 57 | http.StatusBadRequest, 58 | code.AdminResetPasswordError, 59 | code.Text(code.AdminResetPasswordError)).WithError(err), 60 | ) 61 | return 62 | } 63 | 64 | res.Id = id 65 | c.Payload(res) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/api/admin/func_updateused.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type updateUsedRequest struct { 11 | Id string `form:"id"` // 主键ID 12 | Used int32 `form:"used"` // 是否启用 1:是 -1:否 13 | } 14 | 15 | type updateUsedResponse struct { 16 | Id int32 `json:"id"` // 主键ID 17 | } 18 | 19 | // UpdateUsed 更新管理员为启用/禁用 20 | // @Summary 更新管理员为启用/禁用 21 | // @Description 更新管理员为启用/禁用 22 | // @Tags API.admin 23 | // @Accept application/x-www-form-urlencoded 24 | // @Produce json 25 | // @Param id formData string true "Hashid" 26 | // @Param used formData int true "是否启用 1:是 -1:否" 27 | // @Success 200 {object} updateUsedResponse 28 | // @Failure 400 {object} code.Failure 29 | // @Router /api/admin/used [patch] 30 | // @Security LoginToken 31 | func (h *handler) UpdateUsed() core.HandlerFunc { 32 | return func(c core.Context) { 33 | req := new(updateUsedRequest) 34 | res := new(updateUsedResponse) 35 | if err := c.ShouldBindForm(req); err != nil { 36 | c.AbortWithError(core.Error( 37 | http.StatusBadRequest, 38 | code.ParamBindError, 39 | code.Text(code.ParamBindError)).WithError(err), 40 | ) 41 | return 42 | } 43 | 44 | ids, err := h.hashids.HashidsDecode(req.Id) 45 | if err != nil { 46 | c.AbortWithError(core.Error( 47 | http.StatusBadRequest, 48 | code.HashIdsDecodeError, 49 | code.Text(code.HashIdsDecodeError)).WithError(err), 50 | ) 51 | return 52 | } 53 | 54 | id := int32(ids[0]) 55 | 56 | err = h.adminService.UpdateUsed(c, id, req.Used) 57 | if err != nil { 58 | c.AbortWithError(core.Error( 59 | http.StatusBadRequest, 60 | code.AdminUpdateError, 61 | code.Text(code.AdminUpdateError)).WithError(err), 62 | ) 63 | return 64 | } 65 | 66 | res.Id = id 67 | c.Payload(res) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/api/authorized/func_delete.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type deleteRequest struct { 11 | Id string `uri:"id"` // HashID 12 | } 13 | 14 | type deleteResponse struct { 15 | Id int32 `json:"id"` // 主键ID 16 | } 17 | 18 | // Delete 删除调用方 19 | // @Summary 删除调用方 20 | // @Description 删除调用方 21 | // @Tags API.authorized 22 | // @Accept json 23 | // @Produce json 24 | // @Param id path string true "hashId" 25 | // @Success 200 {object} deleteResponse 26 | // @Failure 400 {object} code.Failure 27 | // @Router /api/authorized/{id} [delete] 28 | // @Security LoginToken 29 | func (h *handler) Delete() core.HandlerFunc { 30 | return func(c core.Context) { 31 | req := new(deleteRequest) 32 | res := new(deleteResponse) 33 | if err := c.ShouldBindURI(req); err != nil { 34 | c.AbortWithError(core.Error( 35 | http.StatusBadRequest, 36 | code.ParamBindError, 37 | code.Text(code.ParamBindError)).WithError(err), 38 | ) 39 | return 40 | } 41 | 42 | ids, err := h.hashids.HashidsDecode(req.Id) 43 | if err != nil { 44 | c.AbortWithError(core.Error( 45 | http.StatusBadRequest, 46 | code.HashIdsDecodeError, 47 | code.Text(code.HashIdsDecodeError)).WithError(err), 48 | ) 49 | return 50 | } 51 | 52 | id := int32(ids[0]) 53 | 54 | err = h.authorizedService.Delete(c, id) 55 | if err != nil { 56 | c.AbortWithError(core.Error( 57 | http.StatusBadRequest, 58 | code.AuthorizedDeleteError, 59 | code.Text(code.AuthorizedDeleteError)).WithError(err), 60 | ) 61 | return 62 | } 63 | 64 | res.Id = id 65 | c.Payload(res) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/api/authorized/func_deleteapi.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type deleteAPIRequest struct { 11 | Id string `uri:"id"` // HashID 12 | } 13 | 14 | type deleteAPIResponse struct { 15 | Id int32 `json:"id"` // 主键ID 16 | } 17 | 18 | // DeleteAPI 删除调用方接口地址 19 | // @Summary 删除调用方接口地址 20 | // @Description 删除调用方接口地址 21 | // @Tags API.authorized 22 | // @Accept json 23 | // @Produce json 24 | // @Param id path string true "主键ID" 25 | // @Success 200 {object} deleteAPIResponse 26 | // @Failure 400 {object} code.Failure 27 | // @Router /api/authorized_api/{id} [delete] 28 | // @Security LoginToken 29 | func (h *handler) DeleteAPI() core.HandlerFunc { 30 | return func(c core.Context) { 31 | req := new(deleteAPIRequest) 32 | res := new(deleteAPIResponse) 33 | if err := c.ShouldBindURI(req); err != nil { 34 | c.AbortWithError(core.Error( 35 | http.StatusBadRequest, 36 | code.ParamBindError, 37 | code.Text(code.ParamBindError)).WithError(err), 38 | ) 39 | return 40 | } 41 | 42 | ids, err := h.hashids.HashidsDecode(req.Id) 43 | if err != nil { 44 | c.AbortWithError(core.Error( 45 | http.StatusBadRequest, 46 | code.HashIdsDecodeError, 47 | code.Text(code.HashIdsDecodeError)).WithError(err), 48 | ) 49 | return 50 | } 51 | 52 | id := int32(ids[0]) 53 | 54 | err = h.authorizedService.DeleteAPI(c, id) 55 | if err != nil { 56 | c.AbortWithError(core.Error( 57 | http.StatusBadRequest, 58 | code.AuthorizedDeleteAPIError, 59 | code.Text(code.AuthorizedDeleteAPIError)).WithError(err), 60 | ) 61 | return 62 | } 63 | 64 | res.Id = id 65 | c.Payload(res) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/api/authorized/func_updateused.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type updateUsedRequest struct { 11 | Id string `form:"id"` // 主键ID 12 | Used int32 `form:"used"` // 是否启用 1:是 -1:否 13 | } 14 | 15 | type updateUsedResponse struct { 16 | Id int32 `json:"id"` // 主键ID 17 | } 18 | 19 | // UpdateUsed 更新调用方为启用/禁用 20 | // @Summary 更新调用方为启用/禁用 21 | // @Description 更新调用方为启用/禁用 22 | // @Tags API.authorized 23 | // @Accept application/x-www-form-urlencoded 24 | // @Produce json 25 | // @Param id formData string true "hashID" 26 | // @Param used formData int true "是否启用 1:是 -1:否" 27 | // @Success 200 {object} updateUsedResponse 28 | // @Failure 400 {object} code.Failure 29 | // @Router /api/authorized/used [patch] 30 | // @Security LoginToken 31 | func (h *handler) UpdateUsed() core.HandlerFunc { 32 | return func(c core.Context) { 33 | req := new(updateUsedRequest) 34 | res := new(updateUsedResponse) 35 | if err := c.ShouldBindForm(req); err != nil { 36 | c.AbortWithError(core.Error( 37 | http.StatusBadRequest, 38 | code.ParamBindError, 39 | code.Text(code.ParamBindError)).WithError(err), 40 | ) 41 | return 42 | } 43 | 44 | ids, err := h.hashids.HashidsDecode(req.Id) 45 | if err != nil { 46 | c.AbortWithError(core.Error( 47 | http.StatusBadRequest, 48 | code.HashIdsDecodeError, 49 | code.Text(code.HashIdsDecodeError)).WithError(err), 50 | ) 51 | return 52 | } 53 | 54 | id := int32(ids[0]) 55 | 56 | err = h.authorizedService.UpdateUsed(c, id, req.Used) 57 | if err != nil { 58 | c.AbortWithError(core.Error( 59 | http.StatusBadRequest, 60 | code.AuthorizedUpdateError, 61 | code.Text(code.AuthorizedUpdateError)).WithError(err), 62 | ) 63 | return 64 | } 65 | 66 | res.Id = id 67 | c.Payload(res) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/api/config/handler.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 7 | 8 | "go.uber.org/zap" 9 | ) 10 | 11 | var _ Handler = (*handler)(nil) 12 | 13 | type Handler interface { 14 | i() 15 | 16 | // Email 修改邮件配置 17 | // @Tags API.config 18 | // @Router /api/config/email [patch] 19 | Email() core.HandlerFunc 20 | } 21 | 22 | type handler struct { 23 | logger *zap.Logger 24 | cache redis.Repo 25 | } 26 | 27 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo) Handler { 28 | return &handler{ 29 | logger: logger, 30 | cache: cache, 31 | } 32 | } 33 | 34 | func (h *handler) i() {} 35 | -------------------------------------------------------------------------------- /internal/api/cron/func_execute.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | "github.com/xinliangnote/go-gin-api/internal/pkg/validation" 9 | 10 | "github.com/spf13/cast" 11 | ) 12 | 13 | type executeRequest struct { 14 | Id string `uri:"id"` // HashID 15 | } 16 | 17 | type executeResponse struct { 18 | Id int `json:"id"` // ID 19 | } 20 | 21 | // Execute 手动执行单条任务 22 | // @Summary 手动执行单条任务 23 | // @Description 手动执行单条任务 24 | // @Tags API.cron 25 | // @Accept json 26 | // @Produce json 27 | // @Param id path string true "hashId" 28 | // @Success 200 {object} detailResponse 29 | // @Failure 400 {object} code.Failure 30 | // @Router /api/cron/exec/{id} [patch] 31 | // @Security LoginToken 32 | func (h *handler) Execute() core.HandlerFunc { 33 | return func(ctx core.Context) { 34 | req := new(executeRequest) 35 | res := new(executeResponse) 36 | if err := ctx.ShouldBindURI(req); err != nil { 37 | ctx.AbortWithError(core.Error( 38 | http.StatusBadRequest, 39 | code.ParamBindError, 40 | validation.Error(err)).WithError(err), 41 | ) 42 | return 43 | } 44 | 45 | ids, err := h.hashids.HashidsDecode(req.Id) 46 | if err != nil { 47 | ctx.AbortWithError(core.Error( 48 | http.StatusBadRequest, 49 | code.HashIdsDecodeError, 50 | code.Text(code.HashIdsDecodeError)).WithError(err), 51 | ) 52 | return 53 | } 54 | 55 | err = h.cronService.Execute(ctx, cast.ToInt32(ids[0])) 56 | if err != nil { 57 | ctx.AbortWithError(core.Error( 58 | http.StatusBadRequest, 59 | code.CronExecuteError, 60 | code.Text(code.CronExecuteError)).WithError(err), 61 | ) 62 | return 63 | } 64 | 65 | res.Id = ids[0] 66 | ctx.Payload(res) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/api/cron/func_updateused.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | "github.com/xinliangnote/go-gin-api/internal/pkg/validation" 9 | ) 10 | 11 | type updateUsedRequest struct { 12 | Id string `form:"id"` // 主键ID 13 | Used int32 `form:"used"` // 是否启用 1:是 -1:否 14 | } 15 | 16 | type updateUsedResponse struct { 17 | Id int32 `json:"id"` // 主键ID 18 | } 19 | 20 | // UpdateUsed 更新任务为启用/禁用 21 | // @Summary 更新任务为启用/禁用 22 | // @Description 更新任务为启用/禁用 23 | // @Tags API.cron 24 | // @Accept application/x-www-form-urlencoded 25 | // @Produce json 26 | // @Param id formData string true "hashID" 27 | // @Param used formData int true "是否启用 1:是 -1:否" 28 | // @Success 200 {object} updateUsedResponse 29 | // @Failure 400 {object} code.Failure 30 | // @Router /api/cron/used [patch] 31 | // @Security LoginToken 32 | func (h *handler) UpdateUsed() core.HandlerFunc { 33 | return func(ctx core.Context) { 34 | req := new(updateUsedRequest) 35 | res := new(updateUsedResponse) 36 | if err := ctx.ShouldBindForm(req); err != nil { 37 | ctx.AbortWithError(core.Error( 38 | http.StatusBadRequest, 39 | code.ParamBindError, 40 | validation.Error(err)).WithError(err), 41 | ) 42 | return 43 | } 44 | 45 | ids, err := h.hashids.HashidsDecode(req.Id) 46 | if err != nil { 47 | ctx.AbortWithError(core.Error( 48 | http.StatusBadRequest, 49 | code.HashIdsDecodeError, 50 | code.Text(code.HashIdsDecodeError)).WithError(err), 51 | ) 52 | return 53 | } 54 | 55 | id := int32(ids[0]) 56 | 57 | err = h.cronService.UpdateUsed(ctx, id, req.Used) 58 | if err != nil { 59 | ctx.AbortWithError(core.Error( 60 | http.StatusBadRequest, 61 | code.AdminUpdateError, 62 | code.Text(code.AdminUpdateError)).WithError(err), 63 | ) 64 | return 65 | } 66 | 67 | res.Id = id 68 | ctx.Payload(res) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/api/cron/handler.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | cronRepo "github.com/xinliangnote/go-gin-api/internal/repository/cron" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | "github.com/xinliangnote/go-gin-api/internal/services/cron" 10 | "github.com/xinliangnote/go-gin-api/pkg/hash" 11 | 12 | "go.uber.org/zap" 13 | ) 14 | 15 | var _ Handler = (*handler)(nil) 16 | 17 | type Handler interface { 18 | i() 19 | 20 | // Create 创建任务 21 | // @Tags API.cron 22 | // @Router /api/cron [post] 23 | Create() core.HandlerFunc 24 | 25 | // Modify 编辑任务 26 | // @Tags API.cron 27 | // @Router /api/cron/{id} [post] 28 | Modify() core.HandlerFunc 29 | 30 | // List 任务列表 31 | // @Tags API.cron 32 | // @Router /api/cron [get] 33 | List() core.HandlerFunc 34 | 35 | // UpdateUsed 更新任务为启用/禁用 36 | // @Tags API.cron 37 | // @Router /api/cron/used [patch] 38 | UpdateUsed() core.HandlerFunc 39 | 40 | // Detail 获取单条任务详情 41 | // @Tags API.cron 42 | // @Router /api/cron/{id} [get] 43 | Detail() core.HandlerFunc 44 | 45 | // Execute 手动执行任务 46 | // @Tags API.cron 47 | // @Router /api/cron/exec/{id} [patch] 48 | Execute() core.HandlerFunc 49 | } 50 | 51 | type handler struct { 52 | logger *zap.Logger 53 | cache redis.Repo 54 | hashids hash.Hash 55 | cronService cron.Service 56 | } 57 | 58 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo, cronServer cronRepo.Server) Handler { 59 | return &handler{ 60 | logger: logger, 61 | cache: cache, 62 | hashids: hash.New(configs.Get().HashIds.Secret, configs.Get().HashIds.Length), 63 | cronService: cron.New(db, cache, cronServer), 64 | } 65 | } 66 | 67 | func (h *handler) i() {} 68 | -------------------------------------------------------------------------------- /internal/api/helper/func_md5.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "net/http" 7 | 8 | "github.com/xinliangnote/go-gin-api/internal/code" 9 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 10 | ) 11 | 12 | type md5Request struct { 13 | Str string `uri:"str" binding:"required"` // 需要加密的字符串 14 | } 15 | 16 | type md5Response struct { 17 | Md5Str string `json:"md5_str"` // MD5后的字符串 18 | } 19 | 20 | // Md5 加密 21 | // @Summary 加密 22 | // @Description 加密 23 | // @Tags Helper 24 | // @Accept application/x-www-form-urlencoded 25 | // @Produce json 26 | // @Param str path string true "需要加密的字符串" 27 | // @Success 200 {object} md5Response 28 | // @Failure 400 {object} code.Failure 29 | // @Router /helper/md5/{str} [get] 30 | func (h *handler) Md5() core.HandlerFunc { 31 | return func(ctx core.Context) { 32 | req := new(md5Request) 33 | res := new(md5Response) 34 | 35 | if err := ctx.ShouldBindURI(req); err != nil { 36 | ctx.AbortWithError(core.Error( 37 | http.StatusBadRequest, 38 | code.ParamBindError, 39 | code.Text(code.ParamBindError)).WithError(err), 40 | ) 41 | return 42 | } 43 | 44 | m := md5.New() 45 | m.Write([]byte(req.Str)) 46 | res.Md5Str = hex.EncodeToString(m.Sum(nil)) 47 | ctx.Payload(res) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/api/helper/handler.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 7 | "github.com/xinliangnote/go-gin-api/internal/services/authorized" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | var _ Handler = (*handler)(nil) 13 | 14 | type Handler interface { 15 | i() 16 | 17 | // Md5 加密 18 | // @Tags Helper 19 | // @Router /helper/md5/{str} [get] 20 | Md5() core.HandlerFunc 21 | 22 | // Sign 签名 23 | // @Tags Helper 24 | // @Router /helper/sign [post] 25 | Sign() core.HandlerFunc 26 | } 27 | 28 | type handler struct { 29 | logger *zap.Logger 30 | db mysql.Repo 31 | authorizedService authorized.Service 32 | } 33 | 34 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo) Handler { 35 | return &handler{ 36 | logger: logger, 37 | db: db, 38 | authorizedService: authorized.New(db, cache), 39 | } 40 | } 41 | 42 | func (h *handler) i() {} 43 | -------------------------------------------------------------------------------- /internal/api/menu/func_delete.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type deleteRequest struct { 11 | Id string `uri:"id"` // HashID 12 | } 13 | 14 | type deleteResponse struct { 15 | Id int32 `json:"id"` // 主键ID 16 | } 17 | 18 | // Delete 删除菜单 19 | // @Summary 删除菜单 20 | // @Description 删除菜单 21 | // @Tags API.menu 22 | // @Accept json 23 | // @Produce json 24 | // @Param id path string true "hashId" 25 | // @Success 200 {object} deleteResponse 26 | // @Failure 400 {object} code.Failure 27 | // @Router /api/menu/{id} [delete] 28 | // @Security LoginToken 29 | func (h *handler) Delete() core.HandlerFunc { 30 | return func(c core.Context) { 31 | req := new(deleteRequest) 32 | res := new(deleteResponse) 33 | if err := c.ShouldBindURI(req); err != nil { 34 | c.AbortWithError(core.Error( 35 | http.StatusBadRequest, 36 | code.ParamBindError, 37 | code.Text(code.ParamBindError)).WithError(err), 38 | ) 39 | return 40 | } 41 | 42 | ids, err := h.hashids.HashidsDecode(req.Id) 43 | if err != nil { 44 | c.AbortWithError(core.Error( 45 | http.StatusBadRequest, 46 | code.HashIdsDecodeError, 47 | code.Text(code.HashIdsDecodeError)).WithError(err), 48 | ) 49 | return 50 | } 51 | 52 | id := int32(ids[0]) 53 | 54 | err = h.menuService.Delete(c, id) 55 | if err != nil { 56 | c.AbortWithError(core.Error( 57 | http.StatusBadRequest, 58 | code.MenuDeleteError, 59 | code.Text(code.MenuDeleteError)).WithError(err), 60 | ) 61 | return 62 | } 63 | 64 | res.Id = id 65 | c.Payload(res) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/api/menu/func_deleteaction.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type deleteActionRequest struct { 11 | Id string `uri:"id"` // HashID 12 | } 13 | 14 | type deleteActionResponse struct { 15 | Id int32 `json:"id"` // 主键ID 16 | } 17 | 18 | // DeleteAction 删除功能权限 19 | // @Summary 删除功能权限 20 | // @Description 删除功能权限 21 | // @Tags API.menu 22 | // @Accept json 23 | // @Produce json 24 | // @Param id path string true "hashId" 25 | // @Success 200 {object} deleteActionResponse 26 | // @Failure 400 {object} code.Failure 27 | // @Router /api/menu_action/{id} [delete] 28 | // @Security LoginToken 29 | func (h *handler) DeleteAction() core.HandlerFunc { 30 | return func(c core.Context) { 31 | req := new(deleteActionRequest) 32 | res := new(deleteActionResponse) 33 | if err := c.ShouldBindURI(req); err != nil { 34 | c.AbortWithError(core.Error( 35 | http.StatusBadRequest, 36 | code.ParamBindError, 37 | code.Text(code.ParamBindError)).WithError(err), 38 | ) 39 | return 40 | } 41 | 42 | ids, err := h.hashids.HashidsDecode(req.Id) 43 | if err != nil { 44 | c.AbortWithError(core.Error( 45 | http.StatusBadRequest, 46 | code.HashIdsDecodeError, 47 | code.Text(code.HashIdsDecodeError)).WithError(err), 48 | ) 49 | return 50 | } 51 | 52 | id := int32(ids[0]) 53 | 54 | err = h.menuService.DeleteAction(c, id) 55 | if err != nil { 56 | c.AbortWithError(core.Error( 57 | http.StatusBadRequest, 58 | code.MenuDeleteActionError, 59 | code.Text(code.MenuDeleteActionError)).WithError(err), 60 | ) 61 | return 62 | } 63 | 64 | res.Id = id 65 | c.Payload(res) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/api/menu/func_updatesort.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type updateSortRequest struct { 11 | Id string `form:"id"` // HashId 12 | Sort int32 `form:"sort"` // 排序 13 | } 14 | 15 | type updateSortResponse struct { 16 | Id int32 `json:"id"` // 主键ID 17 | } 18 | 19 | // UpdateSort 更新菜单排序 20 | // @Summary 更新菜单排序 21 | // @Description 更新菜单排序 22 | // @Tags API.menu 23 | // @Accept application/x-www-form-urlencoded 24 | // @Produce json 25 | // @Param id formData string true "hashId" 26 | // @Param sort formData int true "排序" 27 | // @Success 200 {object} updateSortResponse 28 | // @Failure 400 {object} code.Failure 29 | // @Router /api/menu/sort [patch] 30 | // @Security LoginToken 31 | func (h *handler) UpdateSort() core.HandlerFunc { 32 | return func(c core.Context) { 33 | req := new(updateSortRequest) 34 | res := new(updateSortResponse) 35 | if err := c.ShouldBindForm(req); err != nil { 36 | c.AbortWithError(core.Error( 37 | http.StatusBadRequest, 38 | code.ParamBindError, 39 | code.Text(code.ParamBindError)).WithError(err), 40 | ) 41 | return 42 | } 43 | 44 | ids, err := h.hashids.HashidsDecode(req.Id) 45 | if err != nil { 46 | c.AbortWithError(core.Error( 47 | http.StatusBadRequest, 48 | code.HashIdsDecodeError, 49 | code.Text(code.HashIdsDecodeError)).WithError(err), 50 | ) 51 | return 52 | } 53 | 54 | id := int32(ids[0]) 55 | 56 | err = h.menuService.UpdateSort(c, id, req.Sort) 57 | if err != nil { 58 | c.AbortWithError(core.Error( 59 | http.StatusBadRequest, 60 | code.MenuUpdateError, 61 | code.Text(code.MenuUpdateError)).WithError(err), 62 | ) 63 | return 64 | } 65 | 66 | res.Id = id 67 | c.Payload(res) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/api/menu/func_updateused.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type updateUsedRequest struct { 11 | Id string `form:"id"` // 主键ID 12 | Used int32 `form:"used"` // 是否启用 1:是 -1:否 13 | } 14 | 15 | type updateUsedResponse struct { 16 | Id int32 `json:"id"` // 主键ID 17 | } 18 | 19 | // UpdateUsed 更新菜单为启用/禁用 20 | // @Summary 更新菜单为启用/禁用 21 | // @Description 更新菜单为启用/禁用 22 | // @Tags API.menu 23 | // @Accept application/x-www-form-urlencoded 24 | // @Produce json 25 | // @Param id formData string true "hashId" 26 | // @Param used formData int true "是否启用 1:是 -1:否" 27 | // @Success 200 {object} updateUsedResponse 28 | // @Failure 400 {object} code.Failure 29 | // @Router /api/menu/used [patch] 30 | // @Security LoginToken 31 | func (h *handler) UpdateUsed() core.HandlerFunc { 32 | return func(c core.Context) { 33 | req := new(updateUsedRequest) 34 | res := new(updateUsedResponse) 35 | if err := c.ShouldBindForm(req); err != nil { 36 | c.AbortWithError(core.Error( 37 | http.StatusBadRequest, 38 | code.ParamBindError, 39 | code.Text(code.ParamBindError)).WithError(err), 40 | ) 41 | return 42 | } 43 | 44 | ids, err := h.hashids.HashidsDecode(req.Id) 45 | if err != nil { 46 | c.AbortWithError(core.Error( 47 | http.StatusBadRequest, 48 | code.HashIdsDecodeError, 49 | code.Text(code.HashIdsDecodeError)).WithError(err), 50 | ) 51 | return 52 | } 53 | 54 | id := int32(ids[0]) 55 | 56 | err = h.menuService.UpdateUsed(c, id, req.Used) 57 | if err != nil { 58 | c.AbortWithError(core.Error( 59 | http.StatusBadRequest, 60 | code.MenuUpdateError, 61 | code.Text(code.MenuUpdateError)).WithError(err), 62 | ) 63 | return 64 | } 65 | 66 | res.Id = id 67 | c.Payload(res) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/api/tool/func_clearcache.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | ) 10 | 11 | type clearCacheRequest struct { 12 | RedisKey string `form:"redis_key"` // Redis Key 13 | } 14 | 15 | type clearCacheResponse struct { 16 | Bool bool `json:"bool"` // 删除结果 17 | } 18 | 19 | // ClearCache 清空缓存 20 | // @Summary 清空缓存 21 | // @Description 清空缓存 22 | // @Tags API.tool 23 | // @Accept application/x-www-form-urlencoded 24 | // @Produce json 25 | // @Param redis_key formData string true "Redis Key" 26 | // @Success 200 {object} searchCacheResponse 27 | // @Failure 400 {object} code.Failure 28 | // @Router /api/tool/cache/clear [patch] 29 | // @Security LoginToken 30 | func (h *handler) ClearCache() core.HandlerFunc { 31 | return func(c core.Context) { 32 | req := new(clearCacheRequest) 33 | res := new(clearCacheResponse) 34 | if err := c.ShouldBindForm(req); err != nil { 35 | c.AbortWithError(core.Error( 36 | http.StatusBadRequest, 37 | code.ParamBindError, 38 | code.Text(code.ParamBindError)).WithError(err), 39 | ) 40 | return 41 | } 42 | 43 | if b := h.cache.Exists(req.RedisKey); b != true { 44 | c.AbortWithError(core.Error( 45 | http.StatusBadRequest, 46 | code.CacheNotExist, 47 | code.Text(code.CacheNotExist)), 48 | ) 49 | return 50 | } 51 | 52 | b := h.cache.Del(req.RedisKey, redis.WithTrace(c.Trace())) 53 | if b != true { 54 | c.AbortWithError(core.Error( 55 | http.StatusBadRequest, 56 | code.CacheDelError, 57 | code.Text(code.CacheDelError)), 58 | ) 59 | return 60 | } 61 | 62 | res.Bool = b 63 | c.Payload(res) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/api/tool/func_dbs.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | ) 7 | 8 | type dbsResponse struct { 9 | List []dbData `json:"list"` // 数据库列表 10 | } 11 | 12 | type dbData struct { 13 | DbName string `json:"db_name"` // 数据库名称 14 | } 15 | 16 | // Dbs 查询 DB 17 | // @Summary 查询 DB 18 | // @Description 查询 DB 19 | // @Tags API.tool 20 | // @Accept application/x-www-form-urlencoded 21 | // @Produce json 22 | // @Success 200 {object} dbsResponse 23 | // @Failure 400 {object} code.Failure 24 | // @Router /api/tool/data/dbs [get] 25 | // @Security LoginToken 26 | func (h *handler) Dbs() core.HandlerFunc { 27 | return func(c core.Context) { 28 | res := new(dbsResponse) 29 | 30 | // TODO 后期支持查询多个数据库 31 | data := dbData{ 32 | DbName: configs.Get().MySQL.Read.Name, 33 | } 34 | 35 | res.List = append(res.List, data) 36 | c.Payload(res) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/api/tool/func_hashidsdecode.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | ) 9 | 10 | type hashIdsDecodeRequest struct { 11 | Id string `uri:"id"` // 需解密的密文 12 | } 13 | 14 | type hashIdsDecodeResponse struct { 15 | Val int `json:"val"` // 解密后的值 16 | } 17 | 18 | // HashIdsDecode HashIds 解密 19 | // @Summary HashIds 解密 20 | // @Description HashIds 解密 21 | // @Tags API.tool 22 | // @Accept application/x-www-form-urlencoded 23 | // @Produce json 24 | // @Param id path string true "需解密的密文" 25 | // @Success 200 {object} hashIdsDecodeResponse 26 | // @Failure 400 {object} code.Failure 27 | // @Router /api/tool/hashids/decode/{id} [get] 28 | // @Security LoginToken 29 | func (h *handler) HashIdsDecode() core.HandlerFunc { 30 | return func(c core.Context) { 31 | req := new(hashIdsDecodeRequest) 32 | res := new(hashIdsDecodeResponse) 33 | if err := c.ShouldBindURI(req); err != nil { 34 | c.AbortWithError(core.Error( 35 | http.StatusBadRequest, 36 | code.ParamBindError, 37 | code.Text(code.ParamBindError)).WithError(err), 38 | ) 39 | return 40 | } 41 | 42 | hashId, err := h.hashids.HashidsDecode(req.Id) 43 | if err != nil { 44 | c.AbortWithError(core.Error( 45 | http.StatusBadRequest, 46 | code.HashIdsDecodeError, 47 | code.Text(code.HashIdsDecodeError)).WithError(err), 48 | ) 49 | return 50 | } 51 | 52 | res.Val = hashId[0] 53 | 54 | c.Payload(res) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/api/tool/func_hashidsencode.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | 9 | "github.com/spf13/cast" 10 | ) 11 | 12 | type hashIdsEncodeRequest struct { 13 | Id int32 `uri:"id"` // 需加密的数字 14 | } 15 | 16 | type hashIdsEncodeResponse struct { 17 | Val string `json:"val"` // 加密后的值 18 | } 19 | 20 | // HashIdsEncode HashIds 加密 21 | // @Summary HashIds 加密 22 | // @Description HashIds 加密 23 | // @Tags API.tool 24 | // @Accept application/x-www-form-urlencoded 25 | // @Produce json 26 | // @Param id path string true "需加密的数字" 27 | // @Success 200 {object} hashIdsEncodeResponse 28 | // @Failure 400 {object} code.Failure 29 | // @Router /api/tool/hashids/encode/{id} [get] 30 | // @Security LoginToken 31 | func (h *handler) HashIdsEncode() core.HandlerFunc { 32 | return func(c core.Context) { 33 | req := new(hashIdsEncodeRequest) 34 | res := new(hashIdsEncodeResponse) 35 | if err := c.ShouldBindURI(req); err != nil { 36 | c.AbortWithError(core.Error( 37 | http.StatusBadRequest, 38 | code.ParamBindError, 39 | code.Text(code.ParamBindError)).WithError(err), 40 | ) 41 | return 42 | } 43 | 44 | hashId, err := h.hashids.HashidsEncode([]int{cast.ToInt(req.Id)}) 45 | if err != nil { 46 | c.AbortWithError(core.Error( 47 | http.StatusBadRequest, 48 | code.HashIdsEncodeError, 49 | code.Text(code.HashIdsEncodeError)).WithError(err), 50 | ) 51 | return 52 | } 53 | 54 | res.Val = hashId 55 | 56 | c.Payload(res) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/code/README.md: -------------------------------------------------------------------------------- 1 | ## 错误码规则 2 | 3 | - 错误码需在 `code` 包中进行定义。 4 | 5 | #### 错误码为 5 位数 6 | 7 | | 1 | 01 | 01 | 8 | | :------ | :------ | :------ | 9 | | 服务级错误码 | 模块级错误码 | 具体错误码 | 10 | 11 | - 服务级错误码:1 位数进行表示,比如 1 为系统级错误;2 为普通错误,通常是由用户非法操作引起。 12 | - 模块级错误码:2 位数进行表示,比如 01 为用户模块;02 为订单模块。 13 | - 具体的错误码:2 位数进行表示,比如 01 为手机号不合法;02 为验证码输入错误。 -------------------------------------------------------------------------------- /internal/graph/README.md: -------------------------------------------------------------------------------- 1 | ## example 2 | 3 | ```cassandraql 4 | 1. 5 | query { 6 | bySex(sex: "男") { 7 | id 8 | name 9 | sex 10 | mobile 11 | } 12 | } 13 | 14 | 2. 15 | mutation { 16 | updateUserMobile(data: {id: "1", mobile: "13299999999"}) { 17 | id 18 | name 19 | sex 20 | mobile 21 | } 22 | } 23 | 24 | ``` -------------------------------------------------------------------------------- /internal/graph/model/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package model 4 | 5 | type User struct { 6 | ID string `json:"id"` 7 | Name string `json:"name"` 8 | Sex string `json:"sex"` 9 | Mobile string `json:"mobile"` 10 | } 11 | 12 | type UpdateUserMobileInput struct { 13 | ID string `json:"id"` 14 | Mobile string `json:"mobile"` 15 | } 16 | -------------------------------------------------------------------------------- /internal/graph/resolvers/generated/generated.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES. 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/xinliangnote/go-gin-api/internal/graph/generated" 9 | "github.com/xinliangnote/go-gin-api/internal/graph/model" 10 | ) 11 | 12 | type Resolver struct{} 13 | 14 | func (r *mutationResolver) UpdateUserMobile(ctx context.Context, data model.UpdateUserMobileInput) (*model.User, error) { 15 | panic("not implemented") 16 | } 17 | 18 | func (r *queryResolver) BySex(ctx context.Context, sex string) ([]*model.User, error) { 19 | panic("not implemented") 20 | } 21 | 22 | // Mutation returns generated.MutationResolver implementation. 23 | func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } 24 | 25 | // Query returns generated.QueryResolver implementation. 26 | func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 27 | 28 | type mutationResolver struct{ *Resolver } 29 | type queryResolver struct{ *Resolver } 30 | -------------------------------------------------------------------------------- /internal/graph/resolvers/resolvers.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/graph/generated" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 10 | 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type coreCtxKeyType struct{ name string } 15 | 16 | var CoreContextKey = coreCtxKeyType{"_core_context"} 17 | 18 | type mutationResolver struct{ *Resolver } 19 | 20 | type queryResolver struct{ *Resolver } 21 | 22 | type Resolver struct { 23 | logger *zap.Logger 24 | cache redis.Repo 25 | //userService user_service.UserService 26 | } 27 | 28 | func NewRootResolvers(logger *zap.Logger, db mysql.Repo, cache redis.Repo) generated.Config { 29 | c := generated.Config{ 30 | Resolvers: &Resolver{ 31 | logger: logger, 32 | cache: cache, 33 | //userService: user_service.NewUserService(db, cache), 34 | }, 35 | } 36 | return c 37 | } 38 | 39 | func (r *Resolver) Mutation() generated.MutationResolver { 40 | return &mutationResolver{r} 41 | } 42 | 43 | func (r *Resolver) Query() generated.QueryResolver { 44 | return &queryResolver{r} 45 | } 46 | 47 | // getCoreContextByCtx 获取 core context 48 | func (r *Resolver) getCoreContextByCtx(ctx context.Context) core.Context { 49 | return ctx.Value(CoreContextKey).(core.Context) 50 | } 51 | -------------------------------------------------------------------------------- /internal/graph/resolvers/user.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/graph/model" 7 | "github.com/xinliangnote/go-gin-api/pkg/errors" 8 | ) 9 | 10 | func (r *queryResolver) BySex(ctx context.Context, sex string) ([]*model.User, error) { 11 | if sex == "" { 12 | return nil, errors.New("sex required") 13 | } 14 | 15 | //模拟数据 16 | var users []*model.User 17 | users = append(users, &model.User{ID: "1", Name: "Tom", Sex: sex, Mobile: "13266666666"}) 18 | users = append(users, &model.User{ID: "1", Name: "Jack", Sex: sex, Mobile: "13288888888"}) 19 | 20 | return users, nil 21 | } 22 | 23 | func (r *mutationResolver) UpdateUserMobile(ctx context.Context, data model.UpdateUserMobileInput) (*model.User, error) { 24 | if data.ID == "" { 25 | return nil, errors.New("id required") 26 | } 27 | 28 | if data.Mobile == "" { 29 | return nil, errors.New("mobile required") 30 | } 31 | 32 | //模拟数据 33 | user := new(model.User) 34 | user.ID = data.ID 35 | user.Mobile = data.Mobile 36 | user.Sex = "男" 37 | user.Name = "Jack" 38 | 39 | //操作数据库 40 | //userData, err := r.userService.GetUserByUserName(r.getCoreContextByCtx(ctx), "test_user") 41 | //if err != nil { 42 | // return nil, err 43 | //} 44 | 45 | return user, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/graph/schemas/user.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | id: String! 3 | name: String! 4 | sex: String! 5 | mobile: String! 6 | } 7 | 8 | # 查询 集合 9 | type Query { 10 | bySex(sex: String!): [User!] 11 | } 12 | 13 | # 输入类型: 一般用户更改资源中的输入是列表对象,完成复杂任务 14 | input updateUserMobileInput { 15 | id:ID! 16 | mobile: String! 17 | } 18 | 19 | # 更改或者创建 集合 20 | type Mutation { 21 | updateUserMobile(data: updateUserMobileInput!): User 22 | } -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/proposal" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | // RecordHandler 指标处理 10 | func RecordHandler(logger *zap.Logger) func(msg *proposal.MetricsMessage) { 11 | if logger == nil { 12 | panic("logger required") 13 | } 14 | 15 | return func(msg *proposal.MetricsMessage) { 16 | RecordMetrics( 17 | msg.Method, 18 | msg.Path, 19 | msg.IsSuccess, 20 | msg.HTTPCode, 21 | msg.BusinessCode, 22 | msg.CostSeconds, 23 | msg.TraceID, 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/metrics/prometheus.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/spf13/cast" 6 | ) 7 | 8 | const ( 9 | namespace = "xinliangnote" 10 | subsystem = "go_gin_api" 11 | ) 12 | 13 | // metricsRequestsTotal metrics for request total 计数器(Counter) 14 | var metricsRequestsTotal = prometheus.NewCounterVec( 15 | prometheus.CounterOpts{ 16 | Namespace: namespace, 17 | Subsystem: subsystem, 18 | Name: "requests_total", 19 | Help: "request(ms) total", 20 | }, 21 | []string{"method", "path"}, 22 | ) 23 | 24 | // metricsRequestsCost metrics for requests cost 累积直方图(Histogram) 25 | var metricsRequestsCost = prometheus.NewHistogramVec( 26 | prometheus.HistogramOpts{ 27 | Namespace: namespace, 28 | Subsystem: subsystem, 29 | Name: "requests_cost", 30 | Help: "request(ms) cost milliseconds", 31 | }, 32 | []string{"method", "path", "success", "http_code", "business_code", "cost_milliseconds", "trace_id"}, 33 | ) 34 | 35 | func init() { 36 | prometheus.MustRegister(metricsRequestsTotal, metricsRequestsCost) 37 | } 38 | 39 | // RecordMetrics 记录指标 40 | func RecordMetrics(method, path string, success bool, httpCode, businessCode int, costSeconds float64, traceId string) { 41 | metricsRequestsTotal.With(prometheus.Labels{ 42 | "method": method, 43 | "path": path, 44 | }).Inc() 45 | 46 | metricsRequestsCost.With(prometheus.Labels{ 47 | "method": method, 48 | "path": path, 49 | "success": cast.ToString(success), 50 | "http_code": cast.ToString(httpCode), 51 | "business_code": cast.ToString(businessCode), 52 | "cost_milliseconds": cast.ToString(costSeconds * 1000), 53 | "trace_id": traceId, 54 | }).Observe(costSeconds) 55 | } 56 | -------------------------------------------------------------------------------- /internal/pkg/core/error.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/pkg/errors" 5 | ) 6 | 7 | var _ BusinessError = (*businessError)(nil) 8 | 9 | type BusinessError interface { 10 | // i 为了避免被其他包实现 11 | i() 12 | 13 | // WithError 设置错误信息 14 | WithError(err error) BusinessError 15 | 16 | // WithAlert 设置告警通知 17 | WithAlert() BusinessError 18 | 19 | // BusinessCode 获取业务码 20 | BusinessCode() int 21 | 22 | // HTTPCode 获取 HTTP 状态码 23 | HTTPCode() int 24 | 25 | // Message 获取错误描述 26 | Message() string 27 | 28 | // StackError 获取带堆栈的错误信息 29 | StackError() error 30 | 31 | // IsAlert 是否开启告警通知 32 | IsAlert() bool 33 | } 34 | 35 | type businessError struct { 36 | httpCode int // HTTP 状态码 37 | businessCode int // 业务码 38 | message string // 错误描述 39 | stackError error // 含有堆栈信息的错误 40 | isAlert bool // 是否告警通知 41 | } 42 | 43 | func Error(httpCode, businessCode int, message string) BusinessError { 44 | return &businessError{ 45 | httpCode: httpCode, 46 | businessCode: businessCode, 47 | message: message, 48 | isAlert: false, 49 | } 50 | } 51 | 52 | func (e *businessError) i() {} 53 | 54 | func (e *businessError) WithError(err error) BusinessError { 55 | e.stackError = errors.WithStack(err) 56 | return e 57 | } 58 | 59 | func (e *businessError) WithAlert() BusinessError { 60 | e.isAlert = true 61 | return e 62 | } 63 | 64 | func (e *businessError) HTTPCode() int { 65 | return e.httpCode 66 | } 67 | 68 | func (e *businessError) BusinessCode() int { 69 | return e.businessCode 70 | } 71 | 72 | func (e *businessError) Message() string { 73 | return e.message 74 | } 75 | 76 | func (e *businessError) StackError() error { 77 | return e.stackError 78 | } 79 | 80 | func (e *businessError) IsAlert() bool { 81 | return e.isAlert 82 | } 83 | -------------------------------------------------------------------------------- /internal/pkg/password/password.go: -------------------------------------------------------------------------------- 1 | package password 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/md5" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "fmt" 9 | ) 10 | 11 | const ( 12 | saltPassword = "qkhPAGA13HocW3GAEWwb" 13 | defaultPassword = "123456" 14 | ) 15 | 16 | func GeneratePassword(str string) (password string) { 17 | // md5 18 | m := md5.New() 19 | m.Write([]byte(str)) 20 | mByte := m.Sum(nil) 21 | 22 | // hmac 23 | h := hmac.New(sha256.New, []byte(saltPassword)) 24 | h.Write(mByte) 25 | password = hex.EncodeToString(h.Sum(nil)) 26 | 27 | return 28 | } 29 | 30 | func ResetPassword() (password string) { 31 | m := md5.New() 32 | m.Write([]byte(defaultPassword)) 33 | mStr := hex.EncodeToString(m.Sum(nil)) 34 | 35 | password = GeneratePassword(mStr) 36 | 37 | return 38 | } 39 | 40 | func GenerateLoginToken(id int32) (token string) { 41 | m := md5.New() 42 | m.Write([]byte(fmt.Sprintf("%d%s", id, saltPassword))) 43 | token = hex.EncodeToString(m.Sum(nil)) 44 | 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /internal/pkg/validation/validation.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/xinliangnote/go-gin-api/configs" 7 | 8 | "github.com/gin-gonic/gin/binding" 9 | "github.com/go-playground/locales/en" 10 | "github.com/go-playground/locales/zh" 11 | ut "github.com/go-playground/universal-translator" 12 | "github.com/go-playground/validator/v10" 13 | enTranslation "github.com/go-playground/validator/v10/translations/en" 14 | zhTranslation "github.com/go-playground/validator/v10/translations/zh" 15 | ) 16 | 17 | var trans ut.Translator 18 | 19 | func init() { 20 | lang := configs.Get().Language.Local 21 | 22 | if lang == configs.ZhCN { 23 | trans, _ = ut.New(zh.New()).GetTranslator("zh") 24 | if err := zhTranslation.RegisterDefaultTranslations(binding.Validator.Engine().(*validator.Validate), trans); err != nil { 25 | fmt.Println("validator zh translation error", err) 26 | } 27 | } 28 | 29 | if lang == configs.EnUS { 30 | trans, _ = ut.New(en.New()).GetTranslator("en") 31 | if err := enTranslation.RegisterDefaultTranslations(binding.Validator.Engine().(*validator.Validate), trans); err != nil { 32 | fmt.Println("validator en translation error", err) 33 | } 34 | } 35 | } 36 | 37 | func Error(err error) (message string) { 38 | if validationErrors, ok := err.(validator.ValidationErrors); !ok { 39 | return err.Error() 40 | } else { 41 | for _, e := range validationErrors { 42 | message += e.Translate(trans) + ";" 43 | } 44 | } 45 | return message 46 | } 47 | -------------------------------------------------------------------------------- /internal/proposal/alert.go: -------------------------------------------------------------------------------- 1 | package proposal 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // AlertMessage 告警信息 9 | type AlertMessage struct { 10 | ProjectName string `json:"project_name"` // 项目名,用于区分不同项目告警信息 11 | Env string `json:"env"` // 运行环境 12 | TraceID string `json:"trace_id"` // 唯一ID,用于追踪关联 13 | HOST string `json:"host"` // 请求 HOST 14 | URI string `json:"uri"` // 请求 URI 15 | Method string `json:"method"` // 请求 Method 16 | ErrorMessage interface{} `json:"error_message"` // 错误信息 17 | ErrorStack string `json:"error_stack"` // 堆栈信息 18 | Timestamp time.Time `json:"timestamp"` // 时间戳 19 | } 20 | 21 | // Marshal 序列化到JSON 22 | func (a *AlertMessage) Marshal() (jsonRaw []byte) { 23 | jsonRaw, _ = json.Marshal(a) 24 | return 25 | } 26 | 27 | // NotifyHandler 告警的发送句柄 28 | type NotifyHandler func(msg *AlertMessage) 29 | -------------------------------------------------------------------------------- /internal/proposal/metrics.go: -------------------------------------------------------------------------------- 1 | package proposal 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // MetricsMessage 指标信息 8 | type MetricsMessage struct { 9 | ProjectName string `json:"project_name"` // 项目名,用于区分不同项目告警信息 10 | Env string `json:"env"` // 运行环境 11 | TraceID string `json:"trace_id"` // 唯一ID,用于追踪关联 12 | HOST string `json:"host"` // 请求 HOST 13 | Path string `json:"path"` // 请求 Path 14 | Method string `json:"method"` // 请求 Method 15 | HTTPCode int `json:"http_code"` // HTTP 状态码 16 | BusinessCode int `json:"business_code"` // 业务码 17 | CostSeconds float64 `json:"cost_seconds"` // 耗时,单位:秒 18 | IsSuccess bool `json:"is_success"` // 状态,是否成功 19 | } 20 | 21 | // Marshal 序列化到JSON 22 | func (m *MetricsMessage) Marshal() (jsonRaw []byte) { 23 | jsonRaw, _ = json.Marshal(m) 24 | return 25 | } 26 | 27 | // RecordHandler 指标的记录句柄 28 | type RecordHandler func(msg *MetricsMessage) 29 | -------------------------------------------------------------------------------- /internal/proposal/session.go: -------------------------------------------------------------------------------- 1 | package proposal 2 | 3 | import "encoding/json" 4 | 5 | // SessionUserInfo 当前用户会话信息 6 | type SessionUserInfo struct { 7 | UserID int32 `json:"user_id"` // 用户ID 8 | UserName string `json:"user_name"` // 用户名 9 | } 10 | 11 | // Marshal 序列化到JSON 12 | func (user *SessionUserInfo) Marshal() (jsonRaw []byte) { 13 | jsonRaw, _ = json.Marshal(user) 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /internal/render/authorized/authorized.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 10 | 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type handler struct { 15 | db mysql.Repo 16 | logger *zap.Logger 17 | cache redis.Repo 18 | } 19 | 20 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo) *handler { 21 | return &handler{ 22 | logger: logger, 23 | cache: cache, 24 | db: db, 25 | } 26 | } 27 | 28 | func (h *handler) Add() core.HandlerFunc { 29 | return func(ctx core.Context) { 30 | ctx.HTML("authorized_add", nil) 31 | } 32 | } 33 | 34 | func (h *handler) Demo() core.HandlerFunc { 35 | return func(ctx core.Context) { 36 | ctx.HTML("authorized_demo", nil) 37 | } 38 | } 39 | 40 | func (h *handler) List() core.HandlerFunc { 41 | return func(ctx core.Context) { 42 | ctx.HTML("authorized_list", nil) 43 | } 44 | } 45 | 46 | func (h *handler) Api() core.HandlerFunc { 47 | type apiRequest struct { 48 | Id string `uri:"id"` // 主键ID 49 | } 50 | 51 | type apiResponse struct { 52 | HashID string `json:"hash_id"` // hashID 53 | } 54 | 55 | return func(ctx core.Context) { 56 | req := new(apiRequest) 57 | if err := ctx.ShouldBindURI(req); err != nil { 58 | ctx.AbortWithError(core.Error( 59 | http.StatusBadRequest, 60 | code.ParamBindError, 61 | code.Text(code.ParamBindError)).WithError(err), 62 | ) 63 | return 64 | } 65 | 66 | obj := new(apiResponse) 67 | obj.HashID = req.Id 68 | 69 | ctx.HTML("authorized_api", obj) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/render/cron/cron.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/code" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 10 | 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type handler struct { 15 | logger *zap.Logger 16 | cache redis.Repo 17 | db mysql.Repo 18 | } 19 | 20 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo) *handler { 21 | return &handler{ 22 | logger: logger, 23 | cache: cache, 24 | db: db, 25 | } 26 | } 27 | 28 | func (h *handler) Add() core.HandlerFunc { 29 | return func(ctx core.Context) { 30 | ctx.HTML("cron_task_add", nil) 31 | } 32 | } 33 | 34 | func (h *handler) Edit() core.HandlerFunc { 35 | type editRequest struct { 36 | Id string `uri:"id"` // 主键ID 37 | } 38 | 39 | type editResponse struct { 40 | HashID string `json:"hash_id"` // hashID 41 | } 42 | 43 | return func(ctx core.Context) { 44 | req := new(editRequest) 45 | if err := ctx.ShouldBindURI(req); err != nil { 46 | ctx.AbortWithError(core.Error( 47 | http.StatusBadRequest, 48 | code.ParamBindError, 49 | code.Text(code.ParamBindError)).WithError(err), 50 | ) 51 | return 52 | } 53 | 54 | obj := new(editResponse) 55 | obj.HashID = req.Id 56 | ctx.HTML("cron_task_edit", obj) 57 | } 58 | } 59 | 60 | func (h *handler) List() core.HandlerFunc { 61 | return func(ctx core.Context) { 62 | ctx.HTML("cron_task_list", nil) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/render/generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator_handler 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type handler struct { 11 | db mysql.Repo 12 | logger *zap.Logger 13 | cache redis.Repo 14 | } 15 | 16 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo) *handler { 17 | return &handler{ 18 | logger: logger, 19 | cache: cache, 20 | db: db, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/render/generator/gorm_execute.go: -------------------------------------------------------------------------------- 1 | package generator_handler 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/xinliangnote/go-gin-api/configs" 12 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 13 | ) 14 | 15 | type gormExecuteRequest struct { 16 | Tables string `form:"tables"` 17 | } 18 | 19 | func (h *handler) GormExecute() core.HandlerFunc { 20 | dir, _ := os.Getwd() 21 | projectPath := strings.Replace(dir, "\\", "/", -1) 22 | gormgenSh := projectPath + "/scripts/gormgen.sh" 23 | gormgenBat := projectPath + "/scripts/gormgen.bat" 24 | 25 | return func(c core.Context) { 26 | req := new(gormExecuteRequest) 27 | if err := c.ShouldBindPostForm(req); err != nil { 28 | c.Payload("参数传递有误") 29 | return 30 | } 31 | 32 | mysqlConf := configs.Get().MySQL.Read 33 | shellPath := fmt.Sprintf("%s %s %s %s %s %s", gormgenSh, mysqlConf.Addr, mysqlConf.User, mysqlConf.Pass, mysqlConf.Name, req.Tables) 34 | batPath := fmt.Sprintf("%s %s %s %s %s %s", gormgenBat, mysqlConf.Addr, mysqlConf.User, mysqlConf.Pass, mysqlConf.Name, req.Tables) 35 | 36 | command := new(exec.Cmd) 37 | 38 | if runtime.GOOS == "windows" { 39 | command = exec.Command("cmd", "/C", batPath) 40 | } else { 41 | // runtime.GOOS = linux or darwin 42 | command = exec.Command("/bin/bash", "-c", shellPath) 43 | } 44 | 45 | var stderr bytes.Buffer 46 | command.Stderr = &stderr 47 | 48 | output, err := command.Output() 49 | if err != nil { 50 | c.Payload(stderr.String()) 51 | return 52 | } 53 | 54 | c.Payload(string(output)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/render/generator/gorm_view.go: -------------------------------------------------------------------------------- 1 | package generator_handler 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/xinliangnote/go-gin-api/configs" 7 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func (h *handler) GormView() core.HandlerFunc { 13 | return func(c core.Context) { 14 | 15 | type tableInfo struct { 16 | Name string `db:"table_name"` // name 17 | Comment string `db:"table_comment"` // comment 18 | } 19 | 20 | var tableCollect []tableInfo 21 | 22 | mysqlConf := configs.Get().MySQL.Read 23 | sqlTables := fmt.Sprintf("SELECT `table_name`,`table_comment` FROM `information_schema`.`tables` WHERE `table_schema`= '%s'", mysqlConf.Name) 24 | rows, err := h.db.GetDbR().Raw(sqlTables).Rows() 25 | if err != nil { 26 | h.logger.Error("rows err", zap.Error(err)) 27 | 28 | c.HTML("generator_gorm", tableCollect) 29 | return 30 | } 31 | 32 | err = rows.Err() 33 | if err != nil { 34 | h.logger.Error("rows err", zap.Error(err)) 35 | 36 | c.HTML("generator_gorm", tableCollect) 37 | return 38 | } 39 | 40 | defer rows.Close() 41 | 42 | for rows.Next() { 43 | var info tableInfo 44 | err = rows.Scan(&info.Name, &info.Comment) 45 | if err != nil { 46 | fmt.Printf("execute query tables action error,had ignored, detail is [%v]\n", err.Error()) 47 | continue 48 | } 49 | 50 | tableCollect = append(tableCollect, info) 51 | } 52 | 53 | c.HTML("generator_gorm", tableCollect) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/render/generator/handler_execute.go: -------------------------------------------------------------------------------- 1 | package generator_handler 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 12 | ) 13 | 14 | type handlerExecuteRequest struct { 15 | Name string `form:"name"` 16 | } 17 | 18 | func (h *handler) HandlerExecute() core.HandlerFunc { 19 | dir, _ := os.Getwd() 20 | projectPath := strings.Replace(dir, "\\", "/", -1) 21 | handlergenSh := projectPath + "/scripts/handlergen.sh" 22 | handlergenBat := projectPath + "/scripts/handlergen.bat" 23 | 24 | return func(c core.Context) { 25 | req := new(handlerExecuteRequest) 26 | if err := c.ShouldBindPostForm(req); err != nil { 27 | c.Payload("参数传递有误") 28 | return 29 | } 30 | shellPath := fmt.Sprintf("%s %s", handlergenSh, req.Name) 31 | batPath := fmt.Sprintf("%s %s", handlergenBat, req.Name) 32 | 33 | command := new(exec.Cmd) 34 | 35 | if runtime.GOOS == "windows" { 36 | command = exec.Command("cmd", "/C", batPath) 37 | } else { 38 | // runtime.GOOS = linux or darwin 39 | command = exec.Command("/bin/bash", "-c", shellPath) 40 | } 41 | 42 | var stderr bytes.Buffer 43 | command.Stderr = &stderr 44 | 45 | output, err := command.Output() 46 | if err != nil { 47 | c.Payload(stderr.String()) 48 | return 49 | } 50 | 51 | c.Payload(string(output)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/render/generator/handler_view.go: -------------------------------------------------------------------------------- 1 | package generator_handler 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | ) 6 | 7 | func (h *handler) HandlerView() core.HandlerFunc { 8 | return func(c core.Context) { 9 | c.HTML("generator_handler", nil) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /internal/render/index/index.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 7 | 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type handler struct { 12 | logger *zap.Logger 13 | cache redis.Repo 14 | db mysql.Repo 15 | } 16 | 17 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo) *handler { 18 | return &handler{ 19 | logger: logger, 20 | cache: cache, 21 | db: db, 22 | } 23 | } 24 | 25 | func (h *handler) Index() core.HandlerFunc { 26 | return func(ctx core.Context) { 27 | ctx.HTML("index", nil) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/render/install/install.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | "net/http" 5 | "runtime" 6 | 7 | "github.com/xinliangnote/go-gin-api/configs" 8 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 9 | "github.com/xinliangnote/go-gin-api/pkg/file" 10 | 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type handler struct { 15 | logger *zap.Logger 16 | } 17 | 18 | func New(logger *zap.Logger) *handler { 19 | return &handler{ 20 | logger: logger, 21 | } 22 | } 23 | 24 | func (h *handler) View() core.HandlerFunc { 25 | type viewResponse struct { 26 | Config configs.Config 27 | MinGoVersion float64 28 | GoVersion string 29 | } 30 | 31 | return func(ctx core.Context) { 32 | if _, ok := file.IsExists(configs.ProjectInstallMark); ok { 33 | ctx.Redirect(http.StatusTemporaryRedirect, "/") 34 | } 35 | 36 | obj := new(viewResponse) 37 | obj.Config = configs.Get() 38 | obj.MinGoVersion = configs.MinGoVersion 39 | obj.GoVersion = runtime.Version() 40 | ctx.HTML("install_view", obj) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/render/upgrade/upgrade.go: -------------------------------------------------------------------------------- 1 | package upgrade 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type handler struct { 11 | db mysql.Repo 12 | logger *zap.Logger 13 | cache redis.Repo 14 | } 15 | 16 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo) *handler { 17 | return &handler{ 18 | logger: logger, 19 | cache: cache, 20 | db: db, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/repository/cron/cron_add_job.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 7 | 8 | "github.com/jakecoffman/cron" 9 | ) 10 | 11 | func (s *server) AddJob(task *cron_task.CronTask) cron.FuncJob { 12 | return func() { 13 | s.taskCount.Add() 14 | defer s.taskCount.Done() 15 | 16 | // 将 task 信息写入到 Kafka Topic 中,任务执行器订阅 Topic 如果为符合条件的任务并进行执行,反之不执行 17 | // 为了便于演示,不写入到 Kafka 中,仅记录日志 18 | 19 | msg := fmt.Sprintf("执行任务:(%d)%s [%s]", task.Id, task.Name, task.Spec) 20 | s.logger.Info(msg) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/repository/cron/cron_add_task.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 7 | 8 | "github.com/spf13/cast" 9 | ) 10 | 11 | func (s *server) AddTask(task *cron_task.CronTask) { 12 | spec := "0 " + strings.TrimSpace(task.Spec) 13 | name := cast.ToString(task.Id) 14 | 15 | s.cron.AddFunc(spec, s.AddJob(task), name) 16 | } 17 | -------------------------------------------------------------------------------- /internal/repository/cron/cron_remove_task.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import "github.com/spf13/cast" 4 | 5 | func (s *server) RemoveTask(taskId int) { 6 | name := cast.ToString(taskId) 7 | s.cron.RemoveJob(name) 8 | } 9 | -------------------------------------------------------------------------------- /internal/repository/cron/cron_start.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | func (s *server) Start() { 14 | s.cron.Start() 15 | go s.taskCount.Wait() 16 | 17 | qb := cron_task.NewQueryBuilder() 18 | qb.WhereIsUsed(mysql.EqualPredicate, cron_task.IsUsedYES) 19 | totalNum, err := qb.Count(s.db.GetDbR()) 20 | if err != nil { 21 | s.logger.Fatal("cron initialize tasks count err", zap.Error(err)) 22 | } 23 | 24 | pageSize := 50 25 | maxPage := int(math.Ceil(float64(totalNum) / float64(pageSize))) 26 | 27 | taskNum := 0 28 | s.logger.Info("开始初始化后台任务") 29 | 30 | for page := 1; page <= maxPage; page++ { 31 | qb = cron_task.NewQueryBuilder() 32 | qb.WhereIsUsed(mysql.EqualPredicate, cron_task.IsUsedYES) 33 | listData, err := qb. 34 | Limit(pageSize). 35 | Offset((page - 1) * pageSize). 36 | OrderById(false). 37 | QueryAll(s.db.GetDbR()) 38 | if err != nil { 39 | s.logger.Fatal("cron initialize tasks list err", zap.Error(err)) 40 | } 41 | 42 | for _, item := range listData { 43 | s.AddTask(item) 44 | taskNum++ 45 | } 46 | } 47 | 48 | s.logger.Info(fmt.Sprintf("后台任务初始化完成,总数量:%d", taskNum)) 49 | } 50 | -------------------------------------------------------------------------------- /internal/repository/cron/cron_stop.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | func (s *server) Stop() { 4 | s.cron.Stop() 5 | s.taskCount.Exit() 6 | } 7 | -------------------------------------------------------------------------------- /internal/repository/mysql/admin/gen_model.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import "time" 4 | 5 | // Admin 管理员表 6 | //go:generate gormgen -structs Admin -input . 7 | type Admin struct { 8 | Id int32 // 主键 9 | Username string // 用户名 10 | Password string // 密码 11 | Nickname string // 昵称 12 | Mobile string // 手机号 13 | IsUsed int32 // 是否启用 1:是 -1:否 14 | IsDeleted int32 // 是否删除 1:是 -1:否 15 | CreatedAt time.Time `gorm:"time"` // 创建时间 16 | CreatedUser string // 创建人 17 | UpdatedAt time.Time `gorm:"time"` // 更新时间 18 | UpdatedUser string // 更新人 19 | } 20 | -------------------------------------------------------------------------------- /internal/repository/mysql/admin/gen_table.md: -------------------------------------------------------------------------------- 1 | #### go_gin_api.admin 2 | 管理员表 3 | 4 | | 序号 | 名称 | 描述 | 类型 | 键 | 为空 | 额外 | 默认值 | 5 | | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 6 | | 1 | id | 主键 | int unsigned | PRI | NO | auto_increment | | 7 | | 2 | username | 用户名 | varchar(32) | UNI | NO | | | 8 | | 3 | password | 密码 | varchar(100) | | NO | | | 9 | | 4 | nickname | 昵称 | varchar(60) | | NO | | | 10 | | 5 | mobile | 手机号 | varchar(20) | | NO | | | 11 | | 6 | is_used | 是否启用 1:是 -1:否 | tinyint(1) | | NO | | 1 | 12 | | 7 | is_deleted | 是否删除 1:是 -1:否 | tinyint(1) | | NO | | -1 | 13 | | 8 | created_at | 创建时间 | timestamp | | NO | DEFAULT_GENERATED | CURRENT_TIMESTAMP | 14 | | 9 | created_user | 创建人 | varchar(60) | | NO | | | 15 | | 10 | updated_at | 更新时间 | timestamp | | NO | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | CURRENT_TIMESTAMP | 16 | | 11 | updated_user | 更新人 | varchar(60) | | NO | | | 17 | -------------------------------------------------------------------------------- /internal/repository/mysql/admin_menu/gen_model.go: -------------------------------------------------------------------------------- 1 | package admin_menu 2 | 3 | import "time" 4 | 5 | // AdminMenu 管理员菜单栏表 6 | //go:generate gormgen -structs AdminMenu -input . 7 | type AdminMenu struct { 8 | Id int32 // 主键 9 | AdminId int32 // 管理员ID 10 | MenuId int32 // 菜单栏ID 11 | CreatedAt time.Time `gorm:"time"` // 创建时间 12 | CreatedUser string // 创建人 13 | } 14 | -------------------------------------------------------------------------------- /internal/repository/mysql/admin_menu/gen_table.md: -------------------------------------------------------------------------------- 1 | #### go_gin_api.admin_menu 2 | 管理员菜单栏表 3 | 4 | | 序号 | 名称 | 描述 | 类型 | 键 | 为空 | 额外 | 默认值 | 5 | | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 6 | | 1 | id | 主键 | int unsigned | PRI | NO | auto_increment | | 7 | | 2 | admin_id | 管理员ID | int unsigned | MUL | NO | | 0 | 8 | | 3 | menu_id | 菜单栏ID | int unsigned | | NO | | 0 | 9 | | 4 | created_at | 创建时间 | timestamp | | NO | DEFAULT_GENERATED | CURRENT_TIMESTAMP | 10 | | 5 | created_user | 创建人 | varchar(60) | | NO | | | 11 | -------------------------------------------------------------------------------- /internal/repository/mysql/authorized/gen_model.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import "time" 4 | 5 | // Authorized 已授权的调用方表 6 | //go:generate gormgen -structs Authorized -input . 7 | type Authorized struct { 8 | Id int32 // 主键 9 | BusinessKey string // 调用方key 10 | BusinessSecret string // 调用方secret 11 | BusinessDeveloper string // 调用方对接人 12 | Remark string // 备注 13 | IsUsed int32 // 是否启用 1:是 -1:否 14 | IsDeleted int32 // 是否删除 1:是 -1:否 15 | CreatedAt time.Time `gorm:"time"` // 创建时间 16 | CreatedUser string // 创建人 17 | UpdatedAt time.Time `gorm:"time"` // 更新时间 18 | UpdatedUser string // 更新人 19 | } 20 | -------------------------------------------------------------------------------- /internal/repository/mysql/authorized/gen_table.md: -------------------------------------------------------------------------------- 1 | #### go_gin_api.authorized 2 | 已授权的调用方表 3 | 4 | | 序号 | 名称 | 描述 | 类型 | 键 | 为空 | 额外 | 默认值 | 5 | | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 6 | | 1 | id | 主键 | int unsigned | PRI | NO | auto_increment | | 7 | | 2 | business_key | 调用方key | varchar(32) | UNI | NO | | | 8 | | 3 | business_secret | 调用方secret | varchar(60) | | NO | | | 9 | | 4 | business_developer | 调用方对接人 | varchar(60) | | NO | | | 10 | | 5 | remark | 备注 | varchar(255) | | NO | | | 11 | | 6 | is_used | 是否启用 1:是 -1:否 | tinyint(1) | | NO | | 1 | 12 | | 7 | is_deleted | 是否删除 1:是 -1:否 | tinyint(1) | | NO | | -1 | 13 | | 8 | created_at | 创建时间 | timestamp | | NO | DEFAULT_GENERATED | CURRENT_TIMESTAMP | 14 | | 9 | created_user | 创建人 | varchar(60) | | NO | | | 15 | | 10 | updated_at | 更新时间 | timestamp | | NO | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | CURRENT_TIMESTAMP | 16 | | 11 | updated_user | 更新人 | varchar(60) | | NO | | | 17 | -------------------------------------------------------------------------------- /internal/repository/mysql/authorized/model.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | const ( 4 | IsUsedYES = 1 // 启用 5 | IsUsedNo = -1 // 禁用 6 | ) 7 | -------------------------------------------------------------------------------- /internal/repository/mysql/authorized_api/gen_model.go: -------------------------------------------------------------------------------- 1 | package authorized_api 2 | 3 | import "time" 4 | 5 | // AuthorizedApi 已授权接口地址表 6 | //go:generate gormgen -structs AuthorizedApi -input . 7 | type AuthorizedApi struct { 8 | Id int32 // 主键 9 | BusinessKey string // 调用方key 10 | Method string // 请求方式 11 | Api string // 请求地址 12 | IsDeleted int32 // 是否删除 1:是 -1:否 13 | CreatedAt time.Time `gorm:"time"` // 创建时间 14 | CreatedUser string // 创建人 15 | UpdatedAt time.Time `gorm:"time"` // 更新时间 16 | UpdatedUser string // 更新人 17 | } 18 | -------------------------------------------------------------------------------- /internal/repository/mysql/authorized_api/gen_table.md: -------------------------------------------------------------------------------- 1 | #### go_gin_api.authorized_api 2 | 已授权接口地址表 3 | 4 | | 序号 | 名称 | 描述 | 类型 | 键 | 为空 | 额外 | 默认值 | 5 | | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 6 | | 1 | id | 主键 | int unsigned | PRI | NO | auto_increment | | 7 | | 2 | business_key | 调用方key | varchar(32) | | NO | | | 8 | | 3 | method | 请求方式 | varchar(30) | | NO | | | 9 | | 4 | api | 请求地址 | varchar(100) | | NO | | | 10 | | 5 | is_deleted | 是否删除 1:是 -1:否 | tinyint(1) | | NO | | -1 | 11 | | 6 | created_at | 创建时间 | timestamp | | NO | DEFAULT_GENERATED | CURRENT_TIMESTAMP | 12 | | 7 | created_user | 创建人 | varchar(60) | | NO | | | 13 | | 8 | updated_at | 更新时间 | timestamp | | NO | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | CURRENT_TIMESTAMP | 14 | | 9 | updated_user | 更新人 | varchar(60) | | NO | | | 15 | -------------------------------------------------------------------------------- /internal/repository/mysql/cron_task/gen_model.go: -------------------------------------------------------------------------------- 1 | package cron_task 2 | 3 | import "time" 4 | 5 | // CronTask 后台任务表 6 | //go:generate gormgen -structs CronTask -input . 7 | type CronTask struct { 8 | Id int32 // 主键 9 | Name string // 任务名称 10 | Spec string // crontab 表达式 11 | Command string // 执行命令 12 | Protocol int32 // 执行方式 1:shell 2:http 13 | HttpMethod int32 // http 请求方式 1:get 2:post 14 | Timeout int32 // 超时时间(单位:秒) 15 | RetryTimes int32 // 重试次数 16 | RetryInterval int32 // 重试间隔(单位:秒) 17 | NotifyStatus int32 // 执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知 18 | NotifyType int32 // 通知类型 1:邮件 2:webhook 19 | NotifyReceiverEmail string // 通知者邮箱地址(多个用,分割) 20 | NotifyKeyword string // 通知匹配关键字(多个用,分割) 21 | Remark string // 备注 22 | IsUsed int32 // 是否启用 1:是 -1:否 23 | CreatedAt time.Time `gorm:"time"` // 创建时间 24 | CreatedUser string // 创建人 25 | UpdatedAt time.Time `gorm:"time"` // 更新时间 26 | UpdatedUser string // 更新人 27 | } 28 | -------------------------------------------------------------------------------- /internal/repository/mysql/cron_task/gen_table.md: -------------------------------------------------------------------------------- 1 | #### go_gin_api.cron_task 2 | 后台任务表 3 | 4 | | 序号 | 名称 | 描述 | 类型 | 键 | 为空 | 额外 | 默认值 | 5 | | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 6 | | 1 | id | 主键 | int unsigned | PRI | NO | auto_increment | | 7 | | 2 | name | 任务名称 | varchar(64) | MUL | NO | | | 8 | | 3 | spec | crontab 表达式 | varchar(64) | | NO | | | 9 | | 4 | command | 执行命令 | varchar(255) | | NO | | | 10 | | 5 | protocol | 执行方式 1:shell 2:http | tinyint unsigned | | NO | | 1 | 11 | | 6 | http_method | http 请求方式 1:get 2:post | tinyint unsigned | | NO | | 1 | 12 | | 7 | timeout | 超时时间(单位:秒) | int unsigned | | NO | | 60 | 13 | | 8 | retry_times | 重试次数 | tinyint(1) | | NO | | 3 | 14 | | 9 | retry_interval | 重试间隔(单位:秒) | int | | NO | | 60 | 15 | | 10 | notify_status | 执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知 | tinyint unsigned | | NO | | 0 | 16 | | 11 | notify_type | 通知类型 1:邮件 2:webhook | tinyint unsigned | | NO | | 1 | 17 | | 12 | notify_receiver_email | 通知者邮箱地址(多个用,分割) | varchar(255) | | NO | | | 18 | | 13 | notify_keyword | 通知匹配关键字(多个用,分割) | varchar(255) | | NO | | | 19 | | 14 | remark | 备注 | varchar(100) | | NO | | | 20 | | 15 | is_used | 是否启用 1:是 -1:否 | tinyint(1) | | NO | | 1 | 21 | | 16 | created_at | 创建时间 | timestamp | | NO | DEFAULT_GENERATED | CURRENT_TIMESTAMP | 22 | | 17 | created_user | 创建人 | varchar(60) | | NO | | | 23 | | 18 | updated_at | 更新时间 | timestamp | | NO | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | CURRENT_TIMESTAMP | 24 | | 19 | updated_user | 更新人 | varchar(60) | | NO | | | 25 | -------------------------------------------------------------------------------- /internal/repository/mysql/cron_task/model.go: -------------------------------------------------------------------------------- 1 | package cron_task 2 | 3 | const ( 4 | ProtocolShell = 1 5 | ProtocolHTTP = 2 6 | 7 | HttpMethodGet = 1 8 | HttpMethodPost = 2 9 | 10 | NotifyStatusNo = 1 11 | NotifyStatusFailed = 2 12 | NotifyStatusStopped = 3 13 | NotifyStatusKeyword = 4 14 | 15 | NotifyTypeEmail = 1 16 | NotifyTypeWebhook = 2 17 | 18 | IsUsedYES = 1 19 | IsUsedNo = -1 20 | ) 21 | 22 | var ProtocolText = map[int32]string{ 23 | ProtocolShell: "SHELL", 24 | ProtocolHTTP: "HTTP", 25 | } 26 | 27 | var HttpMethodText = map[int32]string{ 28 | HttpMethodGet: "GET", 29 | HttpMethodPost: "POST", 30 | } 31 | 32 | var NotifyStatusText = map[int32]string{ 33 | NotifyStatusNo: "不通知", 34 | NotifyStatusFailed: "失败通知", 35 | NotifyStatusStopped: "结束通知", 36 | NotifyStatusKeyword: "结果关键字匹配通知", 37 | } 38 | 39 | var NotifyTypeText = map[int32]string{ 40 | NotifyTypeEmail: "邮件", 41 | NotifyTypeWebhook: "Webhook", 42 | } 43 | 44 | var IsUsedText = map[int32]string{ 45 | IsUsedYES: "启用", 46 | IsUsedNo: "禁用", 47 | } 48 | -------------------------------------------------------------------------------- /internal/repository/mysql/menu/gen_model.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import "time" 4 | 5 | // Menu 左侧菜单栏表 6 | //go:generate gormgen -structs Menu -input . 7 | type Menu struct { 8 | Id int32 // 主键 9 | Pid int32 // 父类ID 10 | Name string // 菜单名称 11 | Link string // 链接地址 12 | Icon string // 图标 13 | Level int32 // 菜单类型 1:一级菜单 2:二级菜单 14 | Sort int32 // 排序 15 | IsUsed int32 // 是否启用 1:是 -1:否 16 | IsDeleted int32 // 是否删除 1:是 -1:否 17 | CreatedAt time.Time `gorm:"time"` // 创建时间 18 | CreatedUser string // 创建人 19 | UpdatedAt time.Time `gorm:"time"` // 更新时间 20 | UpdatedUser string // 更新人 21 | } 22 | -------------------------------------------------------------------------------- /internal/repository/mysql/menu/gen_table.md: -------------------------------------------------------------------------------- 1 | #### go_gin_api.menu 2 | 左侧菜单栏表 3 | 4 | | 序号 | 名称 | 描述 | 类型 | 键 | 为空 | 额外 | 默认值 | 5 | | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 6 | | 1 | id | 主键 | int unsigned | PRI | NO | auto_increment | | 7 | | 2 | pid | 父类ID | int unsigned | | NO | | 0 | 8 | | 3 | name | 菜单名称 | varchar(32) | | NO | | | 9 | | 4 | link | 链接地址 | varchar(100) | | NO | | | 10 | | 5 | icon | 图标 | varchar(60) | | NO | | | 11 | | 6 | level | 菜单类型 1:一级菜单 2:二级菜单 | tinyint unsigned | | NO | | 1 | 12 | | 7 | sort | 排序 | int unsigned | | NO | | 0 | 13 | | 8 | is_used | 是否启用 1:是 -1:否 | tinyint(1) | | NO | | 1 | 14 | | 9 | is_deleted | 是否删除 1:是 -1:否 | tinyint(1) | | NO | | -1 | 15 | | 10 | created_at | 创建时间 | timestamp | | NO | DEFAULT_GENERATED | CURRENT_TIMESTAMP | 16 | | 11 | created_user | 创建人 | varchar(60) | | NO | | | 17 | | 12 | updated_at | 更新时间 | timestamp | | NO | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | CURRENT_TIMESTAMP | 18 | | 13 | updated_user | 更新人 | varchar(60) | | NO | | | 19 | -------------------------------------------------------------------------------- /internal/repository/mysql/menu_action/gen_model.go: -------------------------------------------------------------------------------- 1 | package menu_action 2 | 3 | import "time" 4 | 5 | // MenuAction 功能权限表 6 | //go:generate gormgen -structs MenuAction -input . 7 | type MenuAction struct { 8 | Id int32 // 主键 9 | MenuId int32 // 菜单栏ID 10 | Method string // 请求方式 11 | Api string // 请求地址 12 | IsDeleted int32 // 是否删除 1:是 -1:否 13 | CreatedAt time.Time `gorm:"time"` // 创建时间 14 | CreatedUser string // 创建人 15 | UpdatedAt time.Time `gorm:"time"` // 更新时间 16 | UpdatedUser string // 更新人 17 | } 18 | -------------------------------------------------------------------------------- /internal/repository/mysql/menu_action/gen_table.md: -------------------------------------------------------------------------------- 1 | #### go_gin_api.menu_action 2 | 功能权限表 3 | 4 | | 序号 | 名称 | 描述 | 类型 | 键 | 为空 | 额外 | 默认值 | 5 | | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | 6 | | 1 | id | 主键 | int unsigned | PRI | NO | auto_increment | | 7 | | 2 | menu_id | 菜单栏ID | int unsigned | MUL | NO | | 0 | 8 | | 3 | method | 请求方式 | varchar(30) | | NO | | | 9 | | 4 | api | 请求地址 | varchar(100) | | NO | | | 10 | | 5 | is_deleted | 是否删除 1:是 -1:否 | tinyint(1) | | NO | | -1 | 11 | | 6 | created_at | 创建时间 | timestamp | | NO | DEFAULT_GENERATED | CURRENT_TIMESTAMP | 12 | | 7 | created_user | 创建人 | varchar(60) | | NO | | | 13 | | 8 | updated_at | 更新时间 | timestamp | | NO | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | CURRENT_TIMESTAMP | 14 | | 9 | updated_user | 更新人 | varchar(60) | | NO | | | 15 | -------------------------------------------------------------------------------- /internal/repository/socket/socket.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | "github.com/xinliangnote/go-gin-api/pkg/errors" 10 | 11 | "github.com/gorilla/websocket" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | var _ Server = (*server)(nil) 16 | 17 | type server struct { 18 | logger *zap.Logger 19 | db mysql.Repo 20 | cache redis.Repo 21 | socket *websocket.Conn 22 | } 23 | 24 | type Server interface { 25 | i() 26 | 27 | // OnMessage 接收消息 28 | OnMessage() 29 | 30 | // OnSend 发送消息 31 | OnSend(message []byte) error 32 | 33 | // OnClose 关闭 34 | OnClose() 35 | } 36 | 37 | var upGrader = websocket.Upgrader{ 38 | HandshakeTimeout: 5 * time.Second, 39 | CheckOrigin: func(r *http.Request) bool { 40 | return true 41 | }, 42 | } 43 | 44 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo, w http.ResponseWriter, r *http.Request, responseHeader http.Header) (Server, error) { 45 | if logger == nil { 46 | return nil, errors.New("logger required") 47 | } 48 | 49 | if db == nil { 50 | return nil, errors.New("db required") 51 | } 52 | 53 | if cache == nil { 54 | return nil, errors.New("cache required") 55 | } 56 | 57 | ws, err := upGrader.Upgrade(w, r, responseHeader) 58 | if err != nil { 59 | return nil, errors.Wrap(err, "ws error") 60 | } 61 | 62 | return &server{ 63 | logger: logger, 64 | db: db, 65 | cache: cache, 66 | socket: ws, 67 | }, nil 68 | } 69 | 70 | func (s *server) i() {} 71 | -------------------------------------------------------------------------------- /internal/repository/socket/socket_on_close.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import "go.uber.org/zap" 4 | 5 | func (s *server) OnClose() { 6 | err := s.socket.Close() 7 | if err != nil { 8 | s.logger.Error("socket on closed error", zap.Error(err)) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/repository/socket/socket_on_message.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import "go.uber.org/zap" 4 | 5 | func (s *server) OnMessage() { 6 | defer func() { 7 | s.OnClose() 8 | }() 9 | 10 | for { 11 | //接收消息 12 | _, message, err := s.socket.ReadMessage() 13 | if err != nil { 14 | s.logger.Error("socket on message error", zap.Error(err)) 15 | break 16 | } 17 | 18 | // 为了便于演示,仅输出到日志文件 19 | s.logger.Info("receive message: " + string(message)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/repository/socket/socket_on_send.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | func (s *server) OnSend(message []byte) error { 9 | err := s.socket.WriteMessage(websocket.TextMessage, message) 10 | if err != nil { 11 | s.OnClose() 12 | s.logger.Error("socket on send error", zap.Error(err)) 13 | } 14 | return err 15 | } 16 | -------------------------------------------------------------------------------- /internal/router/interceptor/check_login.go: -------------------------------------------------------------------------------- 1 | package interceptor 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/xinliangnote/go-gin-api/configs" 8 | "github.com/xinliangnote/go-gin-api/internal/code" 9 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 10 | "github.com/xinliangnote/go-gin-api/internal/proposal" 11 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 12 | "github.com/xinliangnote/go-gin-api/pkg/errors" 13 | ) 14 | 15 | func (i *interceptor) CheckLogin(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError) { 16 | token := ctx.GetHeader(configs.HeaderLoginToken) 17 | if token == "" { 18 | err = core.Error( 19 | http.StatusUnauthorized, 20 | code.AuthorizationError, 21 | code.Text(code.AuthorizationError)).WithError(errors.New("Header 中缺少 Token 参数")) 22 | 23 | return 24 | } 25 | 26 | if !i.cache.Exists(configs.RedisKeyPrefixLoginUser + token) { 27 | err = core.Error( 28 | http.StatusUnauthorized, 29 | code.AuthorizationError, 30 | code.Text(code.AuthorizationError)).WithError(errors.New("请先登录")) 31 | 32 | return 33 | } 34 | 35 | cacheData, cacheErr := i.cache.Get(configs.RedisKeyPrefixLoginUser+token, redis.WithTrace(ctx.Trace())) 36 | if cacheErr != nil { 37 | err = core.Error( 38 | http.StatusUnauthorized, 39 | code.AuthorizationError, 40 | code.Text(code.AuthorizationError)).WithError(cacheErr) 41 | 42 | return 43 | } 44 | 45 | jsonErr := json.Unmarshal([]byte(cacheData), &sessionUserInfo) 46 | if jsonErr != nil { 47 | core.Error( 48 | http.StatusUnauthorized, 49 | code.AuthorizationError, 50 | code.Text(code.AuthorizationError)).WithError(jsonErr) 51 | 52 | return 53 | } 54 | 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /internal/router/interceptor/interceptor.go: -------------------------------------------------------------------------------- 1 | package interceptor 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/proposal" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 8 | "github.com/xinliangnote/go-gin-api/internal/services/admin" 9 | "github.com/xinliangnote/go-gin-api/internal/services/authorized" 10 | 11 | "go.uber.org/zap" 12 | ) 13 | 14 | var _ Interceptor = (*interceptor)(nil) 15 | 16 | type Interceptor interface { 17 | // CheckLogin 验证是否登录 18 | CheckLogin(ctx core.Context) (info proposal.SessionUserInfo, err core.BusinessError) 19 | 20 | // CheckRBAC 验证 RBAC 权限是否合法 21 | CheckRBAC() core.HandlerFunc 22 | 23 | // CheckSignature 验证签名是否合法,对用签名算法 pkg/signature 24 | CheckSignature() core.HandlerFunc 25 | 26 | // i 为了避免被其他包实现 27 | i() 28 | } 29 | 30 | type interceptor struct { 31 | logger *zap.Logger 32 | cache redis.Repo 33 | db mysql.Repo 34 | authorizedService authorized.Service 35 | adminService admin.Service 36 | } 37 | 38 | func New(logger *zap.Logger, cache redis.Repo, db mysql.Repo) Interceptor { 39 | return &interceptor{ 40 | logger: logger, 41 | cache: cache, 42 | db: db, 43 | authorizedService: authorized.New(db, cache), 44 | adminService: admin.New(db, cache), 45 | } 46 | } 47 | 48 | func (i *interceptor) i() {} 49 | -------------------------------------------------------------------------------- /internal/router/router_graphql.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "github.com/xinliangnote/go-gin-api/internal/graph/handler" 4 | 5 | func setGraphQLRouter(r *resource) { 6 | // graphQL 控制器 7 | gqlHandler := handler.New(r.logger, r.db, r.cache) 8 | 9 | gql := r.mux.Group("/graphql") 10 | { 11 | gql.GET("", gqlHandler.Playground()) 12 | gql.POST("/query", gqlHandler.Query()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internal/router/router_socket.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/websocket/sysmessage" 6 | ) 7 | 8 | func setSocketRouter(r *resource) { 9 | systemMessage := sysmessage.New(r.logger, r.db, r.cache) 10 | 11 | // 无需记录日志 12 | socket := r.mux.Group("/socket", core.DisableTraceLog, core.DisableRecordMetrics) 13 | { 14 | // 系统消息 15 | socket.GET("/system/message", systemMessage.Connect()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/services/admin/service.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 8 | ) 9 | 10 | var _ Service = (*service)(nil) 11 | 12 | type Service interface { 13 | i() 14 | 15 | Create(ctx core.Context, adminData *CreateAdminData) (id int32, err error) 16 | PageList(ctx core.Context, searchData *SearchData) (listData []*admin.Admin, err error) 17 | PageListCount(ctx core.Context, searchData *SearchData) (total int64, err error) 18 | UpdateUsed(ctx core.Context, id int32, used int32) (err error) 19 | Delete(ctx core.Context, id int32) (err error) 20 | Detail(ctx core.Context, searchOneData *SearchOneData) (info *admin.Admin, err error) 21 | ResetPassword(ctx core.Context, id int32) (err error) 22 | ModifyPassword(ctx core.Context, id int32, newPassword string) (err error) 23 | ModifyPersonalInfo(ctx core.Context, id int32, modifyData *ModifyData) (err error) 24 | 25 | CreateMenu(ctx core.Context, menuData *CreateMenuData) (err error) 26 | ListMenu(ctx core.Context, searchData *SearchListMenuData) (menuData []ListMenuData, err error) 27 | MyMenu(ctx core.Context, searchData *SearchMyMenuData) (menuData []ListMyMenuData, err error) 28 | MyAction(ctx core.Context, searchData *SearchMyActionData) (actionData []MyActionData, err error) 29 | } 30 | 31 | type service struct { 32 | db mysql.Repo 33 | cache redis.Repo 34 | } 35 | 36 | func New(db mysql.Repo, cache redis.Repo) Service { 37 | return &service{ 38 | db: db, 39 | cache: cache, 40 | } 41 | } 42 | 43 | func (s *service) i() {} 44 | -------------------------------------------------------------------------------- /internal/services/admin/service_create.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/password" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 7 | ) 8 | 9 | type CreateAdminData struct { 10 | Username string // 用户名 11 | Nickname string // 昵称 12 | Mobile string // 手机号 13 | Password string // 密码 14 | } 15 | 16 | func (s *service) Create(ctx core.Context, adminData *CreateAdminData) (id int32, err error) { 17 | model := admin.NewModel() 18 | model.Username = adminData.Username 19 | model.Password = password.GeneratePassword(adminData.Password) 20 | model.Nickname = adminData.Nickname 21 | model.Mobile = adminData.Mobile 22 | model.CreatedUser = ctx.SessionUserInfo().UserName 23 | model.IsUsed = 1 24 | model.IsDeleted = -1 25 | 26 | id, err = model.Create(s.db.GetDbW().WithContext(ctx.RequestContext())) 27 | if err != nil { 28 | return 0, err 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /internal/services/admin/service_createmenu.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin_menu" 9 | 10 | "github.com/spf13/cast" 11 | ) 12 | 13 | type CreateMenuData struct { 14 | AdminId int32 `form:"admin_id"` // AdminID 15 | Actions string `form:"actions"` // 功能权限ID,多个用,分割 16 | } 17 | 18 | func (s *service) CreateMenu(ctx core.Context, menuData *CreateMenuData) (err error) { 19 | qb := admin_menu.NewQueryBuilder() 20 | qb.WhereAdminId(mysql.EqualPredicate, menuData.AdminId) 21 | if err = qb.Delete(s.db.GetDbW().WithContext(ctx.RequestContext())); err != nil { 22 | return 23 | } 24 | 25 | ActionArr := strings.Split(menuData.Actions, ",") 26 | for _, v := range ActionArr { 27 | createModel := admin_menu.NewModel() 28 | createModel.AdminId = menuData.AdminId 29 | createModel.MenuId = cast.ToInt32(v) 30 | createModel.CreatedUser = ctx.SessionUserInfo().UserName 31 | 32 | _, err = createModel.Create(s.db.GetDbW().WithContext(ctx.RequestContext())) 33 | if err != nil { 34 | return 35 | } 36 | } 37 | 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /internal/services/admin/service_delete.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | "github.com/xinliangnote/go-gin-api/internal/pkg/password" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 10 | ) 11 | 12 | func (s *service) Delete(ctx core.Context, id int32) (err error) { 13 | data := map[string]interface{}{ 14 | "is_deleted": 1, 15 | "updated_user": ctx.SessionUserInfo().UserName, 16 | } 17 | 18 | qb := admin.NewQueryBuilder() 19 | qb.WhereId(mysql.EqualPredicate, id) 20 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | s.cache.Del(configs.RedisKeyPrefixLoginUser+password.GenerateLoginToken(id), redis.WithTrace(ctx.Trace())) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /internal/services/admin/service_detail.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 7 | ) 8 | 9 | type SearchOneData struct { 10 | Id int32 // 用户ID 11 | Username string // 用户名 12 | Nickname string // 昵称 13 | Mobile string // 手机号 14 | Password string // 密码 15 | IsUsed int32 // 是否启用 1:是 -1:否 16 | } 17 | 18 | func (s *service) Detail(ctx core.Context, searchOneData *SearchOneData) (info *admin.Admin, err error) { 19 | 20 | qb := admin.NewQueryBuilder() 21 | qb.WhereIsDeleted(mysql.EqualPredicate, -1) 22 | 23 | if searchOneData.Id != 0 { 24 | qb.WhereId(mysql.EqualPredicate, searchOneData.Id) 25 | } 26 | 27 | if searchOneData.Username != "" { 28 | qb.WhereUsername(mysql.EqualPredicate, searchOneData.Username) 29 | } 30 | 31 | if searchOneData.Nickname != "" { 32 | qb.WhereNickname(mysql.EqualPredicate, searchOneData.Nickname) 33 | } 34 | 35 | if searchOneData.Mobile != "" { 36 | qb.WhereMobile(mysql.EqualPredicate, searchOneData.Mobile) 37 | } 38 | 39 | if searchOneData.Password != "" { 40 | qb.WherePassword(mysql.EqualPredicate, searchOneData.Password) 41 | } 42 | 43 | if searchOneData.IsUsed != 0 { 44 | qb.WhereIsUsed(mysql.EqualPredicate, searchOneData.IsUsed) 45 | } 46 | 47 | info, err = qb.QueryOne(s.db.GetDbR().WithContext(ctx.RequestContext())) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /internal/services/admin/service_modifypassword.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | "github.com/xinliangnote/go-gin-api/internal/pkg/password" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 10 | ) 11 | 12 | func (s *service) ModifyPassword(ctx core.Context, id int32, newPassword string) (err error) { 13 | data := map[string]interface{}{ 14 | "password": password.GeneratePassword(newPassword), 15 | "updated_user": ctx.SessionUserInfo().UserName, 16 | } 17 | 18 | qb := admin.NewQueryBuilder() 19 | qb.WhereId(mysql.EqualPredicate, id) 20 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | s.cache.Del(configs.RedisKeyPrefixLoginUser+password.GenerateLoginToken(id), redis.WithTrace(ctx.Trace())) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /internal/services/admin/service_modifypersonalinfo.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 7 | ) 8 | 9 | type ModifyData struct { 10 | Nickname string // 昵称 11 | Mobile string // 手机号 12 | } 13 | 14 | func (s *service) ModifyPersonalInfo(ctx core.Context, id int32, modifyData *ModifyData) (err error) { 15 | data := map[string]interface{}{ 16 | "nickname": modifyData.Nickname, 17 | "mobile": modifyData.Mobile, 18 | "updated_user": ctx.SessionUserInfo().UserName, 19 | } 20 | 21 | qb := admin.NewQueryBuilder() 22 | qb.WhereId(mysql.EqualPredicate, id) 23 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /internal/services/admin/service_myaction.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin_menu" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu_action" 8 | ) 9 | 10 | type SearchMyActionData struct { 11 | AdminId int32 `json:"admin_id"` // 管理员ID 12 | } 13 | 14 | type MyActionData struct { 15 | Id int32 // 主键 16 | MenuId int32 // 菜单栏ID 17 | Method string // 请求方式 18 | Api string // 请求地址 19 | } 20 | 21 | func (s *service) MyAction(ctx core.Context, searchData *SearchMyActionData) (actionData []MyActionData, err error) { 22 | adminMenuQb := admin_menu.NewQueryBuilder() 23 | if searchData.AdminId != 0 { 24 | adminMenuQb.WhereAdminId(mysql.EqualPredicate, searchData.AdminId) 25 | } 26 | 27 | adminMenuListData, err := adminMenuQb. 28 | OrderById(false). 29 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if len(adminMenuListData) <= 0 { 35 | return 36 | } 37 | 38 | var menuIds []int32 39 | for _, v := range adminMenuListData { 40 | menuIds = append(menuIds, v.MenuId) 41 | } 42 | 43 | actionQb := menu_action.NewQueryBuilder() 44 | actionQb.WhereIsDeleted(mysql.EqualPredicate, -1) 45 | actionQb.WhereMenuIdIn(menuIds) 46 | actionListData, err := actionQb.QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if len(actionListData) <= 0 { 52 | return 53 | } 54 | 55 | actionData = make([]MyActionData, len(actionListData)) 56 | 57 | for k, v := range actionListData { 58 | data := MyActionData{ 59 | Id: v.Id, 60 | MenuId: v.MenuId, 61 | Method: v.Method, 62 | Api: v.Api, 63 | } 64 | 65 | actionData[k] = data 66 | } 67 | 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /internal/services/admin/service_mymenu.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin_menu" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 8 | ) 9 | 10 | type SearchMyMenuData struct { 11 | AdminId int32 `json:"admin_id"` // 管理员ID 12 | } 13 | 14 | type ListMyMenuData struct { 15 | Id int32 `json:"id"` // ID 16 | Pid int32 `json:"pid"` // 父类ID 17 | Name string `json:"name"` // 菜单名称 18 | Link string `json:"link"` // 链接地址 19 | Icon string `json:"icon"` // 图标 20 | } 21 | 22 | func (s *service) MyMenu(ctx core.Context, searchData *SearchMyMenuData) (menuData []ListMyMenuData, err error) { 23 | adminMenuQb := admin_menu.NewQueryBuilder() 24 | if searchData.AdminId != 0 { 25 | adminMenuQb.WhereAdminId(mysql.EqualPredicate, searchData.AdminId) 26 | } 27 | 28 | adminMenuListData, err := adminMenuQb. 29 | OrderById(false). 30 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if len(adminMenuListData) <= 0 { 36 | return 37 | } 38 | 39 | menuQb := menu.NewQueryBuilder() 40 | menuQb.WhereIsDeleted(mysql.EqualPredicate, -1) 41 | menuListData, err := menuQb. 42 | OrderBySort(true). 43 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | if len(menuListData) <= 0 { 49 | return 50 | } 51 | 52 | for _, menuAllV := range menuListData { 53 | for _, v := range adminMenuListData { 54 | if menuAllV.Id == v.MenuId { 55 | data := ListMyMenuData{ 56 | Id: menuAllV.Id, 57 | Pid: menuAllV.Pid, 58 | Name: menuAllV.Name, 59 | Link: menuAllV.Link, 60 | Icon: menuAllV.Icon, 61 | } 62 | 63 | menuData = append(menuData, data) 64 | } 65 | } 66 | } 67 | 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /internal/services/admin/service_pagelist.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 7 | ) 8 | 9 | type SearchData struct { 10 | Page int // 第几页 11 | PageSize int // 每页显示条数 12 | Username string // 用户名 13 | Nickname string // 昵称 14 | Mobile string // 手机号 15 | } 16 | 17 | func (s *service) PageList(ctx core.Context, searchData *SearchData) (listData []*admin.Admin, err error) { 18 | 19 | page := searchData.Page 20 | if page == 0 { 21 | page = 1 22 | } 23 | 24 | pageSize := searchData.PageSize 25 | if pageSize == 0 { 26 | pageSize = 10 27 | } 28 | 29 | offset := (page - 1) * pageSize 30 | 31 | qb := admin.NewQueryBuilder() 32 | qb.WhereIsDeleted(mysql.EqualPredicate, -1) 33 | 34 | if searchData.Username != "" { 35 | qb.WhereUsername(mysql.EqualPredicate, searchData.Username) 36 | } 37 | 38 | if searchData.Nickname != "" { 39 | qb.WhereNickname(mysql.EqualPredicate, searchData.Nickname) 40 | } 41 | 42 | if searchData.Mobile != "" { 43 | qb.WhereMobile(mysql.EqualPredicate, searchData.Mobile) 44 | } 45 | 46 | listData, err = qb. 47 | Limit(pageSize). 48 | Offset(offset). 49 | OrderById(false). 50 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /internal/services/admin/service_pagelistcount.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 7 | ) 8 | 9 | func (s *service) PageListCount(ctx core.Context, searchData *SearchData) (total int64, err error) { 10 | qb := admin.NewQueryBuilder() 11 | qb = qb.WhereIsDeleted(mysql.EqualPredicate, -1) 12 | 13 | if searchData.Username != "" { 14 | qb.WhereUsername(mysql.EqualPredicate, searchData.Username) 15 | } 16 | 17 | if searchData.Nickname != "" { 18 | qb.WhereNickname(mysql.EqualPredicate, searchData.Nickname) 19 | } 20 | 21 | if searchData.Mobile != "" { 22 | qb.WhereMobile(mysql.EqualPredicate, searchData.Mobile) 23 | } 24 | 25 | total, err = qb.Count(s.db.GetDbR().WithContext(ctx.RequestContext())) 26 | if err != nil { 27 | return 0, err 28 | } 29 | 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /internal/services/admin/service_resetpassword.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | "github.com/xinliangnote/go-gin-api/internal/pkg/password" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 10 | ) 11 | 12 | func (s *service) ResetPassword(ctx core.Context, id int32) (err error) { 13 | data := map[string]interface{}{ 14 | "password": password.ResetPassword(), 15 | "updated_user": ctx.SessionUserInfo().UserName, 16 | } 17 | 18 | qb := admin.NewQueryBuilder() 19 | qb.WhereId(mysql.EqualPredicate, id) 20 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | s.cache.Del(configs.RedisKeyPrefixLoginUser+password.GenerateLoginToken(id), redis.WithTrace(ctx.Trace())) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /internal/services/admin/service_updateused.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | "github.com/xinliangnote/go-gin-api/internal/pkg/password" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/admin" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 10 | ) 11 | 12 | func (s *service) UpdateUsed(ctx core.Context, id int32, used int32) (err error) { 13 | data := map[string]interface{}{ 14 | "is_used": used, 15 | "updated_user": ctx.SessionUserInfo().UserName, 16 | } 17 | 18 | qb := admin.NewQueryBuilder() 19 | qb.WhereId(mysql.EqualPredicate, id) 20 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | s.cache.Del(configs.RedisKeyPrefixLoginUser+password.GenerateLoginToken(id), redis.WithTrace(ctx.Trace())) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /internal/services/authorized/service.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized_api" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | ) 10 | 11 | var _ Service = (*service)(nil) 12 | 13 | type Service interface { 14 | i() 15 | 16 | Create(ctx core.Context, authorizedData *CreateAuthorizedData) (id int32, err error) 17 | List(ctx core.Context, searchData *SearchData) (listData []*authorized.Authorized, err error) 18 | PageList(ctx core.Context, searchData *SearchData) (listData []*authorized.Authorized, err error) 19 | PageListCount(ctx core.Context, searchData *SearchData) (total int64, err error) 20 | UpdateUsed(ctx core.Context, id int32, used int32) (err error) 21 | Delete(ctx core.Context, id int32) (err error) 22 | Detail(ctx core.Context, id int32) (info *authorized.Authorized, err error) 23 | DetailByKey(ctx core.Context, key string) (data *CacheAuthorizedData, err error) 24 | 25 | CreateAPI(ctx core.Context, authorizedAPIData *CreateAuthorizedAPIData) (id int32, err error) 26 | ListAPI(ctx core.Context, searchAPIData *SearchAPIData) (listData []*authorized_api.AuthorizedApi, err error) 27 | DeleteAPI(ctx core.Context, id int32) (err error) 28 | } 29 | 30 | type service struct { 31 | db mysql.Repo 32 | cache redis.Repo 33 | } 34 | 35 | func New(db mysql.Repo, cache redis.Repo) Service { 36 | return &service{ 37 | db: db, 38 | cache: cache, 39 | } 40 | } 41 | 42 | func (s *service) i() {} 43 | -------------------------------------------------------------------------------- /internal/services/authorized/service_create.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "io" 7 | 8 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 9 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized" 10 | ) 11 | 12 | type CreateAuthorizedData struct { 13 | BusinessKey string `json:"business_key"` // 调用方key 14 | BusinessDeveloper string `json:"business_developer"` // 调用方对接人 15 | Remark string `json:"remark"` // 备注 16 | } 17 | 18 | func (s *service) Create(ctx core.Context, authorizedData *CreateAuthorizedData) (id int32, err error) { 19 | buf := make([]byte, 10) 20 | io.ReadFull(rand.Reader, buf) 21 | secret := hex.EncodeToString(buf) 22 | 23 | model := authorized.NewModel() 24 | model.BusinessKey = authorizedData.BusinessKey 25 | model.BusinessSecret = secret 26 | model.BusinessDeveloper = authorizedData.BusinessDeveloper 27 | model.Remark = authorizedData.Remark 28 | model.CreatedUser = ctx.SessionUserInfo().UserName 29 | model.IsUsed = 1 30 | model.IsDeleted = -1 31 | 32 | id, err = model.Create(s.db.GetDbW().WithContext(ctx.RequestContext())) 33 | if err != nil { 34 | return 0, err 35 | } 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /internal/services/authorized/service_createapi.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized_api" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 8 | ) 9 | 10 | type CreateAuthorizedAPIData struct { 11 | BusinessKey string `json:"business_key"` // 调用方key 12 | Method string `json:"method"` // 请求方法 13 | API string `json:"api"` // 请求地址 14 | } 15 | 16 | func (s *service) CreateAPI(ctx core.Context, authorizedAPIData *CreateAuthorizedAPIData) (id int32, err error) { 17 | model := authorized_api.NewModel() 18 | model.BusinessKey = authorizedAPIData.BusinessKey 19 | model.Method = authorizedAPIData.Method 20 | model.Api = authorizedAPIData.API 21 | model.CreatedUser = ctx.SessionUserInfo().UserName 22 | model.IsDeleted = -1 23 | 24 | id, err = model.Create(s.db.GetDbW().WithContext(ctx.RequestContext())) 25 | if err != nil { 26 | return 0, err 27 | } 28 | 29 | s.cache.Del(configs.RedisKeyPrefixSignature+authorizedAPIData.BusinessKey, redis.WithTrace(ctx.Trace())) 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /internal/services/authorized/service_delete.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | 10 | "gorm.io/gorm" 11 | ) 12 | 13 | func (s *service) Delete(ctx core.Context, id int32) (err error) { 14 | // 先查询 id 是否存在 15 | authorizedInfo, err := authorized.NewQueryBuilder(). 16 | WhereIsDeleted(mysql.EqualPredicate, -1). 17 | WhereId(mysql.EqualPredicate, id). 18 | First(s.db.GetDbR().WithContext(ctx.RequestContext())) 19 | 20 | if err == gorm.ErrRecordNotFound { 21 | return nil 22 | } 23 | 24 | data := map[string]interface{}{ 25 | "is_deleted": 1, 26 | "updated_user": ctx.SessionUserInfo().UserName, 27 | } 28 | 29 | qb := authorized.NewQueryBuilder() 30 | qb.WhereId(mysql.EqualPredicate, id) 31 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | s.cache.Del(configs.RedisKeyPrefixSignature+authorizedInfo.BusinessKey, redis.WithTrace(ctx.Trace())) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /internal/services/authorized/service_deleteapi.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized_api" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | 10 | "gorm.io/gorm" 11 | ) 12 | 13 | func (s *service) DeleteAPI(ctx core.Context, id int32) (err error) { 14 | // 先查询 id 是否存在 15 | authorizedApiInfo, err := authorized_api.NewQueryBuilder(). 16 | WhereIsDeleted(mysql.EqualPredicate, -1). 17 | WhereId(mysql.EqualPredicate, id). 18 | First(s.db.GetDbR().WithContext(ctx.RequestContext())) 19 | 20 | if err == gorm.ErrRecordNotFound { 21 | return nil 22 | } 23 | 24 | data := map[string]interface{}{ 25 | "is_deleted": 1, 26 | "updated_user": ctx.SessionUserInfo().UserName, 27 | } 28 | 29 | qb := authorized_api.NewQueryBuilder() 30 | qb.WhereId(mysql.EqualPredicate, id) 31 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | s.cache.Del(configs.RedisKeyPrefixSignature+authorizedApiInfo.BusinessKey, redis.WithTrace(ctx.Trace())) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /internal/services/authorized/service_detail.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized" 7 | ) 8 | 9 | func (s *service) Detail(ctx core.Context, id int32) (info *authorized.Authorized, err error) { 10 | qb := authorized.NewQueryBuilder() 11 | qb.WhereIsDeleted(mysql.EqualPredicate, -1) 12 | qb.WhereId(mysql.EqualPredicate, id) 13 | 14 | info, err = qb.First(s.db.GetDbR().WithContext(ctx.RequestContext())) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /internal/services/authorized/service_list.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized" 7 | ) 8 | 9 | func (s *service) List(ctx core.Context, searchData *SearchData) (listData []*authorized.Authorized, err error) { 10 | 11 | qb := authorized.NewQueryBuilder() 12 | qb = qb.WhereIsDeleted(mysql.EqualPredicate, -1) 13 | 14 | if searchData.BusinessKey != "" { 15 | qb.WhereBusinessKey(mysql.EqualPredicate, searchData.BusinessKey) 16 | } 17 | 18 | if searchData.BusinessSecret != "" { 19 | qb.WhereBusinessSecret(mysql.EqualPredicate, searchData.BusinessSecret) 20 | } 21 | 22 | if searchData.BusinessDeveloper != "" { 23 | qb.WhereBusinessDeveloper(mysql.EqualPredicate, searchData.BusinessDeveloper) 24 | } 25 | 26 | listData, err = qb. 27 | OrderById(false). 28 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /internal/services/authorized/service_listapi.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized_api" 7 | ) 8 | 9 | type SearchAPIData struct { 10 | BusinessKey string `json:"business_key"` // 调用方key 11 | } 12 | 13 | func (s *service) ListAPI(ctx core.Context, searchAPIData *SearchAPIData) (listData []*authorized_api.AuthorizedApi, err error) { 14 | 15 | qb := authorized_api.NewQueryBuilder() 16 | qb = qb.WhereIsDeleted(mysql.EqualPredicate, -1) 17 | 18 | if searchAPIData.BusinessKey != "" { 19 | qb.WhereBusinessKey(mysql.EqualPredicate, searchAPIData.BusinessKey) 20 | } 21 | 22 | listData, err = qb. 23 | OrderById(false). 24 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /internal/services/authorized/service_pagelist.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized" 7 | ) 8 | 9 | type SearchData struct { 10 | Page int `json:"page"` // 第几页 11 | PageSize int `json:"page_size"` // 每页显示条数 12 | BusinessKey string `json:"business_key"` // 调用方key 13 | BusinessSecret string `json:"business_secret"` // 调用方secret 14 | BusinessDeveloper string `json:"business_developer"` // 调用方对接人 15 | Remark string `json:"remark"` // 备注 16 | } 17 | 18 | func (s *service) PageList(ctx core.Context, searchData *SearchData) (listData []*authorized.Authorized, err error) { 19 | 20 | page := searchData.Page 21 | if page == 0 { 22 | page = 1 23 | } 24 | 25 | pageSize := searchData.PageSize 26 | if pageSize == 0 { 27 | pageSize = 10 28 | } 29 | 30 | offset := (page - 1) * pageSize 31 | 32 | qb := authorized.NewQueryBuilder() 33 | qb = qb.WhereIsDeleted(mysql.EqualPredicate, -1) 34 | 35 | if searchData.BusinessKey != "" { 36 | qb.WhereBusinessKey(mysql.EqualPredicate, searchData.BusinessKey) 37 | } 38 | 39 | if searchData.BusinessSecret != "" { 40 | qb.WhereBusinessSecret(mysql.EqualPredicate, searchData.BusinessSecret) 41 | } 42 | 43 | if searchData.BusinessDeveloper != "" { 44 | qb.WhereBusinessDeveloper(mysql.EqualPredicate, searchData.BusinessDeveloper) 45 | } 46 | 47 | listData, err = qb. 48 | Limit(pageSize). 49 | Offset(offset). 50 | OrderById(false). 51 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /internal/services/authorized/service_pagelistcount.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized" 7 | ) 8 | 9 | func (s *service) PageListCount(ctx core.Context, searchData *SearchData) (total int64, err error) { 10 | qb := authorized.NewQueryBuilder() 11 | qb = qb.WhereIsDeleted(mysql.EqualPredicate, -1) 12 | 13 | if searchData.BusinessKey != "" { 14 | qb.WhereBusinessKey(mysql.EqualPredicate, searchData.BusinessKey) 15 | } 16 | 17 | if searchData.BusinessSecret != "" { 18 | qb.WhereBusinessSecret(mysql.EqualPredicate, searchData.BusinessSecret) 19 | } 20 | 21 | if searchData.BusinessDeveloper != "" { 22 | qb.WhereBusinessDeveloper(mysql.EqualPredicate, searchData.BusinessDeveloper) 23 | } 24 | 25 | total, err = qb.Count(s.db.GetDbR().WithContext(ctx.RequestContext())) 26 | if err != nil { 27 | return 0, err 28 | } 29 | 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /internal/services/authorized/service_updateused.go: -------------------------------------------------------------------------------- 1 | package authorized 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/configs" 5 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/authorized" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | 10 | "gorm.io/gorm" 11 | ) 12 | 13 | func (s *service) UpdateUsed(ctx core.Context, id int32, used int32) (err error) { 14 | authorizedInfo, err := authorized.NewQueryBuilder(). 15 | WhereIsDeleted(mysql.EqualPredicate, -1). 16 | WhereId(mysql.EqualPredicate, id). 17 | First(s.db.GetDbR().WithContext(ctx.RequestContext())) 18 | 19 | if err == gorm.ErrRecordNotFound { 20 | return nil 21 | } 22 | 23 | data := map[string]interface{}{ 24 | "is_used": used, 25 | "updated_user": ctx.SessionUserInfo().UserName, 26 | } 27 | 28 | qb := authorized.NewQueryBuilder() 29 | qb.WhereId(mysql.EqualPredicate, id) 30 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | s.cache.Del(configs.RedisKeyPrefixSignature+authorizedInfo.BusinessKey, redis.WithTrace(ctx.Trace())) 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /internal/services/cron/service.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/cron" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | ) 10 | 11 | var _ Service = (*service)(nil) 12 | 13 | type Service interface { 14 | i() 15 | 16 | Create(ctx core.Context, createData *CreateCronTaskData) (id int32, err error) 17 | Modify(ctx core.Context, id int32, modifyData *ModifyCronTaskData) (err error) 18 | PageList(ctx core.Context, searchData *SearchData) (listData []*cron_task.CronTask, err error) 19 | PageListCount(ctx core.Context, searchData *SearchData) (total int64, err error) 20 | UpdateUsed(ctx core.Context, id int32, used int32) (err error) 21 | Execute(ctx core.Context, id int32) (err error) 22 | Detail(ctx core.Context, searchOneData *SearchOneData) (info *cron_task.CronTask, err error) 23 | } 24 | 25 | type service struct { 26 | db mysql.Repo 27 | cache redis.Repo 28 | cronServer cron.Server 29 | } 30 | 31 | func New(db mysql.Repo, cache redis.Repo, cron cron.Server) Service { 32 | return &service{ 33 | db: db, 34 | cache: cache, 35 | cronServer: cron, 36 | } 37 | } 38 | 39 | func (s *service) i() {} 40 | -------------------------------------------------------------------------------- /internal/services/cron/service_create.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 6 | ) 7 | 8 | type CreateCronTaskData struct { 9 | Name string // 任务名称 10 | Spec string // crontab 表达式 11 | Command string // 执行命令 12 | Protocol int32 // 执行方式 1:shell 2:http 13 | HttpMethod int32 // http 请求方式 1:get 2:post 14 | Timeout int32 // 超时时间(单位:秒) 15 | RetryTimes int32 // 重试次数 16 | RetryInterval int32 // 重试间隔(单位:秒) 17 | NotifyStatus int32 // 执行结束是否通知 1:不通知 2:失败通知 3:结束通知 4:结果关键字匹配通知 18 | NotifyType int32 // 通知类型 1:邮件 2:webhook 19 | NotifyReceiverEmail string // 通知者邮箱地址(多个用,分割) 20 | NotifyKeyword string // 通知匹配关键字(多个用,分割) 21 | Remark string // 备注 22 | IsUsed int32 // 是否启用 1:是 -1:否 23 | } 24 | 25 | func (s *service) Create(ctx core.Context, createData *CreateCronTaskData) (id int32, err error) { 26 | model := cron_task.NewModel() 27 | model.Name = createData.Name 28 | model.Spec = createData.Spec 29 | model.Command = createData.Command 30 | model.Protocol = createData.Protocol 31 | model.HttpMethod = createData.HttpMethod 32 | model.Timeout = createData.Timeout 33 | model.RetryTimes = createData.RetryTimes 34 | model.RetryInterval = createData.RetryInterval 35 | model.NotifyStatus = createData.NotifyStatus 36 | model.NotifyType = createData.NotifyType 37 | model.NotifyReceiverEmail = createData.NotifyReceiverEmail 38 | model.NotifyKeyword = createData.NotifyKeyword 39 | model.Remark = createData.Remark 40 | model.IsUsed = createData.IsUsed 41 | model.CreatedUser = ctx.SessionUserInfo().UserName 42 | 43 | id, err = model.Create(s.db.GetDbW().WithContext(ctx.RequestContext())) 44 | if err != nil { 45 | return 0, err 46 | } 47 | 48 | s.cronServer.AddTask(model) 49 | 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /internal/services/cron/service_detail.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 7 | ) 8 | 9 | type SearchOneData struct { 10 | Id int32 // 任务ID 11 | } 12 | 13 | func (s *service) Detail(ctx core.Context, searchOneData *SearchOneData) (info *cron_task.CronTask, err error) { 14 | qb := cron_task.NewQueryBuilder() 15 | 16 | if searchOneData.Id != 0 { 17 | qb.WhereId(mysql.EqualPredicate, searchOneData.Id) 18 | } 19 | 20 | info, err = qb.QueryOne(s.db.GetDbR().WithContext(ctx.RequestContext())) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /internal/services/cron/service_execute.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 7 | ) 8 | 9 | func (s *service) Execute(ctx core.Context, id int32) (err error) { 10 | qb := cron_task.NewQueryBuilder() 11 | qb.WhereId(mysql.EqualPredicate, id) 12 | info, err := qb.QueryOne(s.db.GetDbR().WithContext(ctx.RequestContext())) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | info.Spec = "手动执行" 18 | go s.cronServer.AddJob(info)() 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/services/cron/service_pagelist.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 7 | ) 8 | 9 | type SearchData struct { 10 | Page int // 第几页 11 | PageSize int // 每页显示条数 12 | Name string // 任务名称 13 | Protocol int32 // 执行方式 14 | IsUsed int32 // 是否启用 15 | } 16 | 17 | func (s *service) PageList(ctx core.Context, searchData *SearchData) (listData []*cron_task.CronTask, err error) { 18 | page := searchData.Page 19 | if page == 0 { 20 | page = 1 21 | } 22 | 23 | pageSize := searchData.PageSize 24 | if pageSize == 0 { 25 | pageSize = 10 26 | } 27 | 28 | offset := (page - 1) * pageSize 29 | 30 | qb := cron_task.NewQueryBuilder() 31 | 32 | if searchData.Name != "" { 33 | qb.WhereName(mysql.EqualPredicate, searchData.Name) 34 | } 35 | 36 | if searchData.Protocol != 0 { 37 | qb.WhereProtocol(mysql.EqualPredicate, searchData.Protocol) 38 | } 39 | 40 | if searchData.IsUsed != 0 { 41 | qb.WhereIsUsed(mysql.EqualPredicate, searchData.IsUsed) 42 | } 43 | 44 | listData, err = qb. 45 | Limit(pageSize). 46 | Offset(offset). 47 | OrderById(false). 48 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /internal/services/cron/service_pagelistcount.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 7 | ) 8 | 9 | func (s *service) PageListCount(ctx core.Context, searchData *SearchData) (total int64, err error) { 10 | qb := cron_task.NewQueryBuilder() 11 | 12 | if searchData.Name != "" { 13 | qb.WhereName(mysql.EqualPredicate, searchData.Name) 14 | } 15 | 16 | if searchData.Protocol != 0 { 17 | qb.WhereProtocol(mysql.EqualPredicate, searchData.Protocol) 18 | } 19 | 20 | if searchData.IsUsed != 0 { 21 | qb.WhereIsUsed(mysql.EqualPredicate, searchData.IsUsed) 22 | } 23 | 24 | total, err = qb.Count(s.db.GetDbR().WithContext(ctx.RequestContext())) 25 | if err != nil { 26 | return 0, err 27 | } 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /internal/services/cron/service_updateused.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/cron_task" 7 | 8 | "github.com/spf13/cast" 9 | ) 10 | 11 | func (s *service) UpdateUsed(ctx core.Context, id int32, used int32) (err error) { 12 | data := map[string]interface{}{ 13 | "is_used": used, 14 | "updated_user": ctx.SessionUserInfo().UserName, 15 | } 16 | 17 | qb := cron_task.NewQueryBuilder() 18 | qb.WhereId(mysql.EqualPredicate, id) 19 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | // region 操作定时任务 避免主从同步延迟,在这需要查询主库 25 | if used == cron_task.IsUsedNo { 26 | s.cronServer.RemoveTask(cast.ToInt(id)) 27 | } else { 28 | qb = cron_task.NewQueryBuilder() 29 | qb.WhereId(mysql.EqualPredicate, id) 30 | info, err := qb.QueryOne(s.db.GetDbW().WithContext(ctx.RequestContext())) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | s.cronServer.RemoveTask(cast.ToInt(id)) 36 | s.cronServer.AddTask(info) 37 | 38 | } 39 | // endregion 40 | 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /internal/services/menu/service.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu_action" 8 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 9 | ) 10 | 11 | var _ Service = (*service)(nil) 12 | 13 | type Service interface { 14 | i() 15 | 16 | Create(ctx core.Context, menuData *CreateMenuData) (id int32, err error) 17 | Modify(ctx core.Context, id int32, menuData *UpdateMenuData) (err error) 18 | List(ctx core.Context, searchData *SearchData) (listData []*menu.Menu, err error) 19 | UpdateUsed(ctx core.Context, id int32, used int32) (err error) 20 | UpdateSort(ctx core.Context, id int32, sort int32) (err error) 21 | Delete(ctx core.Context, id int32) (err error) 22 | Detail(ctx core.Context, searchOneData *SearchOneData) (info *menu.Menu, err error) 23 | 24 | CreateAction(ctx core.Context, menuActionData *CreateMenuActionData) (id int32, err error) 25 | ListAction(ctx core.Context, searchListActionData *SearchListActionData) (listData []*menu_action.MenuAction, err error) 26 | DeleteAction(ctx core.Context, id int32) (err error) 27 | } 28 | 29 | type service struct { 30 | db mysql.Repo 31 | cache redis.Repo 32 | } 33 | 34 | func New(db mysql.Repo, cache redis.Repo) Service { 35 | return &service{ 36 | db: db, 37 | cache: cache, 38 | } 39 | } 40 | 41 | func (s *service) i() {} 42 | -------------------------------------------------------------------------------- /internal/services/menu/service_create.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 6 | ) 7 | 8 | type CreateMenuData struct { 9 | Pid int32 // 父类ID 10 | Name string // 菜单名称 11 | Link string // 链接地址 12 | Icon string // 图标 13 | Level int32 // 菜单类型 1:一级菜单 2:二级菜单 14 | } 15 | 16 | func (s *service) Create(ctx core.Context, menuData *CreateMenuData) (id int32, err error) { 17 | model := menu.NewModel() 18 | model.Pid = menuData.Pid 19 | model.Name = menuData.Name 20 | model.Link = menuData.Link 21 | model.Icon = menuData.Icon 22 | model.Level = menuData.Level 23 | model.CreatedUser = ctx.SessionUserInfo().UserName 24 | model.IsUsed = 1 25 | model.IsDeleted = -1 26 | 27 | id, err = model.Create(s.db.GetDbW().WithContext(ctx.RequestContext())) 28 | if err != nil { 29 | return 0, err 30 | } 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /internal/services/menu/service_createaction.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu_action" 6 | ) 7 | 8 | type CreateMenuActionData struct { 9 | MenuId int32 `json:"menu_id"` // 菜单栏ID 10 | Method string `json:"method"` // 请求方法 11 | API string `json:"api"` // 请求地址 12 | } 13 | 14 | func (s *service) CreateAction(ctx core.Context, menuActionData *CreateMenuActionData) (id int32, err error) { 15 | model := menu_action.NewModel() 16 | model.MenuId = menuActionData.MenuId 17 | model.Method = menuActionData.Method 18 | model.Api = menuActionData.API 19 | model.CreatedUser = ctx.SessionUserInfo().UserName 20 | model.IsDeleted = -1 21 | 22 | id, err = model.Create(s.db.GetDbW().WithContext(ctx.RequestContext())) 23 | if err != nil { 24 | return 0, err 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /internal/services/menu/service_delete.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 7 | ) 8 | 9 | func (s *service) Delete(ctx core.Context, id int32) (err error) { 10 | data := map[string]interface{}{ 11 | "is_deleted": 1, 12 | "updated_user": ctx.SessionUserInfo().UserName, 13 | } 14 | 15 | qb := menu.NewQueryBuilder() 16 | qb.WhereId(mysql.EqualPredicate, id) 17 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /internal/services/menu/service_deleteaction.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu_action" 7 | 8 | "gorm.io/gorm" 9 | ) 10 | 11 | func (s *service) DeleteAction(ctx core.Context, id int32) (err error) { 12 | // 先查询 id 是否存在 13 | _, err = menu_action.NewQueryBuilder(). 14 | WhereIsDeleted(mysql.EqualPredicate, -1). 15 | WhereId(mysql.EqualPredicate, id). 16 | First(s.db.GetDbR().WithContext(ctx.RequestContext())) 17 | 18 | if err == gorm.ErrRecordNotFound { 19 | return nil 20 | } 21 | 22 | data := map[string]interface{}{ 23 | "is_deleted": 1, 24 | "updated_user": ctx.SessionUserInfo().UserName, 25 | } 26 | 27 | qb := menu_action.NewQueryBuilder() 28 | qb.WhereId(mysql.EqualPredicate, id) 29 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /internal/services/menu/service_detail.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 7 | ) 8 | 9 | type SearchOneData struct { 10 | Id int32 // 用户ID 11 | IsUsed int32 // 是否启用 1:是 -1:否 12 | } 13 | 14 | func (s *service) Detail(ctx core.Context, searchOneData *SearchOneData) (info *menu.Menu, err error) { 15 | 16 | qb := menu.NewQueryBuilder() 17 | qb.WhereIsDeleted(mysql.EqualPredicate, -1) 18 | 19 | if searchOneData.Id != 0 { 20 | qb.WhereId(mysql.EqualPredicate, searchOneData.Id) 21 | } 22 | 23 | if searchOneData.IsUsed != 0 { 24 | qb.WhereIsUsed(mysql.EqualPredicate, searchOneData.IsUsed) 25 | } 26 | 27 | info, err = qb.QueryOne(s.db.GetDbR().WithContext(ctx.RequestContext())) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /internal/services/menu/service_list.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 7 | ) 8 | 9 | type SearchData struct { 10 | Pid int32 // 父类ID 11 | } 12 | 13 | func (s *service) List(ctx core.Context, searchData *SearchData) (listData []*menu.Menu, err error) { 14 | 15 | qb := menu.NewQueryBuilder() 16 | qb.WhereIsDeleted(mysql.EqualPredicate, -1) 17 | 18 | if searchData.Pid != 0 { 19 | qb.WherePid(mysql.EqualPredicate, searchData.Pid) 20 | } 21 | 22 | listData, err = qb. 23 | OrderBySort(true). 24 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /internal/services/menu/service_listaction.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu_action" 7 | ) 8 | 9 | type SearchListActionData struct { 10 | MenuId int32 `json:"menu_id"` // 菜单栏ID 11 | } 12 | 13 | func (s *service) ListAction(ctx core.Context, searchData *SearchListActionData) (listData []*menu_action.MenuAction, err error) { 14 | 15 | qb := menu_action.NewQueryBuilder() 16 | qb.WhereIsDeleted(mysql.EqualPredicate, -1) 17 | 18 | if searchData.MenuId != 0 { 19 | qb.WhereMenuId(mysql.EqualPredicate, searchData.MenuId) 20 | } 21 | 22 | listData, err = qb. 23 | OrderById(false). 24 | QueryAll(s.db.GetDbR().WithContext(ctx.RequestContext())) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /internal/services/menu/service_modify.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 7 | ) 8 | 9 | type UpdateMenuData struct { 10 | Name string // 菜单名称 11 | Link string // 链接地址 12 | Icon string // 图标 13 | } 14 | 15 | func (s *service) Modify(ctx core.Context, id int32, menuData *UpdateMenuData) (err error) { 16 | data := map[string]interface{}{ 17 | "name": menuData.Name, 18 | "link": menuData.Link, 19 | "icon": menuData.Icon, 20 | "updated_user": ctx.SessionUserInfo().UserName, 21 | } 22 | 23 | qb := menu.NewQueryBuilder() 24 | qb.WhereId(mysql.EqualPredicate, id) 25 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /internal/services/menu/service_updatesort.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 7 | ) 8 | 9 | func (s *service) UpdateSort(ctx core.Context, id int32, sort int32) (err error) { 10 | data := map[string]interface{}{ 11 | "sort": sort, 12 | "updated_user": ctx.SessionUserInfo().UserName, 13 | } 14 | 15 | qb := menu.NewQueryBuilder() 16 | qb.WhereId(mysql.EqualPredicate, id) 17 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /internal/services/menu/service_updateused.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql/menu" 7 | ) 8 | 9 | func (s *service) UpdateUsed(ctx core.Context, id int32, used int32) (err error) { 10 | data := map[string]interface{}{ 11 | "is_used": used, 12 | "updated_user": ctx.SessionUserInfo().UserName, 13 | } 14 | 15 | qb := menu.NewQueryBuilder() 16 | qb.WhereId(mysql.EqualPredicate, id) 17 | err = qb.Updates(s.db.GetDbW().WithContext(ctx.RequestContext()), data) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /internal/websocket/sysmessage/sysmessage.go: -------------------------------------------------------------------------------- 1 | package sysmessage 2 | 3 | import ( 4 | "github.com/xinliangnote/go-gin-api/internal/pkg/core" 5 | "github.com/xinliangnote/go-gin-api/internal/repository/mysql" 6 | "github.com/xinliangnote/go-gin-api/internal/repository/redis" 7 | "github.com/xinliangnote/go-gin-api/internal/repository/socket" 8 | "github.com/xinliangnote/go-gin-api/pkg/errors" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var ( 14 | err error 15 | server socket.Server 16 | ) 17 | 18 | type handler struct { 19 | logger *zap.Logger 20 | cache redis.Repo 21 | db mysql.Repo 22 | } 23 | 24 | func New(logger *zap.Logger, db mysql.Repo, cache redis.Repo) *handler { 25 | return &handler{ 26 | logger: logger, 27 | cache: cache, 28 | db: db, 29 | } 30 | } 31 | 32 | func GetConn() (socket.Server, error) { 33 | if server != nil { 34 | return server, nil 35 | } 36 | 37 | return nil, errors.New("conn is nil") 38 | } 39 | 40 | func (h *handler) Connect() core.HandlerFunc { 41 | return func(ctx core.Context) { 42 | server, err = socket.New(h.logger, h.db, h.cache, ctx.ResponseWriter(), ctx.Request(), nil) 43 | if err != nil { 44 | return 45 | } 46 | 47 | go server.OnMessage() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/go-gin-api/8fd9a005d1953545f74f68cc0e0bc1fa14b332cf/logs/.gitkeep -------------------------------------------------------------------------------- /pkg/aes/aes_test.go: -------------------------------------------------------------------------------- 1 | package aes 2 | 3 | import "testing" 4 | 5 | const ( 6 | key = "IgkibX71IEf382PT" 7 | iv = "IgkibX71IEf382PT" 8 | ) 9 | 10 | func TestEncrypt(t *testing.T) { 11 | t.Log(New(key, iv).Encrypt("123456")) 12 | } 13 | 14 | func TestDecrypt(t *testing.T) { 15 | t.Log(New(key, iv).Decrypt("GO-ri84zevE-z1biJwfQPw==")) 16 | } 17 | 18 | func BenchmarkEncryptAndDecrypt(b *testing.B) { 19 | b.ResetTimer() 20 | aes := New(key, iv) 21 | for i := 0; i < b.N; i++ { 22 | encryptString, _ := aes.Encrypt("123456") 23 | aes.Decrypt(encryptString) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/browser/browser.go: -------------------------------------------------------------------------------- 1 | package browser 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "runtime" 7 | ) 8 | 9 | var commands = map[string]string{ 10 | "windows": "start", 11 | "darwin": "open", 12 | "linux": "xdg-open", 13 | } 14 | 15 | func Open(uri string) error { 16 | run, ok := commands[runtime.GOOS] 17 | if !ok { 18 | return fmt.Errorf("don't know how to open things on %s platform", runtime.GOOS) 19 | } 20 | 21 | cmd := exec.Command(run, uri) 22 | return cmd.Start() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/color/string_darwin.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package color 4 | 5 | import ( 6 | "fmt" 7 | "math/rand" 8 | "strconv" 9 | ) 10 | 11 | var _ = RandomColor() 12 | 13 | // RandomColor generates a random color. 14 | func RandomColor() string { 15 | return fmt.Sprintf("#%s", strconv.FormatInt(int64(rand.Intn(16777216)), 16)) 16 | } 17 | 18 | // Yellow ... 19 | func Yellow(msg string) string { 20 | return fmt.Sprintf("\x1b[33m%s\x1b[0m", msg) 21 | } 22 | 23 | // Red ... 24 | func Red(msg string) string { 25 | return fmt.Sprintf("\x1b[31m%s\x1b[0m", msg) 26 | } 27 | 28 | // Redf ... 29 | func Redf(msg string, arg interface{}) string { 30 | return fmt.Sprintf("\x1b[31m%s\x1b[0m %+v\n", msg, arg) 31 | } 32 | 33 | // Blue ... 34 | func Blue(msg string) string { 35 | return fmt.Sprintf("\x1b[34m%s\x1b[0m", msg) 36 | } 37 | 38 | // Green ... 39 | func Green(msg string) string { 40 | return fmt.Sprintf("\x1b[32m%s\x1b[0m", msg) 41 | } 42 | 43 | // Greenf ... 44 | func Greenf(msg string, arg interface{}) string { 45 | return fmt.Sprintf("\x1b[32m%s\x1b[0m %+v\n", msg, arg) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/color/string_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package color 4 | 5 | import ( 6 | "fmt" 7 | "math/rand" 8 | "strconv" 9 | ) 10 | 11 | var _ = RandomColor() 12 | 13 | // RandomColor generates a random color. 14 | func RandomColor() string { 15 | return fmt.Sprintf("#%s", strconv.FormatInt(int64(rand.Intn(16777216)), 16)) 16 | } 17 | 18 | // Yellow ... 19 | func Yellow(msg string) string { 20 | return fmt.Sprintf("\x1b[33m%s\x1b[0m", msg) 21 | } 22 | 23 | // Red ... 24 | func Red(msg string) string { 25 | return fmt.Sprintf("\x1b[31m%s\x1b[0m", msg) 26 | } 27 | 28 | // Redf ... 29 | func Redf(msg string, arg interface{}) string { 30 | return fmt.Sprintf("\x1b[31m%s\x1b[0m %+v\n", msg, arg) 31 | } 32 | 33 | // Blue ... 34 | func Blue(msg string) string { 35 | return fmt.Sprintf("\x1b[34m%s\x1b[0m", msg) 36 | } 37 | 38 | // Green ... 39 | func Green(msg string) string { 40 | return fmt.Sprintf("\x1b[32m%s\x1b[0m", msg) 41 | } 42 | 43 | // Greenf ... 44 | func Greenf(msg string, arg interface{}) string { 45 | return fmt.Sprintf("\x1b[32m%s\x1b[0m %+v\n", msg, arg) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/color/string_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package color 4 | 5 | import ( 6 | "fmt" 7 | "math/rand" 8 | "strconv" 9 | ) 10 | 11 | var _ = RandomColor() 12 | 13 | // RandomColor generates a random color. 14 | func RandomColor() string { 15 | return fmt.Sprintf("#%s", strconv.FormatInt(int64(rand.Intn(16777216)), 16)) 16 | } 17 | 18 | // Yellow ... 19 | func Yellow(msg string) string { 20 | return fmt.Sprintf("%s", msg) 21 | } 22 | 23 | // Red ... 24 | func Red(msg string) string { 25 | return fmt.Sprintf("%s", msg) 26 | } 27 | 28 | // Redf ... 29 | func Redf(msg string, arg interface{}) string { 30 | return fmt.Sprintf("%s %+v\n", msg, arg) 31 | } 32 | 33 | // Blue ... 34 | func Blue(msg string) string { 35 | return fmt.Sprintf("%s", msg) 36 | } 37 | 38 | // Green ... 39 | func Green(msg string) string { 40 | return fmt.Sprintf("%s", msg) 41 | } 42 | 43 | // Greenf ... 44 | func Greenf(msg string, arg interface{}) string { 45 | return fmt.Sprintf("%s %+v\n", msg, arg) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/ddm/README.md: -------------------------------------------------------------------------------- 1 | ## DDM 2 | 3 | 动态数据掩码(Dynamic Data Masking,简称为DDM)能够防止把敏感数据暴露给未经授权的用户。 4 | 5 | | 类型 | 要求 | 示例 | 说明 6 | | ---- | ---- | ---- | ---- 7 | | 手机号 | 前 3 后 4 | 132****7986 | 定长 11 位数字 8 | | 邮箱地址 | 前 1 后 1 | l**w@gmail.com | 仅对 @ 之前的邮箱名称进行掩码 9 | | 姓名 | 隐姓 | *鸿章 | 将姓氏隐藏 10 | | 密码 | 不输出 | ****** | 11 | | 银行卡卡号 | 前 6 后 4 | 622888******5676 | 银行卡卡号最多 19 位数字 12 | | 身份证号 | 前 1 后 1 | 1******7 | 定长 18 位 13 | 14 | #### 代码示例 15 | 16 | ``` 17 | // 返回值 18 | type message struct { 19 | Email ddm.Email `json:"email"` 20 | } 21 | 22 | msg := new(message) 23 | msg.Email = ddm.Email("xinliangnote@163.com") 24 | ... 25 | 26 | ``` 27 | -------------------------------------------------------------------------------- /pkg/ddm/mark.go: -------------------------------------------------------------------------------- 1 | package ddm 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func (m Mobile) MarshalJSON() ([]byte, error) { 9 | if len(m) != 11 { 10 | return []byte(`"` + m + `"`), nil 11 | } 12 | 13 | v := fmt.Sprintf("%s****%s", m[:3], m[len(m)-4:]) 14 | return []byte(`"` + v + `"`), nil 15 | } 16 | 17 | func (bc BankCard) MarshalJSON() ([]byte, error) { 18 | if len(bc) > 19 || len(bc) < 16 { 19 | return []byte(`"` + bc + `"`), nil 20 | } 21 | 22 | v := fmt.Sprintf("%s******%s", bc[:6], bc[len(bc)-4:]) 23 | return []byte(`"` + v + `"`), nil 24 | } 25 | 26 | func (card IDCard) MarshalJSON() ([]byte, error) { 27 | if len(card) != 18 { 28 | return []byte(`"` + card + `"`), nil 29 | } 30 | 31 | v := fmt.Sprintf("%s******%s", card[:1], card[len(card)-1:]) 32 | return []byte(`"` + v + `"`), nil 33 | } 34 | 35 | func (name IDName) MarshalJSON() ([]byte, error) { 36 | if len(name) < 1 { 37 | return []byte(`""`), nil 38 | } 39 | 40 | nameRune := []rune(name) 41 | v := fmt.Sprintf("*%s", string(nameRune[1:])) 42 | return []byte(`"` + v + `"`), nil 43 | } 44 | 45 | func (pw PassWord) MarshalJSON() ([]byte, error) { 46 | v := "******" 47 | return []byte(`"` + v + `"`), nil 48 | } 49 | 50 | func (e Email) MarshalJSON() ([]byte, error) { 51 | if !strings.Contains(string(e), "@") { 52 | return []byte(`"` + e + `"`), nil 53 | } 54 | 55 | split := strings.Split(string(e), "@") 56 | if len(split[0]) < 1 || len(split[1]) < 1 { 57 | return []byte(`"` + e + `"`), nil 58 | } 59 | 60 | v := fmt.Sprintf("%s***%s", split[0][:1], split[0][len(split[0])-1:]) 61 | return []byte(`"` + v + "@" + split[1] + `"`), nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/ddm/type.go: -------------------------------------------------------------------------------- 1 | package ddm 2 | 3 | // Mobile 手机号 132****7986 4 | type Mobile string 5 | 6 | // BankCard 银行卡号 622888******5676 7 | type BankCard string 8 | 9 | // IDCard 身份证号 1******7 10 | type IDCard string 11 | 12 | // IDName 姓名 *鸿章 13 | // TODO:参考 https://blog.thinkeridea.com/201910/go/efficient_string_truncation.html 14 | // Deprecated:有更好的性能选择 15 | type IDName string 16 | 17 | // PassWord 密码 ****** 18 | type PassWord string 19 | 20 | // Email 邮箱 l***w@gmail.com 21 | type Email string 22 | -------------------------------------------------------------------------------- /pkg/ddm/type_test.go: -------------------------------------------------------------------------------- 1 | package ddm 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | type message struct { 9 | Name IDName `json:"name"` 10 | Mobile Mobile `json:"mobile"` 11 | IDCard IDCard `json:"id_card"` 12 | PassWord PassWord `json:"password"` 13 | Email Email `json:"email"` 14 | BankCard1 BankCard `json:"bank_card_1"` 15 | BankCard2 BankCard `json:"bank_card_2"` 16 | BankCard3 BankCard `json:"bank_card_3"` 17 | } 18 | 19 | func TestMarshalJSON(t *testing.T) { 20 | msg := new(message) 21 | msg.Name = IDName("李鸿章") 22 | msg.Mobile = Mobile("13288887986") 23 | msg.IDCard = IDCard("125252525252525252") 24 | msg.PassWord = PassWord("123456") 25 | msg.Email = Email("xinliangnote@163.com") 26 | msg.BankCard1 = BankCard("6545654565456545") 27 | msg.BankCard2 = BankCard("65485269874569852") 28 | msg.BankCard3 = BankCard("6548526987456985298") 29 | 30 | marshal, _ := json.Marshal(msg) 31 | t.Log(string(marshal)) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/debugs/print.go: -------------------------------------------------------------------------------- 1 | package debugs 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/xinliangnote/go-gin-api/pkg/trace" 8 | ) 9 | 10 | type Option func(*option) 11 | 12 | type Trace = trace.T 13 | 14 | type option struct { 15 | Trace *trace.Trace 16 | Debug *trace.Debug 17 | } 18 | 19 | func newOption() *option { 20 | return &option{} 21 | } 22 | 23 | func Println(key string, value interface{}, options ...Option) { 24 | ts := time.Now() 25 | opt := newOption() 26 | defer func() { 27 | if opt.Trace != nil { 28 | opt.Debug.Key = key 29 | opt.Debug.Value = value 30 | opt.Debug.CostSeconds = time.Since(ts).Seconds() 31 | opt.Trace.AppendDebug(opt.Debug) 32 | } 33 | }() 34 | 35 | for _, f := range options { 36 | f(opt) 37 | } 38 | 39 | fmt.Println(fmt.Sprintf("KEY: %s | VALUE: %v", key, value)) 40 | } 41 | 42 | // WithTrace 设置trace信息 43 | func WithTrace(t Trace) Option { 44 | return func(opt *option) { 45 | if t != nil { 46 | opt.Trace = t.(*trace.Trace) 47 | opt.Debug = new(trace.Debug) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | active Environment 11 | dev Environment = &environment{value: "dev"} 12 | fat Environment = &environment{value: "fat"} 13 | uat Environment = &environment{value: "uat"} 14 | pro Environment = &environment{value: "pro"} 15 | ) 16 | 17 | var _ Environment = (*environment)(nil) 18 | 19 | // Environment 环境配置 20 | type Environment interface { 21 | Value() string 22 | IsDev() bool 23 | IsFat() bool 24 | IsUat() bool 25 | IsPro() bool 26 | t() 27 | } 28 | 29 | type environment struct { 30 | value string 31 | } 32 | 33 | func (e *environment) Value() string { 34 | return e.value 35 | } 36 | 37 | func (e *environment) IsDev() bool { 38 | return e.value == "dev" 39 | } 40 | 41 | func (e *environment) IsFat() bool { 42 | return e.value == "fat" 43 | } 44 | 45 | func (e *environment) IsUat() bool { 46 | return e.value == "uat" 47 | } 48 | 49 | func (e *environment) IsPro() bool { 50 | return e.value == "pro" 51 | } 52 | 53 | func (e *environment) t() {} 54 | 55 | func init() { 56 | env := flag.String("env", "", "请输入运行环境:\n dev:开发环境\n fat:测试环境\n uat:预上线环境\n pro:正式环境\n") 57 | flag.Parse() 58 | 59 | switch strings.ToLower(strings.TrimSpace(*env)) { 60 | case "dev": 61 | active = dev 62 | case "fat": 63 | active = fat 64 | case "uat": 65 | active = uat 66 | case "pro": 67 | active = pro 68 | default: 69 | active = fat 70 | fmt.Println("Warning: '-env' cannot be found, or it is illegal. The default 'fat' will be used.") 71 | } 72 | } 73 | 74 | // Active 当前配置的env 75 | func Active() Environment { 76 | return active 77 | } 78 | -------------------------------------------------------------------------------- /pkg/errors/err_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | func TestErr(t *testing.T) { 11 | logger, _ := zap.NewProduction() 12 | 13 | logger.Info("errorf", zap.Error(Errorf("%s %d", "127.0.0.1", 80))) 14 | 15 | err := New("a dummy err") 16 | logger.Info("new", zap.Error(err)) 17 | 18 | err = Wrap(err, "ping timeout err") 19 | logger.Info("wrap", zap.Error(err)) 20 | 21 | err = Wrapf(err, "ip: %s port: %d", "localhost", 80) 22 | logger.Info("wrapf", zap.Error(err)) 23 | 24 | err = WithStack(err) 25 | logger.Info("withstack", zap.Error(err)) 26 | 27 | logger.Info("wrap std", zap.Error(Wrap(errors.New("std err"), "some err occurs"))) 28 | 29 | logger.Info("wrapf std", zap.Error(Wrapf(errors.New("std err"), "ip: %s port: %d", "localhost", 80))) 30 | 31 | logger.Info("withstack std", zap.Error(WithStack(errors.New("std err")))) 32 | 33 | t.Logf("%+v", New("a dummy error")) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/hash/hash.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | var _ Hash = (*hash)(nil) 4 | 5 | type Hash interface { 6 | i() 7 | 8 | // HashidsEncode 加密 9 | HashidsEncode(params []int) (string, error) 10 | 11 | // HashidsDecode 解密 12 | HashidsDecode(hash string) ([]int, error) 13 | } 14 | 15 | type hash struct { 16 | secret string 17 | length int 18 | } 19 | 20 | func New(secret string, length int) Hash { 21 | return &hash{ 22 | secret: secret, 23 | length: length, 24 | } 25 | } 26 | 27 | func (h *hash) i() {} 28 | -------------------------------------------------------------------------------- /pkg/hash/hash_hashids.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/speps/go-hashids" 5 | ) 6 | 7 | func (h *hash) HashidsEncode(params []int) (string, error) { 8 | hd := hashids.NewData() 9 | hd.Salt = h.secret 10 | hd.MinLength = h.length 11 | 12 | hashStr, err := hashids.NewWithData(hd).Encode(params) 13 | if err != nil { 14 | return "", err 15 | } 16 | 17 | return hashStr, nil 18 | } 19 | 20 | func (h *hash) HashidsDecode(hash string) ([]int, error) { 21 | hd := hashids.NewData() 22 | hd.Salt = h.secret 23 | hd.MinLength = h.length 24 | 25 | ids, err := hashids.NewWithData(hd).DecodeWithError(hash) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return ids, nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/hash/hash_hashids_test.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import "testing" 4 | 5 | const secret = "i1ydX9RtHyuJTrw7frcu" 6 | const length = 12 7 | 8 | func TestHashidsEncode(t *testing.T) { 9 | str, _ := New(secret, length).HashidsEncode([]int{99}) 10 | t.Log(str) 11 | 12 | //GyV5pJqXvwAR 13 | } 14 | 15 | func TestHashidsDecode(t *testing.T) { 16 | ids, _ := New(secret, length).HashidsDecode("GyV5pJqXvwAR") 17 | t.Log(ids) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/httpclient/alarm.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // AlarmVerify Verify parse the body and verify that it is correct 11 | type AlarmVerify func(body []byte) (shouldAlarm bool) 12 | 13 | type AlarmObject interface { 14 | Send(subject, body string) error 15 | } 16 | 17 | func onFailedAlarm(title string, raw []byte, logger *zap.Logger, alarmObject AlarmObject) { 18 | buf := bytes.NewBuffer(nil) 19 | 20 | scanner := bufio.NewScanner(bytes.NewReader(raw)) 21 | for scanner.Scan() { 22 | buf.WriteString(scanner.Text()) 23 | buf.WriteString("
") 24 | } 25 | 26 | if err := alarmObject.Send(title, buf.String()); err != nil && logger != nil { 27 | logger.Error("calls failed alarm err", zap.Error(err)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/httpclient/error.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | var _ ReplyErr = (*replyErr)(nil) 4 | 5 | // ReplyErr 错误响应,当 resp.StatusCode != http.StatusOK 时用来包装返回的 httpcode 和 body 。 6 | type ReplyErr interface { 7 | error 8 | StatusCode() int 9 | Body() []byte 10 | } 11 | 12 | type replyErr struct { 13 | err error 14 | statusCode int 15 | body []byte 16 | } 17 | 18 | func (r *replyErr) Error() string { 19 | return r.err.Error() 20 | } 21 | 22 | func (r *replyErr) StatusCode() int { 23 | return r.statusCode 24 | } 25 | 26 | func (r *replyErr) Body() []byte { 27 | return r.body 28 | } 29 | 30 | func newReplyErr(statusCode int, body []byte, err error) ReplyErr { 31 | return &replyErr{ 32 | statusCode: statusCode, 33 | body: body, 34 | err: err, 35 | } 36 | } 37 | 38 | // ToReplyErr 尝试将 err 转换为 ReplyErr 39 | func ToReplyErr(err error) (ReplyErr, bool) { 40 | if err == nil { 41 | return nil, false 42 | } 43 | 44 | e, ok := err.(ReplyErr) 45 | return e, ok 46 | } 47 | -------------------------------------------------------------------------------- /pkg/httpclient/retry.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | const ( 10 | // DefaultRetryTimes 如果请求失败,最多重试3次 11 | DefaultRetryTimes = 3 12 | // DefaultRetryDelay 在重试前,延迟等待100毫秒 13 | DefaultRetryDelay = time.Millisecond * 100 14 | ) 15 | 16 | // RetryVerify Verify parse the body and verify that it is correct 17 | type RetryVerify func(body []byte) (shouldRetry bool) 18 | 19 | func shouldRetry(ctx context.Context, httpCode int) bool { 20 | select { 21 | case <-ctx.Done(): 22 | return false 23 | default: 24 | } 25 | 26 | switch httpCode { 27 | case 28 | _StatusReadRespErr, 29 | _StatusDoReqErr, 30 | 31 | http.StatusRequestTimeout, 32 | http.StatusLocked, 33 | http.StatusTooEarly, 34 | http.StatusTooManyRequests, 35 | 36 | http.StatusServiceUnavailable, 37 | http.StatusGatewayTimeout: 38 | 39 | return true 40 | 41 | default: 42 | return false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func TestJSONLogger(t *testing.T) { 10 | logger, err := NewJSONLogger( 11 | WithField("defined_key", "defined_value"), 12 | ) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer logger.Sync() 17 | 18 | err = errors.New("pkg error") 19 | logger.Error("err occurs", WrapMeta(nil, NewMeta("para1", "value1"), NewMeta("para2", "value2"))...) 20 | logger.Error("err occurs", WrapMeta(err, NewMeta("para1", "value1"), NewMeta("para2", "value2"))...) 21 | 22 | } 23 | 24 | func BenchmarkJsonLogger(b *testing.B) { 25 | b.ResetTimer() 26 | logger, err := NewJSONLogger( 27 | WithField("defined_key", "defined_value"), 28 | ) 29 | if err != nil { 30 | b.Fatal(err) 31 | } 32 | 33 | defer logger.Sync() 34 | 35 | } 36 | -------------------------------------------------------------------------------- /pkg/mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "strings" 5 | 6 | "gopkg.in/gomail.v2" 7 | ) 8 | 9 | type Options struct { 10 | MailHost string 11 | MailPort int 12 | MailUser string // 发件人 13 | MailPass string // 发件人密码 14 | MailTo string // 收件人 多个用,分割 15 | Subject string // 邮件主题 16 | Body string // 邮件内容 17 | } 18 | 19 | func Send(o *Options) error { 20 | 21 | m := gomail.NewMessage() 22 | 23 | //设置发件人 24 | m.SetHeader("From", o.MailUser) 25 | 26 | //设置发送给多个用户 27 | mailArrTo := strings.Split(o.MailTo, ",") 28 | m.SetHeader("To", mailArrTo...) 29 | 30 | //设置邮件主题 31 | m.SetHeader("Subject", o.Subject) 32 | 33 | //设置邮件正文 34 | m.SetBody("text/html", o.Body) 35 | 36 | d := gomail.NewDialer(o.MailHost, o.MailPort, o.MailUser, o.MailPass) 37 | 38 | return d.DialAndSend(m) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/mail/mail_test.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSend(t *testing.T) { 8 | options := &Options{ 9 | MailHost: "smtp.163.com", 10 | MailPort: 465, 11 | MailUser: "xxx@163.com", 12 | MailPass: "", //密码或授权码 13 | MailTo: "", 14 | Subject: "subject", 15 | Body: "body", 16 | } 17 | err := Send(options) 18 | if err != nil { 19 | t.Error("Mail Send error", err) 20 | return 21 | } 22 | t.Log("success") 23 | } 24 | -------------------------------------------------------------------------------- /pkg/shutdown/shutdown.go: -------------------------------------------------------------------------------- 1 | package shutdown 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | ) 8 | 9 | var _ Hook = (*hook)(nil) 10 | 11 | // Hook a graceful shutdown hook, default with signals of SIGINT and SIGTERM 12 | type Hook interface { 13 | // WithSignals add more signals into hook 14 | WithSignals(signals ...syscall.Signal) Hook 15 | 16 | // Close register shutdown handles 17 | Close(funcs ...func()) 18 | } 19 | 20 | type hook struct { 21 | ctx chan os.Signal 22 | } 23 | 24 | // NewHook create a Hook instance 25 | func NewHook() Hook { 26 | hook := &hook{ 27 | ctx: make(chan os.Signal, 1), 28 | } 29 | 30 | return hook.WithSignals(syscall.SIGINT, syscall.SIGTERM) 31 | } 32 | 33 | func (h *hook) WithSignals(signals ...syscall.Signal) Hook { 34 | for _, s := range signals { 35 | signal.Notify(h.ctx, s) 36 | } 37 | 38 | return h 39 | } 40 | 41 | func (h *hook) Close(funcs ...func()) { 42 | select { 43 | case <-h.ctx: 44 | } 45 | signal.Stop(h.ctx) 46 | 47 | for _, f := range funcs { 48 | f() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/signature/signature.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "time" 7 | ) 8 | 9 | var _ Signature = (*signature)(nil) 10 | 11 | const ( 12 | delimiter = "|" 13 | ) 14 | 15 | // 合法的 Methods 16 | var methods = map[string]bool{ 17 | http.MethodGet: true, 18 | http.MethodPost: true, 19 | http.MethodHead: true, 20 | http.MethodPut: true, 21 | http.MethodPatch: true, 22 | http.MethodDelete: true, 23 | http.MethodConnect: true, 24 | http.MethodOptions: true, 25 | http.MethodTrace: true, 26 | } 27 | 28 | type Signature interface { 29 | i() 30 | 31 | // Generate 生成签名 32 | Generate(path string, method string, params url.Values) (authorization, date string, err error) 33 | 34 | // Verify 验证签名 35 | Verify(authorization, date string, path string, method string, params url.Values) (ok bool, err error) 36 | } 37 | 38 | type signature struct { 39 | key string 40 | secret string 41 | ttl time.Duration 42 | } 43 | 44 | func New(key, secret string, ttl time.Duration) Signature { 45 | return &signature{ 46 | key: key, 47 | secret: secret, 48 | ttl: ttl, 49 | } 50 | } 51 | 52 | func (s *signature) i() {} 53 | -------------------------------------------------------------------------------- /pkg/signature/signature_generate.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "fmt" 9 | "net/url" 10 | "strings" 11 | 12 | "github.com/xinliangnote/go-gin-api/pkg/errors" 13 | "github.com/xinliangnote/go-gin-api/pkg/timeutil" 14 | ) 15 | 16 | // Generate 17 | // path 请求的路径 (不附带 querystring) 18 | func (s *signature) Generate(path string, method string, params url.Values) (authorization, date string, err error) { 19 | if path == "" { 20 | err = errors.New("path required") 21 | return 22 | } 23 | 24 | if method == "" { 25 | err = errors.New("method required") 26 | return 27 | } 28 | 29 | methodName := strings.ToUpper(method) 30 | if !methods[methodName] { 31 | err = errors.New("method param error") 32 | return 33 | } 34 | 35 | // Date 36 | date = timeutil.CSTLayoutString() 37 | 38 | // Encode() 方法中自带 sorted by key 39 | sortParamsEncode, err := url.QueryUnescape(params.Encode()) 40 | if err != nil { 41 | err = errors.Errorf("url QueryUnescape error %v", err) 42 | return 43 | } 44 | 45 | // 加密字符串规则 46 | buffer := bytes.NewBuffer(nil) 47 | buffer.WriteString(path) 48 | buffer.WriteString(delimiter) 49 | buffer.WriteString(methodName) 50 | buffer.WriteString(delimiter) 51 | buffer.WriteString(sortParamsEncode) 52 | buffer.WriteString(delimiter) 53 | buffer.WriteString(date) 54 | 55 | // 对数据进行 sha256 加密,并进行 base64 encode 56 | hash := hmac.New(sha256.New, []byte(s.secret)) 57 | hash.Write(buffer.Bytes()) 58 | digest := base64.StdEncoding.EncodeToString(hash.Sum(nil)) 59 | 60 | authorization = fmt.Sprintf("%s %s", s.key, digest) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /pkg/signature/signature_test.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const ( 10 | key = "blog" 11 | secret = "i1ydX9RtHyuJTrw7frcu" 12 | ttl = time.Minute * 10 13 | ) 14 | 15 | func TestSignature_Generate(t *testing.T) { 16 | path := "/echo" 17 | method := "POST" 18 | 19 | params := url.Values{} 20 | params.Add("a", "a1") 21 | params.Add("d", "d1") 22 | params.Add("c", "c1 c2") 23 | 24 | authorization, date, err := New(key, secret, ttl).Generate(path, method, params) 25 | t.Log("authorization:", authorization) 26 | t.Log("date:", date) 27 | t.Log("err:", err) 28 | } 29 | 30 | func TestSignature_Verify(t *testing.T) { 31 | 32 | authorization := "blog y7a326f3aWvIxdeNIgRo0P7FSDnCNSsN8gJi/4y+cZo=" 33 | date := "2021-04-06 16:15:26" 34 | 35 | path := "/echo" 36 | method := "post" 37 | params := url.Values{} 38 | params.Add("a", "a1") 39 | params.Add("d", "d1") 40 | params.Add("c", "c1 c2*") 41 | 42 | ok, err := New(key, secret, ttl).Verify(authorization, date, path, method, params) 43 | t.Log(ok) 44 | t.Log(err) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/signature/signature_verify.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "fmt" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "github.com/xinliangnote/go-gin-api/pkg/errors" 14 | "github.com/xinliangnote/go-gin-api/pkg/timeutil" 15 | ) 16 | 17 | func (s *signature) Verify(authorization, date string, path string, method string, params url.Values) (ok bool, err error) { 18 | if date == "" { 19 | err = errors.New("date required") 20 | return 21 | } 22 | 23 | if path == "" { 24 | err = errors.New("path required") 25 | return 26 | } 27 | 28 | if method == "" { 29 | err = errors.New("method required") 30 | return 31 | } 32 | 33 | methodName := strings.ToUpper(method) 34 | if !methods[methodName] { 35 | err = errors.New("method param error") 36 | return 37 | } 38 | 39 | ts, err := timeutil.ParseCSTInLocation(date) 40 | if err != nil { 41 | err = errors.New("date must follow '2006-01-02 15:04:05'") 42 | return 43 | } 44 | 45 | if timeutil.SubInLocation(ts) > float64(s.ttl/time.Second) { 46 | err = errors.Errorf("date exceeds limit %v", s.ttl) 47 | return 48 | } 49 | 50 | // Encode() 方法中自带 sorted by key 51 | sortParamsEncode, err := url.QueryUnescape(params.Encode()) 52 | if err != nil { 53 | err = errors.Errorf("url QueryUnescape error %v", err) 54 | return 55 | } 56 | 57 | buffer := bytes.NewBuffer(nil) 58 | buffer.WriteString(path) 59 | buffer.WriteString(delimiter) 60 | buffer.WriteString(methodName) 61 | buffer.WriteString(delimiter) 62 | buffer.WriteString(sortParamsEncode) 63 | buffer.WriteString(delimiter) 64 | buffer.WriteString(date) 65 | 66 | // 对数据进行 hmac 加密,并进行 base64 encode 67 | hash := hmac.New(sha256.New, []byte(s.secret)) 68 | hash.Write(buffer.Bytes()) 69 | digest := base64.StdEncoding.EncodeToString(hash.Sum(nil)) 70 | 71 | ok = authorization == fmt.Sprintf("%s %s", s.key, digest) 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /pkg/timeutil/timeutil.go: -------------------------------------------------------------------------------- 1 | package timeutil 2 | 3 | import ( 4 | "math" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | var ( 10 | cst *time.Location 11 | ) 12 | 13 | // CSTLayout China Standard Time Layout 14 | const CSTLayout = "2006-01-02 15:04:05" 15 | 16 | func init() { 17 | var err error 18 | if cst, err = time.LoadLocation("Asia/Shanghai"); err != nil { 19 | panic(err) 20 | } 21 | 22 | // 默认设置为中国时区 23 | time.Local = cst 24 | } 25 | 26 | // RFC3339ToCSTLayout convert rfc3339 value to china standard time layout 27 | // 2020-11-08T08:18:46+08:00 => 2020-11-08 08:18:46 28 | func RFC3339ToCSTLayout(value string) (string, error) { 29 | ts, err := time.Parse(time.RFC3339, value) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | return ts.In(cst).Format(CSTLayout), nil 35 | } 36 | 37 | // CSTLayoutString 格式化时间 38 | // 返回 "2006-01-02 15:04:05" 格式的时间 39 | func CSTLayoutString() string { 40 | ts := time.Now() 41 | return ts.In(cst).Format(CSTLayout) 42 | } 43 | 44 | // ParseCSTInLocation 格式化时间 45 | func ParseCSTInLocation(date string) (time.Time, error) { 46 | return time.ParseInLocation(CSTLayout, date, cst) 47 | } 48 | 49 | // CSTLayoutStringToUnix 返回 unix 时间戳 50 | // 2020-01-24 21:11:11 => 1579871471 51 | func CSTLayoutStringToUnix(cstLayoutString string) (int64, error) { 52 | stamp, err := time.ParseInLocation(CSTLayout, cstLayoutString, cst) 53 | if err != nil { 54 | return 0, err 55 | } 56 | return stamp.Unix(), nil 57 | } 58 | 59 | // GMTLayoutString 格式化时间 60 | // 返回 "Mon, 02 Jan 2006 15:04:05 GMT" 格式的时间 61 | func GMTLayoutString() string { 62 | return time.Now().In(cst).Format(http.TimeFormat) 63 | } 64 | 65 | // ParseGMTInLocation 格式化时间 66 | func ParseGMTInLocation(date string) (time.Time, error) { 67 | return time.ParseInLocation(http.TimeFormat, date, cst) 68 | } 69 | 70 | // SubInLocation 计算时间差 71 | func SubInLocation(ts time.Time) float64 { 72 | return math.Abs(time.Now().In(cst).Sub(ts).Seconds()) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/timeutil/timeutil_test.go: -------------------------------------------------------------------------------- 1 | package timeutil 2 | 3 | import "testing" 4 | 5 | func TestRFC3339ToCSTLayout(t *testing.T) { 6 | t.Log(RFC3339ToCSTLayout("2020-11-08T08:18:46+08:00")) 7 | } 8 | 9 | func TestCSTLayoutString(t *testing.T) { 10 | t.Log(CSTLayoutString()) 11 | } 12 | 13 | func TestCSTLayoutStringToUnix(t *testing.T) { 14 | t.Log(CSTLayoutStringToUnix("2020-01-24 21:11:11")) 15 | } 16 | 17 | func TestGMTLayoutString(t *testing.T) { 18 | t.Log(GMTLayoutString()) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/trace/debug.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | type Debug struct { 4 | Key string `json:"key"` // 标示 5 | Value interface{} `json:"value"` // 值 6 | CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/trace/dialog.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import "sync" 4 | 5 | var _ D = (*Dialog)(nil) 6 | 7 | type D interface { 8 | i() 9 | AppendResponse(resp *Response) 10 | } 11 | 12 | // Dialog 内部调用其它方接口的会话信息;失败时会有retry操作,所以 response 会有多次。 13 | type Dialog struct { 14 | mux sync.Mutex 15 | Request *Request `json:"request"` // 请求信息 16 | Responses []*Response `json:"responses"` // 返回信息 17 | Success bool `json:"success"` // 是否成功,true 或 false 18 | CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒) 19 | } 20 | 21 | func (d *Dialog) i() {} 22 | 23 | // AppendResponse 按转的追加response信息 24 | func (d *Dialog) AppendResponse(resp *Response) { 25 | if resp == nil { 26 | return 27 | } 28 | 29 | d.mux.Lock() 30 | d.Responses = append(d.Responses, resp) 31 | d.mux.Unlock() 32 | } 33 | -------------------------------------------------------------------------------- /pkg/trace/redis.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | type Redis struct { 4 | Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05 5 | Handle string `json:"handle"` // 操作,SET/GET 等 6 | Key string `json:"key"` // Key 7 | Value string `json:"value,omitempty"` // Value 8 | TTL float64 `json:"ttl,omitempty"` // 超时时长(单位分) 9 | CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/trace/sql.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | type SQL struct { 4 | Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05 5 | Stack string `json:"stack"` // 文件地址和行号 6 | SQL string `json:"sql"` // SQL 语句 7 | Rows int64 `json:"rows_affected"` // 影响行数 8 | CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒) 9 | } 10 | -------------------------------------------------------------------------------- /scripts/gormgen.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 3 | echo. 4 | echo Regenerating file 5 | echo. 6 | go run -v .\cmd\mysqlmd\main.go -addr %1 -user %2 -pass %3 -name %4 -tables %5 7 | if %errorlevel% == 1 ( 8 | echo. 9 | echo failed!!! 10 | exit 1 11 | ) 12 | echo. 13 | echo create curd code : 14 | echo. 15 | go build -o gormgen .\cmd\gormgen\main.go 16 | if %errorlevel% == 1 ( 17 | echo. 18 | echo failed!!! 19 | exit 1 20 | ) 21 | 22 | if exist %GOPATH%\bin ( 23 | move gormgen %GOPATH%\bin\gormgen.exe 24 | ) else ( 25 | md %GOPATH%\bin 26 | move gormgen %GOPATH%\bin\gormgen.exe 27 | ) 28 | if %errorlevel% == 1 ( 29 | echo. 30 | echo failed!!! 31 | exit 1 32 | ) 33 | 34 | go generate .\... 35 | if %errorlevel% == 1 ( 36 | echo. 37 | echo failed!!! 38 | exit 1 39 | ) 40 | echo. 41 | echo Formatting code 42 | echo. 43 | go run -v .\cmd\mfmt\main.go 44 | if %errorlevel% == 1 ( 45 | echo. 46 | echo failed!!! 47 | exit 1 48 | ) 49 | echo. 50 | echo Done. 51 | -------------------------------------------------------------------------------- /scripts/gormgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shellExit() 4 | { 5 | if [ $1 -eq 1 ]; then 6 | printf "\nfailed!!!\n\n" 7 | exit 1 8 | fi 9 | } 10 | 11 | printf "\nRegenerating file\n\n" 12 | time go run -v ./cmd/mysqlmd/main.go -addr $1 -user $2 -pass $3 -name $4 -tables $5 13 | shellExit $? 14 | 15 | printf "\ncreate curd code : \n" 16 | time go build -o gormgen ./cmd/gormgen/main.go 17 | shellExit $? 18 | 19 | if [ ! -d $GOPATH/bin ];then 20 | mkdir -p $GOPATH/bin 21 | fi 22 | 23 | mv gormgen $GOPATH/bin 24 | shellExit $? 25 | 26 | go generate ./... 27 | shellExit $? 28 | 29 | printf "\nFormatting code\n\n" 30 | time go run -v ./cmd/mfmt/main.go 31 | shellExit $? 32 | 33 | printf "\nDone.\n\n" 34 | -------------------------------------------------------------------------------- /scripts/gqlgen.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 3 | echo. 4 | echo Regenerating gqlgen file 5 | echo. 6 | del internal\graph\generated\generated.go internal\graph\model\generated.go internal\graph\resolvers\generated\generated.go 7 | go get github.com/99designs/gqlgen 8 | gqlgen 9 | echo. 10 | echo Done. 11 | echo. -------------------------------------------------------------------------------- /scripts/gqlgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | printf "\nRegenerating gqlgen files\n" 3 | rm -f internal/graph/generated/generated.go \ 4 | internal/graph/model/generated.go \ 5 | internal/graph/resolvers/generated/generated.go 6 | go get github.com/99designs/gqlgen 7 | time gqlgen 8 | printf "\nDone.\n\n" -------------------------------------------------------------------------------- /scripts/handlergen.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 3 | echo. 4 | echo Regenerating handler file 5 | echo. 6 | go run -v .\cmd\handlergen\main.go -handler %1 7 | if %errorlevel% == 1 ( 8 | echo. 9 | echo failed!!! 10 | exit 1 11 | ) 12 | echo. 13 | echo Formatting code 14 | echo. 15 | go run -v .\cmd\mfmt\main.go 16 | if %errorlevel% == 1 ( 17 | echo. 18 | echo failed!!! 19 | exit 1 20 | ) 21 | echo. 22 | echo Done. 23 | -------------------------------------------------------------------------------- /scripts/handlergen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shellExit() 4 | { 5 | if [ $1 -eq 1 ]; then 6 | printf "\nfailed!!!\n\n" 7 | exit 1 8 | fi 9 | } 10 | 11 | printf "\nRegenerating handler file\n\n" 12 | time go run -v ./cmd/handlergen/main.go -handler $1 13 | shellExit $? 14 | 15 | printf "\nFormatting code\n\n" 16 | time go run -v ./cmd/mfmt/main.go 17 | shellExit $? 18 | 19 | printf "\nDone.\n\n" 20 | -------------------------------------------------------------------------------- /scripts/swagger.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 3 | echo. 4 | echo Regenerating swagger doc 5 | echo. 6 | go install github.com/swaggo/swag/cmd/swag@v1.7.4 7 | swag init 8 | echo. 9 | echo Done. -------------------------------------------------------------------------------- /scripts/swagger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | printf "\nRegenerating swagger doc\n\n" 3 | go install github.com/swaggo/swag/cmd/swag@v1.7.4 4 | time swag init 5 | printf "\nDone.\n\n" --------------------------------------------------------------------------------