├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── README.md ├── admin.sh ├── conf ├── config.yaml ├── server.crt └── server.key ├── config └── config.go ├── db.sql ├── docs ├── docs.go └── swagger │ ├── swagger.json │ └── swagger.yaml ├── handler ├── handler.go ├── sd │ └── check.go └── user │ ├── create.go │ ├── delete.go │ ├── get.go │ ├── list.go │ ├── login.go │ ├── update.go │ └── user.go ├── main.go ├── model ├── init.go ├── model.go └── user.go ├── pkg ├── auth │ └── auth.go ├── constvar │ └── constvar.go ├── errno │ ├── code.go │ └── errno.go ├── token │ └── token.go └── version │ ├── base.go │ ├── doc.go │ └── version.go ├── router ├── middleware │ ├── auth.go │ ├── header.go │ ├── logging.go │ └── requestid.go └── router.go ├── service └── service.go ├── util ├── util.go └── util_test.go └── wrktest.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | /node_modules 4 | /vendor -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/PuerkitoBio/purell" 6 | packages = ["."] 7 | revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4" 8 | version = "v1.1.0" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/PuerkitoBio/urlesc" 13 | packages = ["."] 14 | revision = "de5bf2ad457846296e2031421a34e2568e304e35" 15 | 16 | [[projects]] 17 | name = "github.com/StackExchange/wmi" 18 | packages = ["."] 19 | revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338" 20 | version = "1.0.0" 21 | 22 | [[projects]] 23 | name = "github.com/dgrijalva/jwt-go" 24 | packages = ["."] 25 | revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" 26 | version = "v3.2.0" 27 | 28 | [[projects]] 29 | name = "github.com/fsnotify/fsnotify" 30 | packages = ["."] 31 | revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" 32 | version = "v1.4.7" 33 | 34 | [[projects]] 35 | name = "github.com/gin-contrib/pprof" 36 | packages = ["."] 37 | revision = "cea200e02ddddca4b843d75d32aa13c413549de9" 38 | version = "v1.1" 39 | 40 | [[projects]] 41 | branch = "master" 42 | name = "github.com/gin-contrib/sse" 43 | packages = ["."] 44 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae" 45 | 46 | [[projects]] 47 | name = "github.com/gin-gonic/gin" 48 | packages = [ 49 | ".", 50 | "binding", 51 | "json", 52 | "render" 53 | ] 54 | revision = "b869fe1415e4b9eb52f247441830d502aece2d4d" 55 | version = "v1.3.0" 56 | 57 | [[projects]] 58 | name = "github.com/go-ole/go-ole" 59 | packages = [ 60 | ".", 61 | "oleutil" 62 | ] 63 | revision = "a41e3c4b706f6ae8dfbff342b06e40fa4d2d0506" 64 | version = "v1.2.1" 65 | 66 | [[projects]] 67 | name = "github.com/go-openapi/jsonpointer" 68 | packages = ["."] 69 | revision = "3a0015ad55fa9873f41605d3e8f28cd279c32ab2" 70 | version = "0.16.0" 71 | 72 | [[projects]] 73 | name = "github.com/go-openapi/jsonreference" 74 | packages = ["."] 75 | revision = "3fb327e6747da3043567ee86abd02bb6376b6be2" 76 | version = "0.16.0" 77 | 78 | [[projects]] 79 | name = "github.com/go-openapi/spec" 80 | packages = ["."] 81 | revision = "384415f06ee238aae1df5caad877de6ceac3a5c4" 82 | version = "0.16.0" 83 | 84 | [[projects]] 85 | name = "github.com/go-openapi/swag" 86 | packages = ["."] 87 | revision = "becd2f08beafcca035645a8a101e0e3e18140458" 88 | version = "0.16.0" 89 | 90 | [[projects]] 91 | name = "github.com/go-playground/locales" 92 | packages = [ 93 | ".", 94 | "currency" 95 | ] 96 | revision = "f63010822830b6fe52288ee52d5a1151088ce039" 97 | version = "v0.12.1" 98 | 99 | [[projects]] 100 | name = "github.com/go-playground/universal-translator" 101 | packages = ["."] 102 | revision = "b32fa301c9fe55953584134cb6853a13c87ec0a1" 103 | version = "v0.16.0" 104 | 105 | [[projects]] 106 | name = "github.com/go-sql-driver/mysql" 107 | packages = ["."] 108 | revision = "d523deb1b23d913de5bdada721a6071e71283618" 109 | version = "v1.4.0" 110 | 111 | [[projects]] 112 | name = "github.com/golang/protobuf" 113 | packages = ["proto"] 114 | revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" 115 | version = "v1.2.0" 116 | 117 | [[projects]] 118 | branch = "master" 119 | name = "github.com/hashicorp/hcl" 120 | packages = [ 121 | ".", 122 | "hcl/ast", 123 | "hcl/parser", 124 | "hcl/printer", 125 | "hcl/scanner", 126 | "hcl/strconv", 127 | "hcl/token", 128 | "json/parser", 129 | "json/scanner", 130 | "json/token" 131 | ] 132 | revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" 133 | 134 | [[projects]] 135 | name = "github.com/jinzhu/gorm" 136 | packages = [ 137 | ".", 138 | "dialects/mysql" 139 | ] 140 | revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166" 141 | version = "v1.9.1" 142 | 143 | [[projects]] 144 | branch = "master" 145 | name = "github.com/jinzhu/inflection" 146 | packages = ["."] 147 | revision = "04140366298a54a039076d798123ffa108fff46c" 148 | 149 | [[projects]] 150 | name = "github.com/json-iterator/go" 151 | packages = ["."] 152 | revision = "1624edc4454b8682399def8740d46db5e4362ba4" 153 | version = "v1.1.5" 154 | 155 | [[projects]] 156 | branch = "master" 157 | name = "github.com/lexkong/log" 158 | packages = [ 159 | ".", 160 | "lager", 161 | "lager/color" 162 | ] 163 | revision = "972f9cd951fc7e5c8797f1fb2b57b63b09ff4de9" 164 | 165 | [[projects]] 166 | name = "github.com/magiconair/properties" 167 | packages = ["."] 168 | revision = "c2353362d570a7bfa228149c62842019201cfb71" 169 | version = "v1.8.0" 170 | 171 | [[projects]] 172 | branch = "master" 173 | name = "github.com/mailru/easyjson" 174 | packages = [ 175 | "buffer", 176 | "jlexer", 177 | "jwriter" 178 | ] 179 | revision = "60711f1a8329503b04e1c88535f419d0bb440bff" 180 | 181 | [[projects]] 182 | name = "github.com/mattn/go-isatty" 183 | packages = ["."] 184 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 185 | version = "v0.0.3" 186 | 187 | [[projects]] 188 | name = "github.com/mitchellh/mapstructure" 189 | packages = ["."] 190 | revision = "fa473d140ef3c6adf42d6b391fe76707f1f243c8" 191 | version = "v1.0.0" 192 | 193 | [[projects]] 194 | name = "github.com/modern-go/concurrent" 195 | packages = ["."] 196 | revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" 197 | version = "1.0.3" 198 | 199 | [[projects]] 200 | name = "github.com/modern-go/reflect2" 201 | packages = ["."] 202 | revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" 203 | version = "1.0.1" 204 | 205 | [[projects]] 206 | name = "github.com/pelletier/go-toml" 207 | packages = ["."] 208 | revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" 209 | version = "v1.2.0" 210 | 211 | [[projects]] 212 | name = "github.com/satori/go.uuid" 213 | packages = ["."] 214 | revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3" 215 | version = "v1.2.0" 216 | 217 | [[projects]] 218 | name = "github.com/shirou/gopsutil" 219 | packages = [ 220 | "cpu", 221 | "disk", 222 | "internal/common", 223 | "load", 224 | "mem" 225 | ] 226 | revision = "8048a2e9c5773235122027dd585cf821b2af1249" 227 | version = "v2.18.07" 228 | 229 | [[projects]] 230 | name = "github.com/spf13/afero" 231 | packages = [ 232 | ".", 233 | "mem" 234 | ] 235 | revision = "787d034dfe70e44075ccc060d346146ef53270ad" 236 | version = "v1.1.1" 237 | 238 | [[projects]] 239 | name = "github.com/spf13/cast" 240 | packages = ["."] 241 | revision = "8965335b8c7107321228e3e3702cab9832751bac" 242 | version = "v1.2.0" 243 | 244 | [[projects]] 245 | branch = "master" 246 | name = "github.com/spf13/jwalterweatherman" 247 | packages = ["."] 248 | revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2" 249 | 250 | [[projects]] 251 | name = "github.com/spf13/pflag" 252 | packages = ["."] 253 | revision = "9a97c102cda95a86cec2345a6f09f55a939babf5" 254 | version = "v1.0.2" 255 | 256 | [[projects]] 257 | name = "github.com/spf13/viper" 258 | packages = ["."] 259 | revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9" 260 | version = "v1.1.0" 261 | 262 | [[projects]] 263 | name = "github.com/swaggo/gin-swagger" 264 | packages = [ 265 | ".", 266 | "swaggerFiles" 267 | ] 268 | revision = "8cf3fa9932e247205d1cf2be6fd13a9d09ceb9a4" 269 | version = "v1.0.0" 270 | 271 | [[projects]] 272 | name = "github.com/swaggo/swag" 273 | packages = ["."] 274 | revision = "db2ee6c14a35e4fca95b0d75054296ef2b699050" 275 | version = "v1.3.2" 276 | 277 | [[projects]] 278 | name = "github.com/teris-io/shortid" 279 | packages = ["."] 280 | revision = "6c56cef5189ca1b3d5ef01dc07f4d611dfc0bb33" 281 | version = "v1.0" 282 | 283 | [[projects]] 284 | name = "github.com/ugorji/go" 285 | packages = ["codec"] 286 | revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab" 287 | version = "v1.1.1" 288 | 289 | [[projects]] 290 | branch = "master" 291 | name = "github.com/willf/pad" 292 | packages = ["."] 293 | revision = "b3d7806010228b1a954b4d9c4ee4cf6cb476b063" 294 | 295 | [[projects]] 296 | branch = "master" 297 | name = "golang.org/x/crypto" 298 | packages = [ 299 | "bcrypt", 300 | "blowfish" 301 | ] 302 | revision = "614d502a4dac94afa3a6ce146bd1736da82514c6" 303 | 304 | [[projects]] 305 | branch = "master" 306 | name = "golang.org/x/net" 307 | packages = [ 308 | "context", 309 | "idna", 310 | "webdav", 311 | "webdav/internal/xml" 312 | ] 313 | revision = "4bcd98cce591d8c7061bf313d7a3cbad05b58549" 314 | 315 | [[projects]] 316 | branch = "master" 317 | name = "golang.org/x/sys" 318 | packages = [ 319 | "unix", 320 | "windows" 321 | ] 322 | revision = "4910a1d54f876d7b22162a85f4d066d3ee649450" 323 | 324 | [[projects]] 325 | name = "golang.org/x/text" 326 | packages = [ 327 | "collate", 328 | "collate/build", 329 | "internal/colltab", 330 | "internal/gen", 331 | "internal/tag", 332 | "internal/triegen", 333 | "internal/ucd", 334 | "language", 335 | "secure/bidirule", 336 | "transform", 337 | "unicode/bidi", 338 | "unicode/cldr", 339 | "unicode/norm", 340 | "unicode/rangetable", 341 | "width" 342 | ] 343 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 344 | version = "v0.3.0" 345 | 346 | [[projects]] 347 | name = "google.golang.org/appengine" 348 | packages = ["cloudsql"] 349 | revision = "b1f26356af11148e710935ed1ac8a7f5702c7612" 350 | version = "v1.1.0" 351 | 352 | [[projects]] 353 | name = "gopkg.in/go-playground/validator.v8" 354 | packages = ["."] 355 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" 356 | version = "v8.18.2" 357 | 358 | [[projects]] 359 | name = "gopkg.in/go-playground/validator.v9" 360 | packages = ["."] 361 | revision = "e69e9a28bb62b977fdc58d051f1bb477b7cbe486" 362 | version = "v9.21.0" 363 | 364 | [[projects]] 365 | name = "gopkg.in/yaml.v2" 366 | packages = ["."] 367 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 368 | version = "v2.2.1" 369 | 370 | [solve-meta] 371 | analyzer-name = "dep" 372 | analyzer-version = 1 373 | inputs-digest = "a3f42e11bfff15b736ad14ddbc73d4e5283458476e74ab21bd70f738def1ec29" 374 | solver-name = "gps-cdcl" 375 | solver-version = 1 376 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/dgrijalva/jwt-go" 30 | version = "3.2.0" 31 | 32 | [[constraint]] 33 | name = "github.com/fsnotify/fsnotify" 34 | version = "1.4.7" 35 | 36 | [[constraint]] 37 | name = "github.com/gin-contrib/pprof" 38 | version = "1.1.0" 39 | 40 | [[constraint]] 41 | name = "github.com/gin-gonic/gin" 42 | version = "1.3.0" 43 | 44 | [[constraint]] 45 | name = "github.com/jinzhu/gorm" 46 | version = "1.9.1" 47 | 48 | [[constraint]] 49 | branch = "master" 50 | name = "github.com/lexkong/log" 51 | 52 | [[constraint]] 53 | name = "github.com/satori/go.uuid" 54 | version = "1.2.0" 55 | 56 | [[constraint]] 57 | name = "github.com/shirou/gopsutil" 58 | version = "2.18.7" 59 | 60 | [[constraint]] 61 | name = "github.com/spf13/pflag" 62 | version = "1.0.2" 63 | 64 | [[constraint]] 65 | name = "github.com/spf13/viper" 66 | version = "1.1.0" 67 | 68 | [[constraint]] 69 | name = "github.com/swaggo/gin-swagger" 70 | version = "1.0.0" 71 | 72 | [[constraint]] 73 | name = "github.com/swaggo/swag" 74 | version = "1.3.2" 75 | 76 | [[constraint]] 77 | name = "github.com/teris-io/shortid" 78 | version = "1.0.0" 79 | 80 | [[constraint]] 81 | branch = "master" 82 | name = "github.com/willf/pad" 83 | 84 | [[constraint]] 85 | branch = "master" 86 | name = "golang.org/x/crypto" 87 | 88 | [[constraint]] 89 | name = "gopkg.in/go-playground/validator.v9" 90 | version = "9.21.0" 91 | 92 | [prune] 93 | go-tests = true 94 | unused-packages = true 95 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | BASEDIR = $(shell pwd) 3 | 4 | # build with verison infos 5 | versionDir = "tapi-blog/pkg/version" 6 | gitTag = $(shell if [ "`git describe --tags --abbrev=0 2>/dev/null`" != "" ];then git describe --tags --abbrev=0; else git log --pretty=format:'%h' -n 1; fi) 7 | buildDate = $(shell TZ=Asia/Shanghai date +%FT%T%z) 8 | gitCommit = $(shell git log --pretty=format:'%H' -n 1) 9 | gitTreeState = $(shell if git status|grep -q 'clean';then echo clean; else echo dirty; fi) 10 | 11 | ldflags="-w -X ${versionDir}.gitTag=${gitTag} -X ${versionDir}.buildDate=${buildDate} -X ${versionDir}.gitCommit=${gitCommit} -X ${versionDir}.gitTreeState=${gitTreeState}" 12 | 13 | all: gotool 14 | @go build -v -ldflags ${ldflags} . 15 | clean: 16 | rm -f tapi-blog 17 | find . -name "[._]*.s[a-w][a-z]" | xargs -i rm -f {} 18 | gotool: 19 | gofmt -w . 20 | go tool vet . |& grep -v vendor;true 21 | ca: 22 | openssl req -new -nodes -x509 -out conf/server.crt -keyout conf/server.key -days 3650 -subj "/C=DE/ST=NRW/L=Earth/O=Random Company/OU=IT/CN=127.0.0.1/emailAddress=xxxxx@qq.com" 23 | 24 | help: 25 | @echo "make - compile the source code" 26 | @echo "make clean - remove binary file and vim swp files" 27 | @echo "make gotool - run go tool 'fmt' and 'vet'" 28 | @echo "make ca - generate ca files" 29 | 30 | .PHONY: clean gotool ca help 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## go-apiservice 2 | -------------------------------------------------------------------------------- /admin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SERVER="tapi-blog" 4 | BASE_DIR=$PWD 5 | INTERVAL=2 6 | 7 | # 命令行参数,需要手动指定 8 | ARGS="" 9 | 10 | function start() 11 | { 12 | if [ "`pgrep $SERVER -u $UID`" != "" ];then 13 | echo "$SERVER already running" 14 | exit 1 15 | fi 16 | 17 | nohup $BASE_DIR/$SERVER $ARGS server &>/dev/null & 18 | 19 | echo "sleeping..." && sleep $INTERVAL 20 | 21 | # check status 22 | if [ "`pgrep $SERVER -u $UID`" == "" ];then 23 | echo "$SERVER start failed" 24 | exit 1 25 | fi 26 | } 27 | 28 | function status() 29 | { 30 | if [ "`pgrep $SERVER -u $UID`" != "" ];then 31 | echo $SERVER is running 32 | else 33 | echo $SERVER is not running 34 | fi 35 | } 36 | 37 | function stop() 38 | { 39 | if [ "`pgrep $SERVER -u $UID`" != "" ];then 40 | kill -9 `pgrep $SERVER -u $UID` 41 | fi 42 | 43 | echo "sleeping..." && sleep $INTERVAL 44 | 45 | if [ "`pgrep $SERVER -u $UID`" != "" ];then 46 | echo "$SERVER stop failed" 47 | exit 1 48 | fi 49 | } 50 | 51 | case "$1" in 52 | 'start') 53 | start 54 | ;; 55 | 'stop') 56 | stop 57 | ;; 58 | 'status') 59 | status 60 | ;; 61 | 'restart') 62 | stop && start 63 | ;; 64 | *) 65 | echo "usage: $0 {start|stop|restart|status}" 66 | exit 1 67 | ;; 68 | esac 69 | -------------------------------------------------------------------------------- /conf/config.yaml: -------------------------------------------------------------------------------- 1 | runmode: debug # 开发模式, debug, release, test 2 | addr: :8080 # HTTP绑定端口 3 | name: tapi-blog # API Server的名字 4 | url: http://127.0.0.1:8080 # pingServer函数请求的API服务器的ip:port 5 | max_ping_count: 10 # pingServer函数try的次数 6 | jwt_secret: Rtg8BPKNEf2mB4mgvKONGPZZQSaJWNLijxR42qRgq0iBb5 7 | tls: 8 | addr: :8081 9 | cert: conf/server.crt 10 | key: conf/server.key 11 | log: 12 | writers: stdout 13 | logger_level: DEBUG 14 | logger_file: log/tapi-blog.log 15 | log_format_text: true 16 | rollingPolicy: size 17 | log_rotate_date: 1 18 | log_rotate_size: 1 19 | log_backup_count: 7 20 | db: 21 | name: db_tapi-blog 22 | addr: 127.0.0.1:3306 23 | username: root 24 | password: root 25 | docker_db: 26 | name: db_tapi-blog 27 | addr: 127.0.0.1:3306 28 | username: root 29 | password: root 30 | -------------------------------------------------------------------------------- /conf/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID2TCCAsGgAwIBAgIJAI/8axTNy37vMA0GCSqGSIb3DQEBBQUAMIGCMQswCQYD 3 | VQQGEwJERTEMMAoGA1UECAwDTlJXMQ4wDAYDVQQHDAVFYXJ0aDEXMBUGA1UECgwO 4 | UmFuZG9tIENvbXBhbnkxCzAJBgNVBAsMAklUMRIwEAYDVQQDDAkxMjcuMC4wLjEx 5 | GzAZBgkqhkiG9w0BCQEWDHh4eHh4QHFxLmNvbTAeFw0xODA2MDQwNjMwMzFaFw0y 6 | ODA2MDEwNjMwMzFaMIGCMQswCQYDVQQGEwJERTEMMAoGA1UECAwDTlJXMQ4wDAYD 7 | VQQHDAVFYXJ0aDEXMBUGA1UECgwOUmFuZG9tIENvbXBhbnkxCzAJBgNVBAsMAklU 8 | MRIwEAYDVQQDDAkxMjcuMC4wLjExGzAZBgkqhkiG9w0BCQEWDHh4eHh4QHFxLmNv 9 | bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKHbnpmaxHokFC8rnJlG 10 | 42eL4C3tog7rffv96DrwDajdFNk8DeaNUpZ+R4X4YPecumsCsbol2dDC1IHHGJUN 11 | q/A1vRjyDptMLmzp30p1aqVPcz+j8GMXtZUflLgup+HeKZRdrsk/0SaH1C3ywUBm 12 | yDMNHUxEvoFj0x9aJoPutnkEk9miZt/kF2vxlTbKFM14/TecVr3ljNwTebJAqEw9 13 | iaZpNXn/IQV2I2PTGlxjyT6XNsCchjMTJ9y/XS9rBeX6BFHgQsV52CW1mWz8FieM 14 | /nyF9hE/TZKCpMtHWEYIdBbrxxY0BlhMxEtyIFzTha5zltFrQJGUYz7RIlyuabjk 15 | or8CAwEAAaNQME4wHQYDVR0OBBYEFIjLBQlToqUDoyOLeUd6HeaqiiLJMB8GA1Ud 16 | IwQYMBaAFIjLBQlToqUDoyOLeUd6HeaqiiLJMAwGA1UdEwQFMAMBAf8wDQYJKoZI 17 | hvcNAQEFBQADggEBAHAqpqGNUNgYCr3JvrVULDqFKmqLpvDKfLd6kmtnFzvuZLSz 18 | ANE2RXXqQ5Ms3F0oHqZQq4aOvgvfas5dBLoToPL/ip+GibQ7O/1PsPD5bZaczUel 19 | AnmClX4ewcOEWmZx9mbYOz2C8RREvKU3MoBERrWA33LquYH0+8W8WHyMKNNF97oD 20 | BgWZ8R1t3N59ySZyHcye7ddlJUubRbdyCGrHbmziHV3Cj61NJiwS2JRvhYkc71M6 21 | 8921j63qEM6i0NusFYG/w0GM3x/OeaeY/om6x+IOhJp3vIa1JPSekVyNSCwDEc3B 22 | +8WmVFaeMMXwErSkuu8nCbqRP8J1jiKgzpG/92g= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /conf/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCh256ZmsR6JBQv 3 | K5yZRuNni+At7aIO6337/eg68A2o3RTZPA3mjVKWfkeF+GD3nLprArG6JdnQwtSB 4 | xxiVDavwNb0Y8g6bTC5s6d9KdWqlT3M/o/BjF7WVH5S4Lqfh3imUXa7JP9Emh9Qt 5 | 8sFAZsgzDR1MRL6BY9MfWiaD7rZ5BJPZombf5Bdr8ZU2yhTNeP03nFa95YzcE3my 6 | QKhMPYmmaTV5/yEFdiNj0xpcY8k+lzbAnIYzEyfcv10vawXl+gRR4ELFedgltZls 7 | /BYnjP58hfYRP02SgqTLR1hGCHQW68cWNAZYTMRLciBc04Wuc5bRa0CRlGM+0SJc 8 | rmm45KK/AgMBAAECggEASNMWvgf7pPT8u+iEchaKFLnDqQaFZu8f5TRtu67shnDK 9 | g59YpcYqRZoVtjp17pLu8Vzp+FY1dY9jq+yXq+DV3qNfLI0kc01IiiqEE+1WiYCA 10 | 2z541y0Av1LRSDl9wcuCq8Wm8der1AlDN1VFDCPyqb2Z1AoOKQtwH2ghcjUCltn4 11 | NxuMYOVdql+Idl/7PCEha9pW3DQvPqJVXRPvBh6tB7FFuUOIq4+EFRUWWZlyCJfQ 12 | fYxB9jcFUWmAUieV3iBajg6H6QVkj/STLSgKuSktEBM2mqyL4slHXFWRt7+NQikg 13 | tOEWnqcTC4Ei2Dyam4SBXHJ1wT8AJV4kMTrN1ZBRmQKBgQDOMcIGHyuBI1aHcvPW 14 | Tkein7bslFAKsA1LBqnw+alWzSHMKEmfWbBjeW0FQTIGEYMXwIq5JhnNcS5Kb5Qt 15 | BZgbBPDb7S80DtJnkno3iJ6hpijE3X/29lezigIuGibO5MD0Na1am2BP0iU77ifV 16 | xpYvj/j/pkdcTLM7hRDImSeNIwKBgQDI9EUFkGi6H9NZEles+MCHenVRYUBT2PXe 17 | hmtd9d5f4v3CmRsXvl4Dl/slIrJ9kuZktAck3Ocx6D0hnZcj1mwq077K7H3QqKCl 18 | zRwB11H5v/imsZ1ey0hLVPhCzr2miRPM/9oL5B2st9eotzhyRcrMn+oYZSSt79S8 19 | Q3YDiZ7TtQKBgENSr7z79GJnvVrgR4kTagRJDZrVGgVDUjPK6zXI7mdu9rgH93HW 20 | AOeZv+TVUpX0pc7diO3G6OnRKIIZSFIi33UC+fl0ydK/fCdhBhKXwuOYsvsEL0Hd 21 | UOlICEoxM7adrfqOhBlvXdTyEkItEkiUXHkPEwe1rNsQF/05By/YAbftAoGAYB2F 22 | jd2+WZezTN0bFl58J9CIoH31eKVDJEYCwJRC4nX9jcARV0/0Q5/DvcVUvf8vN2ds 23 | K1OFOTetVZC8o6WBYxKYJRLsMosVG3h5NuA4E06grYoyjQ6J644emEWuLCNQVzLg 24 | peNb1iqweb/4vZ9oGms6WqS14IPfqpRRs+t1DikCgYEAyFtQKlLmNm/EirOMqjS2 25 | 4/zMRj4B6n8BB8ZJsVC/MkuD+4B03rY44pP324cJnqVIcug9Zoijffnh1qbtZEc/ 26 | MhNCa4a2MY0qcd4r/43cGbjqv94En7e5TgkH/79W+lbR3uBayt33RuuKStauZkf/ 27 | 2eBi4LlGO5stoeD63h9Ten0= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/fsnotify/fsnotify" 7 | "github.com/lexkong/log" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | type Config struct { 12 | Name string 13 | } 14 | 15 | func Init(cfg string) error { 16 | c := Config{ 17 | Name: cfg, 18 | } 19 | 20 | // 初始化配置文件 21 | if err := c.initConfig(); err != nil { 22 | return err 23 | } 24 | 25 | // 初始化日志包 26 | c.initLog() 27 | 28 | // 监控配置文件变化并热加载程序 29 | c.watchConfig() 30 | 31 | return nil 32 | } 33 | 34 | func (c *Config) initConfig() error { 35 | if c.Name != "" { 36 | viper.SetConfigFile(c.Name) // 如果指定了配置文件,则解析指定的配置文件 37 | } else { 38 | viper.AddConfigPath("conf") // 如果没有指定配置文件,则解析默认的配置文件 39 | viper.SetConfigName("config") 40 | } 41 | viper.SetConfigType("yaml") // 设置配置文件格式为YAML 42 | viper.AutomaticEnv() // 读取匹配的环境变量 43 | viper.SetEnvPrefix("tapi-blog") // 读取环境变量的前缀为tapi-blog 44 | replacer := strings.NewReplacer(".", "_") 45 | viper.SetEnvKeyReplacer(replacer) 46 | if err := viper.ReadInConfig(); err != nil { // viper解析配置文件 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (c *Config) initLog() { 54 | passLagerCfg := log.PassLagerCfg{ 55 | Writers: viper.GetString("log.writers"), 56 | LoggerLevel: viper.GetString("log.logger_level"), 57 | LoggerFile: viper.GetString("log.logger_file"), 58 | LogFormatText: viper.GetBool("log.log_format_text"), 59 | RollingPolicy: viper.GetString("log.rollingPolicy"), 60 | LogRotateDate: viper.GetInt("log.log_rotate_date"), 61 | LogRotateSize: viper.GetInt("log.log_rotate_size"), 62 | LogBackupCount: viper.GetInt("log.log_backup_count"), 63 | } 64 | 65 | log.InitWithConfig(&passLagerCfg) 66 | } 67 | 68 | // 监控配置文件变化并热加载程序 69 | func (c *Config) watchConfig() { 70 | viper.WatchConfig() 71 | viper.OnConfigChange(func(e fsnotify.Event) { 72 | log.Infof("Config file changed: %s", e.Name) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /db.sql: -------------------------------------------------------------------------------- 1 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 2 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 3 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 4 | /*!40101 SET NAMES utf8 */; 5 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 6 | /*!40103 SET TIME_ZONE='+00:00' */; 7 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 8 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 9 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 10 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 11 | 12 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `db_tapi-blog` /*!40100 DEFAULT CHARACTER SET utf8 */; 13 | 14 | USE `db_tapi-blog`; 15 | 16 | -- 17 | -- Table structure for table `tb_users` 18 | -- 19 | 20 | DROP TABLE IF EXISTS `tb_users`; 21 | /*!40101 SET @saved_cs_client = @@character_set_client */; 22 | /*!40101 SET character_set_client = utf8 */; 23 | CREATE TABLE `tb_users` ( 24 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 25 | `username` varchar(255) NOT NULL, 26 | `password` varchar(255) NOT NULL, 27 | `createdAt` timestamp NULL DEFAULT NULL, 28 | `updatedAt` timestamp NULL DEFAULT NULL, 29 | `deletedAt` timestamp NULL DEFAULT NULL, 30 | PRIMARY KEY (`id`), 31 | UNIQUE KEY `username` (`username`), 32 | KEY `idx_tb_users_deletedAt` (`deletedAt`) 33 | ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 34 | /*!40101 SET character_set_client = @saved_cs_client */; 35 | 36 | -- 37 | -- Dumping data for table `tb_users` 38 | -- 39 | 40 | LOCK TABLES `tb_users` WRITE; 41 | /*!40000 ALTER TABLE `tb_users` DISABLE KEYS */; 42 | INSERT INTO `tb_users` VALUES (0,'admin','$2a$10$veGcArz47VGj7l9xN7g2iuT9TF21jLI1YGXarGzvARNdnt4inC9PG','2018-05-27 16:25:33','2018-05-27 16:25:33',NULL); 43 | /*!40000 ALTER TABLE `tb_users` ENABLE KEYS */; 44 | UNLOCK TABLES; 45 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 46 | 47 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 48 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 49 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 50 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 51 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 52 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 53 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 54 | 55 | -- Dump completed on 2018-05-28 0:25:41 56 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 2 | // This file was generated by swaggo/swag at 3 | // 2018-06-18 02:15:43.723659228 +0800 CST m=+0.040196270 4 | 5 | package docs 6 | 7 | import ( 8 | "github.com/swaggo/swag" 9 | ) 10 | 11 | var doc = `{ 12 | "swagger": "2.0", 13 | "info": { 14 | "description": "tapi-blog demo", 15 | "title": "tapi-blog Example API", 16 | "contact": { 17 | "name": "lkong", 18 | "url": "http://www.swagger.io/support", 19 | "email": "466701708@qq.com" 20 | }, 21 | "license": {}, 22 | "version": "1.0" 23 | }, 24 | "host": "localhost:8080", 25 | "basePath": "/v1", 26 | "paths": { 27 | "/login": { 28 | "post": { 29 | "produces": [ 30 | "application/json" 31 | ], 32 | "summary": "Login generates the authentication token", 33 | "parameters": [ 34 | { 35 | "description": "Username", 36 | "name": "username", 37 | "in": "body", 38 | "required": true, 39 | "schema": { 40 | "type": "object" 41 | } 42 | }, 43 | { 44 | "description": "Password", 45 | "name": "password", 46 | "in": "body", 47 | "required": true, 48 | "schema": { 49 | "type": "object" 50 | } 51 | } 52 | ], 53 | "responses": { 54 | "200": { 55 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":{\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjgwMTY5MjIsImlkIjowLCJuYmYiOjE1MjgwMTY5MjIsInVzZXJuYW1lIjoiYWRtaW4ifQ.LjxrK9DuAwAzUD8-9v43NzWBN7HXsSLfebw92DKd1JQ\"}}", 56 | "schema": { 57 | "type": "string" 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | "/sd/cpu": { 64 | "get": { 65 | "description": "Checks the cpu usage", 66 | "consumes": [ 67 | "application/json" 68 | ], 69 | "produces": [ 70 | "application/json" 71 | ], 72 | "tags": [ 73 | "sd" 74 | ], 75 | "summary": "Checks the cpu usage", 76 | "responses": { 77 | "200": { 78 | "description": "CRITICAL - Load average: 1.78, 1.99, 2.02 | Cores: 2", 79 | "schema": { 80 | "type": "string" 81 | } 82 | } 83 | } 84 | } 85 | }, 86 | "/sd/disk": { 87 | "get": { 88 | "description": "Checks the disk usage", 89 | "consumes": [ 90 | "application/json" 91 | ], 92 | "produces": [ 93 | "application/json" 94 | ], 95 | "tags": [ 96 | "sd" 97 | ], 98 | "summary": "Checks the disk usage", 99 | "responses": { 100 | "200": { 101 | "description": "OK - Free space: 17233MB (16GB) / 51200MB (50GB) | Used: 33%", 102 | "schema": { 103 | "type": "string" 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "/sd/health": { 110 | "get": { 111 | "description": "Shows OK as the ping-pong result", 112 | "consumes": [ 113 | "application/json" 114 | ], 115 | "produces": [ 116 | "application/json" 117 | ], 118 | "tags": [ 119 | "sd" 120 | ], 121 | "summary": "Shows OK as the ping-pong result", 122 | "responses": { 123 | "200": { 124 | "description": "OK", 125 | "schema": { 126 | "type": "string" 127 | } 128 | } 129 | } 130 | } 131 | }, 132 | "/sd/ram": { 133 | "get": { 134 | "description": "Checks the ram usage", 135 | "consumes": [ 136 | "application/json" 137 | ], 138 | "produces": [ 139 | "application/json" 140 | ], 141 | "tags": [ 142 | "sd" 143 | ], 144 | "summary": "Checks the ram usage", 145 | "responses": { 146 | "200": { 147 | "description": "OK - Free space: 402MB (0GB) / 8192MB (8GB) | Used: 4%", 148 | "schema": { 149 | "type": "string" 150 | } 151 | } 152 | } 153 | } 154 | }, 155 | "/user": { 156 | "get": { 157 | "description": "List users", 158 | "consumes": [ 159 | "application/json" 160 | ], 161 | "produces": [ 162 | "application/json" 163 | ], 164 | "tags": [ 165 | "user" 166 | ], 167 | "summary": "List the users in the database", 168 | "parameters": [ 169 | { 170 | "description": "List users", 171 | "name": "user", 172 | "in": "body", 173 | "required": true, 174 | "schema": { 175 | "type": "object", 176 | "$ref": "#/definitions/user.ListRequest" 177 | } 178 | } 179 | ], 180 | "responses": { 181 | "200": { 182 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":{\"totalCount\":1,\"userList\":[{\"id\":0,\"username\":\"admin\",\"random\":\"user 'admin' get random string 'EnqntiSig'\",\"password\":\"$2a$10$veGcArz47VGj7l9xN7g2iuT9TF21jLI1YGXarGzvARNdnt4inC9PG\",\"createdAt\":\"2018-05-28 00:25:33\",\"updatedAt\":\"2018-05-28 00:25:33\"}]}}", 183 | "schema": { 184 | "type": "object", 185 | "$ref": "#/definitions/user.SwaggerListResponse" 186 | } 187 | } 188 | } 189 | }, 190 | "post": { 191 | "description": "Add a new user", 192 | "consumes": [ 193 | "application/json" 194 | ], 195 | "produces": [ 196 | "application/json" 197 | ], 198 | "tags": [ 199 | "user" 200 | ], 201 | "summary": "Add new user to the database", 202 | "parameters": [ 203 | { 204 | "description": "Create a new user", 205 | "name": "user", 206 | "in": "body", 207 | "required": true, 208 | "schema": { 209 | "type": "object", 210 | "$ref": "#/definitions/user.CreateRequest" 211 | } 212 | } 213 | ], 214 | "responses": { 215 | "200": { 216 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":{\"username\":\"kong\"}}", 217 | "schema": { 218 | "type": "object", 219 | "$ref": "#/definitions/user.CreateResponse" 220 | } 221 | } 222 | } 223 | } 224 | }, 225 | "/user/{id}": { 226 | "put": { 227 | "description": "Update a user by ID", 228 | "consumes": [ 229 | "application/json" 230 | ], 231 | "produces": [ 232 | "application/json" 233 | ], 234 | "tags": [ 235 | "user" 236 | ], 237 | "summary": "Update a user info by the user identifier", 238 | "parameters": [ 239 | { 240 | "type": "integer", 241 | "description": "The user's database id index num", 242 | "name": "id", 243 | "in": "path", 244 | "required": true 245 | }, 246 | { 247 | "description": "The user info", 248 | "name": "user", 249 | "in": "body", 250 | "required": true, 251 | "schema": { 252 | "type": "object", 253 | "$ref": "#/definitions/model.UserModel" 254 | } 255 | } 256 | ], 257 | "responses": { 258 | "200": { 259 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":null}", 260 | "schema": { 261 | "type": "object", 262 | "$ref": "#/definitions/handler.Response" 263 | } 264 | } 265 | } 266 | }, 267 | "delete": { 268 | "description": "Delete user by ID", 269 | "consumes": [ 270 | "application/json" 271 | ], 272 | "produces": [ 273 | "application/json" 274 | ], 275 | "tags": [ 276 | "user" 277 | ], 278 | "summary": "Delete an user by the user identifier", 279 | "parameters": [ 280 | { 281 | "type": "integer", 282 | "description": "The user's database id index num", 283 | "name": "id", 284 | "in": "path", 285 | "required": true 286 | } 287 | ], 288 | "responses": { 289 | "200": { 290 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":null}", 291 | "schema": { 292 | "type": "object", 293 | "$ref": "#/definitions/handler.Response" 294 | } 295 | } 296 | } 297 | } 298 | }, 299 | "/user/{username}": { 300 | "get": { 301 | "description": "Get an user by username", 302 | "consumes": [ 303 | "application/json" 304 | ], 305 | "produces": [ 306 | "application/json" 307 | ], 308 | "tags": [ 309 | "user" 310 | ], 311 | "summary": "Get an user by the user identifier", 312 | "parameters": [ 313 | { 314 | "type": "string", 315 | "description": "Username", 316 | "name": "username", 317 | "in": "path", 318 | "required": true 319 | } 320 | ], 321 | "responses": { 322 | "200": { 323 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":{\"username\":\"kong\",\"password\":\"$2a$10$E0kwtmtLZbwW/bDQ8qI8e.eHPqhQOW9tvjwpyo/p05f/f4Qvr3OmS\"}}", 324 | "schema": { 325 | "type": "object", 326 | "$ref": "#/definitions/model.UserModel" 327 | } 328 | } 329 | } 330 | } 331 | } 332 | }, 333 | "definitions": { 334 | "handler.Response": { 335 | "type": "object", 336 | "properties": { 337 | "code": { 338 | "type": "integer" 339 | }, 340 | "data": { 341 | "type": "object" 342 | }, 343 | "message": { 344 | "type": "string" 345 | } 346 | } 347 | }, 348 | "model.UserModel": { 349 | "type": "object", 350 | "required": [ 351 | "username", 352 | "password" 353 | ], 354 | "properties": { 355 | "password": { 356 | "type": "string" 357 | }, 358 | "username": { 359 | "type": "string" 360 | } 361 | } 362 | }, 363 | "user.CreateRequest": { 364 | "type": "object", 365 | "properties": { 366 | "password": { 367 | "type": "string" 368 | }, 369 | "username": { 370 | "type": "string" 371 | } 372 | } 373 | }, 374 | "user.CreateResponse": { 375 | "type": "object", 376 | "properties": { 377 | "username": { 378 | "type": "string" 379 | } 380 | } 381 | }, 382 | "user.ListRequest": { 383 | "type": "object", 384 | "properties": { 385 | "limit": { 386 | "type": "integer" 387 | }, 388 | "offset": { 389 | "type": "integer" 390 | }, 391 | "username": { 392 | "type": "string" 393 | } 394 | } 395 | }, 396 | "user.SwaggerListResponse": { 397 | "type": "object", 398 | "properties": { 399 | "totalCount": { 400 | "type": "integer" 401 | }, 402 | "userList": { 403 | "type": "array", 404 | "items": { 405 | "type": "\u0026{model UserInfo}" 406 | } 407 | } 408 | } 409 | } 410 | } 411 | }` 412 | 413 | type s struct{} 414 | 415 | func (s *s) ReadDoc() string { 416 | return doc 417 | } 418 | func init() { 419 | swag.Register(swag.Name, &s{}) 420 | } 421 | -------------------------------------------------------------------------------- /docs/swagger/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "tapi-blog demo", 5 | "title": "tapi-blog Example API", 6 | "contact": { 7 | "name": "lkong", 8 | "url": "http://www.swagger.io/support", 9 | "email": "466701708@qq.com" 10 | }, 11 | "license": {}, 12 | "version": "1.0" 13 | }, 14 | "host": "localhost:8080", 15 | "basePath": "/v1", 16 | "paths": { 17 | "/login": { 18 | "post": { 19 | "produces": [ 20 | "application/json" 21 | ], 22 | "summary": "Login generates the authentication token", 23 | "parameters": [ 24 | { 25 | "description": "Username", 26 | "name": "username", 27 | "in": "body", 28 | "required": true, 29 | "schema": { 30 | "type": "object" 31 | } 32 | }, 33 | { 34 | "description": "Password", 35 | "name": "password", 36 | "in": "body", 37 | "required": true, 38 | "schema": { 39 | "type": "object" 40 | } 41 | } 42 | ], 43 | "responses": { 44 | "200": { 45 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":{\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjgwMTY5MjIsImlkIjowLCJuYmYiOjE1MjgwMTY5MjIsInVzZXJuYW1lIjoiYWRtaW4ifQ.LjxrK9DuAwAzUD8-9v43NzWBN7HXsSLfebw92DKd1JQ\"}}", 46 | "schema": { 47 | "type": "string" 48 | } 49 | } 50 | } 51 | } 52 | }, 53 | "/sd/cpu": { 54 | "get": { 55 | "description": "Checks the cpu usage", 56 | "consumes": [ 57 | "application/json" 58 | ], 59 | "produces": [ 60 | "application/json" 61 | ], 62 | "tags": [ 63 | "sd" 64 | ], 65 | "summary": "Checks the cpu usage", 66 | "responses": { 67 | "200": { 68 | "description": "CRITICAL - Load average: 1.78, 1.99, 2.02 | Cores: 2", 69 | "schema": { 70 | "type": "string" 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | "/sd/disk": { 77 | "get": { 78 | "description": "Checks the disk usage", 79 | "consumes": [ 80 | "application/json" 81 | ], 82 | "produces": [ 83 | "application/json" 84 | ], 85 | "tags": [ 86 | "sd" 87 | ], 88 | "summary": "Checks the disk usage", 89 | "responses": { 90 | "200": { 91 | "description": "OK - Free space: 17233MB (16GB) / 51200MB (50GB) | Used: 33%", 92 | "schema": { 93 | "type": "string" 94 | } 95 | } 96 | } 97 | } 98 | }, 99 | "/sd/health": { 100 | "get": { 101 | "description": "Shows OK as the ping-pong result", 102 | "consumes": [ 103 | "application/json" 104 | ], 105 | "produces": [ 106 | "application/json" 107 | ], 108 | "tags": [ 109 | "sd" 110 | ], 111 | "summary": "Shows OK as the ping-pong result", 112 | "responses": { 113 | "200": { 114 | "description": "OK", 115 | "schema": { 116 | "type": "string" 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "/sd/ram": { 123 | "get": { 124 | "description": "Checks the ram usage", 125 | "consumes": [ 126 | "application/json" 127 | ], 128 | "produces": [ 129 | "application/json" 130 | ], 131 | "tags": [ 132 | "sd" 133 | ], 134 | "summary": "Checks the ram usage", 135 | "responses": { 136 | "200": { 137 | "description": "OK - Free space: 402MB (0GB) / 8192MB (8GB) | Used: 4%", 138 | "schema": { 139 | "type": "string" 140 | } 141 | } 142 | } 143 | } 144 | }, 145 | "/user": { 146 | "get": { 147 | "description": "List users", 148 | "consumes": [ 149 | "application/json" 150 | ], 151 | "produces": [ 152 | "application/json" 153 | ], 154 | "tags": [ 155 | "user" 156 | ], 157 | "summary": "List the users in the database", 158 | "parameters": [ 159 | { 160 | "description": "List users", 161 | "name": "user", 162 | "in": "body", 163 | "required": true, 164 | "schema": { 165 | "type": "object", 166 | "$ref": "#/definitions/user.ListRequest" 167 | } 168 | } 169 | ], 170 | "responses": { 171 | "200": { 172 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":{\"totalCount\":1,\"userList\":[{\"id\":0,\"username\":\"admin\",\"random\":\"user 'admin' get random string 'EnqntiSig'\",\"password\":\"$2a$10$veGcArz47VGj7l9xN7g2iuT9TF21jLI1YGXarGzvARNdnt4inC9PG\",\"createdAt\":\"2018-05-28 00:25:33\",\"updatedAt\":\"2018-05-28 00:25:33\"}]}}", 173 | "schema": { 174 | "type": "object", 175 | "$ref": "#/definitions/user.SwaggerListResponse" 176 | } 177 | } 178 | } 179 | }, 180 | "post": { 181 | "description": "Add a new user", 182 | "consumes": [ 183 | "application/json" 184 | ], 185 | "produces": [ 186 | "application/json" 187 | ], 188 | "tags": [ 189 | "user" 190 | ], 191 | "summary": "Add new user to the database", 192 | "parameters": [ 193 | { 194 | "description": "Create a new user", 195 | "name": "user", 196 | "in": "body", 197 | "required": true, 198 | "schema": { 199 | "type": "object", 200 | "$ref": "#/definitions/user.CreateRequest" 201 | } 202 | } 203 | ], 204 | "responses": { 205 | "200": { 206 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":{\"username\":\"kong\"}}", 207 | "schema": { 208 | "type": "object", 209 | "$ref": "#/definitions/user.CreateResponse" 210 | } 211 | } 212 | } 213 | } 214 | }, 215 | "/user/{id}": { 216 | "put": { 217 | "description": "Update a user by ID", 218 | "consumes": [ 219 | "application/json" 220 | ], 221 | "produces": [ 222 | "application/json" 223 | ], 224 | "tags": [ 225 | "user" 226 | ], 227 | "summary": "Update a user info by the user identifier", 228 | "parameters": [ 229 | { 230 | "type": "integer", 231 | "description": "The user's database id index num", 232 | "name": "id", 233 | "in": "path", 234 | "required": true 235 | }, 236 | { 237 | "description": "The user info", 238 | "name": "user", 239 | "in": "body", 240 | "required": true, 241 | "schema": { 242 | "type": "object", 243 | "$ref": "#/definitions/model.UserModel" 244 | } 245 | } 246 | ], 247 | "responses": { 248 | "200": { 249 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":null}", 250 | "schema": { 251 | "type": "object", 252 | "$ref": "#/definitions/handler.Response" 253 | } 254 | } 255 | } 256 | }, 257 | "delete": { 258 | "description": "Delete user by ID", 259 | "consumes": [ 260 | "application/json" 261 | ], 262 | "produces": [ 263 | "application/json" 264 | ], 265 | "tags": [ 266 | "user" 267 | ], 268 | "summary": "Delete an user by the user identifier", 269 | "parameters": [ 270 | { 271 | "type": "integer", 272 | "description": "The user's database id index num", 273 | "name": "id", 274 | "in": "path", 275 | "required": true 276 | } 277 | ], 278 | "responses": { 279 | "200": { 280 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":null}", 281 | "schema": { 282 | "type": "object", 283 | "$ref": "#/definitions/handler.Response" 284 | } 285 | } 286 | } 287 | } 288 | }, 289 | "/user/{username}": { 290 | "get": { 291 | "description": "Get an user by username", 292 | "consumes": [ 293 | "application/json" 294 | ], 295 | "produces": [ 296 | "application/json" 297 | ], 298 | "tags": [ 299 | "user" 300 | ], 301 | "summary": "Get an user by the user identifier", 302 | "parameters": [ 303 | { 304 | "type": "string", 305 | "description": "Username", 306 | "name": "username", 307 | "in": "path", 308 | "required": true 309 | } 310 | ], 311 | "responses": { 312 | "200": { 313 | "description": "{\"code\":0,\"message\":\"OK\",\"data\":{\"username\":\"kong\",\"password\":\"$2a$10$E0kwtmtLZbwW/bDQ8qI8e.eHPqhQOW9tvjwpyo/p05f/f4Qvr3OmS\"}}", 314 | "schema": { 315 | "type": "object", 316 | "$ref": "#/definitions/model.UserModel" 317 | } 318 | } 319 | } 320 | } 321 | } 322 | }, 323 | "definitions": { 324 | "handler.Response": { 325 | "type": "object", 326 | "properties": { 327 | "code": { 328 | "type": "integer" 329 | }, 330 | "data": { 331 | "type": "object" 332 | }, 333 | "message": { 334 | "type": "string" 335 | } 336 | } 337 | }, 338 | "model.UserModel": { 339 | "type": "object", 340 | "required": [ 341 | "username", 342 | "password" 343 | ], 344 | "properties": { 345 | "password": { 346 | "type": "string" 347 | }, 348 | "username": { 349 | "type": "string" 350 | } 351 | } 352 | }, 353 | "user.CreateRequest": { 354 | "type": "object", 355 | "properties": { 356 | "password": { 357 | "type": "string" 358 | }, 359 | "username": { 360 | "type": "string" 361 | } 362 | } 363 | }, 364 | "user.CreateResponse": { 365 | "type": "object", 366 | "properties": { 367 | "username": { 368 | "type": "string" 369 | } 370 | } 371 | }, 372 | "user.ListRequest": { 373 | "type": "object", 374 | "properties": { 375 | "limit": { 376 | "type": "integer" 377 | }, 378 | "offset": { 379 | "type": "integer" 380 | }, 381 | "username": { 382 | "type": "string" 383 | } 384 | } 385 | }, 386 | "user.SwaggerListResponse": { 387 | "type": "object", 388 | "properties": { 389 | "totalCount": { 390 | "type": "integer" 391 | }, 392 | "userList": { 393 | "type": "array", 394 | "items": { 395 | "type": "\u0026{model UserInfo}" 396 | } 397 | } 398 | } 399 | } 400 | } 401 | } -------------------------------------------------------------------------------- /docs/swagger/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /v1 2 | definitions: 3 | handler.Response: 4 | properties: 5 | code: 6 | type: integer 7 | data: 8 | type: object 9 | message: 10 | type: string 11 | type: object 12 | model.UserModel: 13 | properties: 14 | password: 15 | type: string 16 | username: 17 | type: string 18 | required: 19 | - username 20 | - password 21 | type: object 22 | user.CreateRequest: 23 | properties: 24 | password: 25 | type: string 26 | username: 27 | type: string 28 | type: object 29 | user.CreateResponse: 30 | properties: 31 | username: 32 | type: string 33 | type: object 34 | user.ListRequest: 35 | properties: 36 | limit: 37 | type: integer 38 | offset: 39 | type: integer 40 | username: 41 | type: string 42 | type: object 43 | user.SwaggerListResponse: 44 | properties: 45 | totalCount: 46 | type: integer 47 | userList: 48 | items: 49 | type: '&{model UserInfo}' 50 | type: array 51 | type: object 52 | host: localhost:8080 53 | info: 54 | contact: 55 | email: 466701708@qq.com 56 | name: lkong 57 | url: http://www.swagger.io/support 58 | description: tapi-blog demo 59 | license: {} 60 | title: tapi-blog Example API 61 | version: "1.0" 62 | paths: 63 | /login: 64 | post: 65 | parameters: 66 | - description: Username 67 | in: body 68 | name: username 69 | required: true 70 | schema: 71 | type: object 72 | - description: Password 73 | in: body 74 | name: password 75 | required: true 76 | schema: 77 | type: object 78 | produces: 79 | - application/json 80 | responses: 81 | "200": 82 | description: '{"code":0,"message":"OK","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjgwMTY5MjIsImlkIjowLCJuYmYiOjE1MjgwMTY5MjIsInVzZXJuYW1lIjoiYWRtaW4ifQ.LjxrK9DuAwAzUD8-9v43NzWBN7HXsSLfebw92DKd1JQ"}}' 83 | schema: 84 | type: string 85 | summary: Login generates the authentication token 86 | /sd/cpu: 87 | get: 88 | consumes: 89 | - application/json 90 | description: Checks the cpu usage 91 | produces: 92 | - application/json 93 | responses: 94 | "200": 95 | description: 'CRITICAL - Load average: 1.78, 1.99, 2.02 | Cores: 2' 96 | schema: 97 | type: string 98 | summary: Checks the cpu usage 99 | tags: 100 | - sd 101 | /sd/disk: 102 | get: 103 | consumes: 104 | - application/json 105 | description: Checks the disk usage 106 | produces: 107 | - application/json 108 | responses: 109 | "200": 110 | description: 'OK - Free space: 17233MB (16GB) / 51200MB (50GB) | Used: 33%' 111 | schema: 112 | type: string 113 | summary: Checks the disk usage 114 | tags: 115 | - sd 116 | /sd/health: 117 | get: 118 | consumes: 119 | - application/json 120 | description: Shows OK as the ping-pong result 121 | produces: 122 | - application/json 123 | responses: 124 | "200": 125 | description: OK 126 | schema: 127 | type: string 128 | summary: Shows OK as the ping-pong result 129 | tags: 130 | - sd 131 | /sd/ram: 132 | get: 133 | consumes: 134 | - application/json 135 | description: Checks the ram usage 136 | produces: 137 | - application/json 138 | responses: 139 | "200": 140 | description: 'OK - Free space: 402MB (0GB) / 8192MB (8GB) | Used: 4%' 141 | schema: 142 | type: string 143 | summary: Checks the ram usage 144 | tags: 145 | - sd 146 | /user: 147 | get: 148 | consumes: 149 | - application/json 150 | description: List users 151 | parameters: 152 | - description: List users 153 | in: body 154 | name: user 155 | required: true 156 | schema: 157 | $ref: '#/definitions/user.ListRequest' 158 | type: object 159 | produces: 160 | - application/json 161 | responses: 162 | "200": 163 | description: '{"code":0,"message":"OK","data":{"totalCount":1,"userList":[{"id":0,"username":"admin","random":"user 164 | ''admin'' get random string ''EnqntiSig''","password":"$2a$10$veGcArz47VGj7l9xN7g2iuT9TF21jLI1YGXarGzvARNdnt4inC9PG","createdAt":"2018-05-28 165 | 00:25:33","updatedAt":"2018-05-28 00:25:33"}]}}' 166 | schema: 167 | $ref: '#/definitions/user.SwaggerListResponse' 168 | type: object 169 | summary: List the users in the database 170 | tags: 171 | - user 172 | post: 173 | consumes: 174 | - application/json 175 | description: Add a new user 176 | parameters: 177 | - description: Create a new user 178 | in: body 179 | name: user 180 | required: true 181 | schema: 182 | $ref: '#/definitions/user.CreateRequest' 183 | type: object 184 | produces: 185 | - application/json 186 | responses: 187 | "200": 188 | description: '{"code":0,"message":"OK","data":{"username":"kong"}}' 189 | schema: 190 | $ref: '#/definitions/user.CreateResponse' 191 | type: object 192 | summary: Add new user to the database 193 | tags: 194 | - user 195 | /user/{id}: 196 | delete: 197 | consumes: 198 | - application/json 199 | description: Delete user by ID 200 | parameters: 201 | - description: The user's database id index num 202 | in: path 203 | name: id 204 | required: true 205 | type: integer 206 | produces: 207 | - application/json 208 | responses: 209 | "200": 210 | description: '{"code":0,"message":"OK","data":null}' 211 | schema: 212 | $ref: '#/definitions/handler.Response' 213 | type: object 214 | summary: Delete an user by the user identifier 215 | tags: 216 | - user 217 | put: 218 | consumes: 219 | - application/json 220 | description: Update a user by ID 221 | parameters: 222 | - description: The user's database id index num 223 | in: path 224 | name: id 225 | required: true 226 | type: integer 227 | - description: The user info 228 | in: body 229 | name: user 230 | required: true 231 | schema: 232 | $ref: '#/definitions/model.UserModel' 233 | type: object 234 | produces: 235 | - application/json 236 | responses: 237 | "200": 238 | description: '{"code":0,"message":"OK","data":null}' 239 | schema: 240 | $ref: '#/definitions/handler.Response' 241 | type: object 242 | summary: Update a user info by the user identifier 243 | tags: 244 | - user 245 | /user/{username}: 246 | get: 247 | consumes: 248 | - application/json 249 | description: Get an user by username 250 | parameters: 251 | - description: Username 252 | in: path 253 | name: username 254 | required: true 255 | type: string 256 | produces: 257 | - application/json 258 | responses: 259 | "200": 260 | description: '{"code":0,"message":"OK","data":{"username":"kong","password":"$2a$10$E0kwtmtLZbwW/bDQ8qI8e.eHPqhQOW9tvjwpyo/p05f/f4Qvr3OmS"}}' 261 | schema: 262 | $ref: '#/definitions/model.UserModel' 263 | type: object 264 | summary: Get an user by the user identifier 265 | tags: 266 | - user 267 | swagger: "2.0" 268 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/china-golang/tapi-blog/pkg/errno" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type Response struct { 12 | Code int `json:"code"` 13 | Message string `json:"message"` 14 | Data interface{} `json:"data"` 15 | } 16 | 17 | func SendResponse(c *gin.Context, err error, data interface{}) { 18 | code, message := errno.DecodeErr(err) 19 | 20 | // always return http.StatusOK 21 | c.JSON(http.StatusOK, Response{ 22 | Code: code, 23 | Message: message, 24 | Data: data, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /handler/sd/check.go: -------------------------------------------------------------------------------- 1 | package sd 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/shirou/gopsutil/cpu" 9 | "github.com/shirou/gopsutil/disk" 10 | "github.com/shirou/gopsutil/load" 11 | "github.com/shirou/gopsutil/mem" 12 | ) 13 | 14 | const ( 15 | B = 1 16 | KB = 1024 * B 17 | MB = 1024 * KB 18 | GB = 1024 * MB 19 | ) 20 | 21 | // @Summary Shows OK as the ping-pong result 22 | // @Description Shows OK as the ping-pong result 23 | // @Tags sd 24 | // @Accept json 25 | // @Produce json 26 | // @Success 200 {string} plain "OK" 27 | // @Router /sd/health [get] 28 | func HealthCheck(c *gin.Context) { 29 | message := "OK" 30 | c.String(http.StatusOK, "\n"+message) 31 | } 32 | 33 | // @Summary Checks the disk usage 34 | // @Description Checks the disk usage 35 | // @Tags sd 36 | // @Accept json 37 | // @Produce json 38 | // @Success 200 {string} plain "OK - Free space: 17233MB (16GB) / 51200MB (50GB) | Used: 33%" 39 | // @Router /sd/disk [get] 40 | func DiskCheck(c *gin.Context) { 41 | u, _ := disk.Usage("/") 42 | 43 | usedMB := int(u.Used) / MB 44 | usedGB := int(u.Used) / GB 45 | totalMB := int(u.Total) / MB 46 | totalGB := int(u.Total) / GB 47 | usedPercent := int(u.UsedPercent) 48 | 49 | status := http.StatusOK 50 | text := "OK" 51 | 52 | if usedPercent >= 95 { 53 | status = http.StatusOK 54 | text = "CRITICAL" 55 | } else if usedPercent >= 90 { 56 | status = http.StatusTooManyRequests 57 | text = "WARNING" 58 | } 59 | 60 | message := fmt.Sprintf("%s - Free space: %dMB (%dGB) / %dMB (%dGB) | Used: %d%%", text, usedMB, usedGB, totalMB, totalGB, usedPercent) 61 | c.String(status, "\n"+message) 62 | } 63 | 64 | // @Summary Checks the cpu usage 65 | // @Description Checks the cpu usage 66 | // @Tags sd 67 | // @Accept json 68 | // @Produce json 69 | // @Success 200 {string} plain "CRITICAL - Load average: 1.78, 1.99, 2.02 | Cores: 2" 70 | // @Router /sd/cpu [get] 71 | func CPUCheck(c *gin.Context) { 72 | cores, _ := cpu.Counts(false) 73 | 74 | a, _ := load.Avg() 75 | l1 := a.Load1 76 | l5 := a.Load5 77 | l15 := a.Load15 78 | 79 | status := http.StatusOK 80 | text := "OK" 81 | 82 | if l5 >= float64(cores-1) { 83 | status = http.StatusInternalServerError 84 | text = "CRITICAL" 85 | } else if l5 >= float64(cores-2) { 86 | status = http.StatusTooManyRequests 87 | text = "WARNING" 88 | } 89 | 90 | message := fmt.Sprintf("%s - Load average: %.2f, %.2f, %.2f | Cores: %d", text, l1, l5, l15, cores) 91 | c.String(status, "\n"+message) 92 | } 93 | 94 | // @Summary Checks the ram usage 95 | // @Description Checks the ram usage 96 | // @Tags sd 97 | // @Accept json 98 | // @Produce json 99 | // @Success 200 {string} plain "OK - Free space: 402MB (0GB) / 8192MB (8GB) | Used: 4%" 100 | // @Router /sd/ram [get] 101 | func RAMCheck(c *gin.Context) { 102 | u, _ := mem.VirtualMemory() 103 | 104 | usedMB := int(u.Used) / MB 105 | usedGB := int(u.Used) / GB 106 | totalMB := int(u.Total) / MB 107 | totalGB := int(u.Total) / GB 108 | usedPercent := int(u.UsedPercent) 109 | 110 | status := http.StatusOK 111 | text := "OK" 112 | 113 | if usedPercent >= 95 { 114 | status = http.StatusInternalServerError 115 | text = "CRITICAL" 116 | } else if usedPercent >= 90 { 117 | status = http.StatusTooManyRequests 118 | text = "WARNING" 119 | } 120 | 121 | message := fmt.Sprintf("%s - Free space: %dMB (%dGB) / %dMB (%dGB) | Used: %d%%", text, usedMB, usedGB, totalMB, totalGB, usedPercent) 122 | c.String(status, "\n"+message) 123 | } 124 | -------------------------------------------------------------------------------- /handler/user/create.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | . "github.com/china-golang/tapi-blog/handler" 5 | "github.com/china-golang/tapi-blog/model" 6 | "github.com/china-golang/tapi-blog/pkg/errno" 7 | "github.com/china-golang/tapi-blog/util" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/lexkong/log" 11 | "github.com/lexkong/log/lager" 12 | ) 13 | 14 | // @Summary Add new user to the database 15 | // @Description Add a new user 16 | // @Tags user 17 | // @Accept json 18 | // @Produce json 19 | // @Param user body user.CreateRequest true "Create a new user" 20 | // @Success 200 {object} user.CreateResponse "{"code":0,"message":"OK","data":{"username":"kong"}}" 21 | // @Router /user [post] 22 | func Create(c *gin.Context) { 23 | log.Info("User Create function called.", lager.Data{"X-Request-Id": util.GetReqID(c)}) 24 | var r CreateRequest 25 | if err := c.Bind(&r); err != nil { 26 | SendResponse(c, errno.ErrBind, nil) 27 | return 28 | } 29 | 30 | u := model.UserModel{ 31 | Username: r.Username, 32 | Password: r.Password, 33 | } 34 | 35 | // Validate the data. 36 | if err := u.Validate(); err != nil { 37 | SendResponse(c, errno.ErrValidation, nil) 38 | return 39 | } 40 | 41 | // Encrypt the user password. 42 | if err := u.Encrypt(); err != nil { 43 | SendResponse(c, errno.ErrEncrypt, nil) 44 | return 45 | } 46 | // Insert the user to the database. 47 | if err := u.Create(); err != nil { 48 | SendResponse(c, errno.ErrDatabase, nil) 49 | return 50 | } 51 | 52 | rsp := CreateResponse{ 53 | Username: r.Username, 54 | } 55 | 56 | // Show the user information. 57 | SendResponse(c, nil, rsp) 58 | } 59 | -------------------------------------------------------------------------------- /handler/user/delete.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "strconv" 5 | 6 | . "github.com/china-golang/tapi-blog/handler" 7 | "github.com/china-golang/tapi-blog/model" 8 | "github.com/china-golang/tapi-blog/pkg/errno" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // @Summary Delete an user by the user identifier 14 | // @Description Delete user by ID 15 | // @Tags user 16 | // @Accept json 17 | // @Produce json 18 | // @Param id path uint64 true "The user's database id index num" 19 | // @Success 200 {object} handler.Response "{"code":0,"message":"OK","data":null}" 20 | // @Router /user/{id} [delete] 21 | func Delete(c *gin.Context) { 22 | userId, _ := strconv.Atoi(c.Param("id")) 23 | if err := model.DeleteUser(uint64(userId)); err != nil { 24 | SendResponse(c, errno.ErrDatabase, nil) 25 | return 26 | } 27 | 28 | SendResponse(c, nil, nil) 29 | } 30 | -------------------------------------------------------------------------------- /handler/user/get.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | . "github.com/china-golang/tapi-blog/handler" 5 | "github.com/china-golang/tapi-blog/model" 6 | "github.com/china-golang/tapi-blog/pkg/errno" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // @Summary Get an user by the user identifier 12 | // @Description Get an user by username 13 | // @Tags user 14 | // @Accept json 15 | // @Produce json 16 | // @Param username path string true "Username" 17 | // @Success 200 {object} model.UserModel "{"code":0,"message":"OK","data":{"username":"kong","password":"$2a$10$E0kwtmtLZbwW/bDQ8qI8e.eHPqhQOW9tvjwpyo/p05f/f4Qvr3OmS"}}" 18 | // @Router /user/{username} [get] 19 | func Get(c *gin.Context) { 20 | username := c.Param("username") 21 | // Get the user by the `username` from the database. 22 | user, err := model.GetUser(username) 23 | if err != nil { 24 | SendResponse(c, errno.ErrUserNotFound, nil) 25 | return 26 | } 27 | 28 | SendResponse(c, nil, user) 29 | } 30 | -------------------------------------------------------------------------------- /handler/user/list.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | . "github.com/china-golang/tapi-blog/handler" 5 | "github.com/china-golang/tapi-blog/pkg/errno" 6 | "github.com/china-golang/tapi-blog/service" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/lexkong/log" 10 | ) 11 | 12 | // @Summary List the users in the database 13 | // @Description List users 14 | // @Tags user 15 | // @Accept json 16 | // @Produce json 17 | // @Param user body user.ListRequest true "List users" 18 | // @Success 200 {object} user.SwaggerListResponse "{"code":0,"message":"OK","data":{"totalCount":1,"userList":[{"id":0,"username":"admin","random":"user 'admin' get random string 'EnqntiSig'","password":"$2a$10$veGcArz47VGj7l9xN7g2iuT9TF21jLI1YGXarGzvARNdnt4inC9PG","createdAt":"2018-05-28 00:25:33","updatedAt":"2018-05-28 00:25:33"}]}}" 19 | // @Router /user [get] 20 | func List(c *gin.Context) { 21 | log.Info("List function called.") 22 | var r ListRequest 23 | if err := c.Bind(&r); err != nil { 24 | SendResponse(c, errno.ErrBind, nil) 25 | return 26 | } 27 | 28 | infos, count, err := service.ListUser(r.Username, r.Offset, r.Limit) 29 | if err != nil { 30 | SendResponse(c, err, nil) 31 | return 32 | } 33 | 34 | SendResponse(c, nil, ListResponse{ 35 | TotalCount: count, 36 | UserList: infos, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /handler/user/login.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | . "github.com/china-golang/tapi-blog/handler" 5 | "github.com/china-golang/tapi-blog/model" 6 | "github.com/china-golang/tapi-blog/pkg/auth" 7 | "github.com/china-golang/tapi-blog/pkg/errno" 8 | "github.com/china-golang/tapi-blog/pkg/token" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // @Summary Login generates the authentication token 14 | // @Produce json 15 | // @Param username body string true "Username" 16 | // @Param password body string true "Password" 17 | // @Success 200 {string} json "{"code":0,"message":"OK","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjgwMTY5MjIsImlkIjowLCJuYmYiOjE1MjgwMTY5MjIsInVzZXJuYW1lIjoiYWRtaW4ifQ.LjxrK9DuAwAzUD8-9v43NzWBN7HXsSLfebw92DKd1JQ"}}" 18 | // @Router /login [post] 19 | func Login(c *gin.Context) { 20 | // Binding the data with the user struct. 21 | var u model.UserModel 22 | if err := c.Bind(&u); err != nil { 23 | SendResponse(c, errno.ErrBind, nil) 24 | return 25 | } 26 | 27 | // Get the user information by the login username. 28 | d, err := model.GetUser(u.Username) 29 | if err != nil { 30 | SendResponse(c, errno.ErrUserNotFound, nil) 31 | return 32 | } 33 | 34 | // Compare the login password with the user password. 35 | if err := auth.Compare(d.Password, u.Password); err != nil { 36 | SendResponse(c, errno.ErrPasswordIncorrect, nil) 37 | return 38 | } 39 | 40 | // Sign the json web token. 41 | t, err := token.Sign(c, token.Context{ID: d.Id, Username: d.Username}, "") 42 | if err != nil { 43 | SendResponse(c, errno.ErrToken, nil) 44 | return 45 | } 46 | 47 | SendResponse(c, nil, model.Token{Token: t}) 48 | } 49 | -------------------------------------------------------------------------------- /handler/user/update.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "strconv" 5 | 6 | . "github.com/china-golang/tapi-blog/handler" 7 | "github.com/china-golang/tapi-blog/model" 8 | "github.com/china-golang/tapi-blog/pkg/errno" 9 | "github.com/china-golang/tapi-blog/util" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/lexkong/log" 13 | "github.com/lexkong/log/lager" 14 | ) 15 | 16 | // @Summary Update a user info by the user identifier 17 | // @Description Update a user by ID 18 | // @Tags user 19 | // @Accept json 20 | // @Produce json 21 | // @Param id path uint64 true "The user's database id index num" 22 | // @Param user body model.UserModel true "The user info" 23 | // @Success 200 {object} handler.Response "{"code":0,"message":"OK","data":null}" 24 | // @Router /user/{id} [put] 25 | func Update(c *gin.Context) { 26 | log.Info("Update function called.", lager.Data{"X-Request-Id": util.GetReqID(c)}) 27 | // Get the user id from the url parameter. 28 | userId, _ := strconv.Atoi(c.Param("id")) 29 | 30 | // Binding the user data. 31 | var u model.UserModel 32 | if err := c.Bind(&u); err != nil { 33 | SendResponse(c, errno.ErrBind, nil) 34 | return 35 | } 36 | 37 | // We update the record based on the user id. 38 | u.Id = uint64(userId) 39 | 40 | // Validate the data. 41 | if err := u.Validate(); err != nil { 42 | SendResponse(c, errno.ErrValidation, nil) 43 | return 44 | } 45 | 46 | // Encrypt the user password. 47 | if err := u.Encrypt(); err != nil { 48 | SendResponse(c, errno.ErrEncrypt, nil) 49 | return 50 | } 51 | 52 | // Save changed fields. 53 | if err := u.Update(); err != nil { 54 | SendResponse(c, errno.ErrDatabase, nil) 55 | return 56 | } 57 | 58 | SendResponse(c, nil, nil) 59 | } 60 | -------------------------------------------------------------------------------- /handler/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/china-golang/tapi-blog/model" 5 | ) 6 | 7 | type CreateRequest struct { 8 | Username string `json:"username"` 9 | Password string `json:"password"` 10 | } 11 | 12 | type CreateResponse struct { 13 | Username string `json:"username"` 14 | } 15 | 16 | type ListRequest struct { 17 | Username string `json:"username"` 18 | Offset int `json:"offset"` 19 | Limit int `json:"limit"` 20 | } 21 | 22 | type ListResponse struct { 23 | TotalCount uint64 `json:"totalCount"` 24 | UserList []*model.UserInfo `json:"userList"` 25 | } 26 | 27 | type SwaggerListResponse struct { 28 | TotalCount uint64 `json:"totalCount"` 29 | UserList []model.UserInfo `json:"userList"` 30 | } 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | _ "net/http/pprof" 9 | "os" 10 | "time" 11 | 12 | "github.com/china-golang/tapi-blog/config" 13 | "github.com/china-golang/tapi-blog/model" 14 | v "github.com/china-golang/tapi-blog/pkg/version" 15 | "github.com/china-golang/tapi-blog/router" 16 | "github.com/china-golang/tapi-blog/router/middleware" 17 | 18 | "github.com/gin-gonic/gin" 19 | "github.com/lexkong/log" 20 | "github.com/spf13/pflag" 21 | "github.com/spf13/viper" 22 | ) 23 | 24 | var ( 25 | cfg = pflag.StringP("config", "c", "", "tapi-blog config file path.") 26 | version = pflag.BoolP("version", "v", false, "show version info.") 27 | ) 28 | 29 | // @title tapi-blog Example API 30 | // @version 1.0 31 | // @description tapi-blog demo 32 | 33 | // @contact.name lkong 34 | // @contact.url http://www.swagger.io/support 35 | // @contact.email 466701708@qq.com 36 | 37 | // @host localhost:8080 38 | // @BasePath /v1 39 | func main() { 40 | pflag.Parse() 41 | if *version { 42 | v := v.Get() 43 | marshalled, err := json.MarshalIndent(&v, "", " ") 44 | if err != nil { 45 | fmt.Printf("%v\n", err) 46 | os.Exit(1) 47 | } 48 | 49 | fmt.Println(string(marshalled)) 50 | return 51 | } 52 | 53 | // init config 54 | if err := config.Init(*cfg); err != nil { 55 | panic(err) 56 | } 57 | 58 | // init db 59 | model.DB.Init() 60 | defer model.DB.Close() 61 | 62 | // Set gin mode. 63 | gin.SetMode(viper.GetString("runmode")) 64 | 65 | // Create the Gin engine. 66 | g := gin.New() 67 | 68 | // Routes. 69 | router.Load( 70 | // Cores. 71 | g, 72 | 73 | // Middlwares. 74 | middleware.Logging(), 75 | middleware.RequestId(), 76 | ) 77 | 78 | // Ping the server to make sure the router is working. 79 | go func() { 80 | if err := pingServer(); err != nil { 81 | log.Fatal("The router has no response, or it might took too long to start up.", err) 82 | } 83 | log.Info("The router has been deployed successfully.") 84 | }() 85 | 86 | // Start to listening the incoming requests. 87 | cert := viper.GetString("tls.cert") 88 | key := viper.GetString("tls.key") 89 | if cert != "" && key != "" { 90 | go func() { 91 | log.Infof("Start to listening the incoming requests on https address: %s", viper.GetString("tls.addr")) 92 | log.Info(http.ListenAndServeTLS(viper.GetString("tls.addr"), cert, key, g).Error()) 93 | }() 94 | } 95 | 96 | log.Infof("Start to listening the incoming requests on http address: %s", viper.GetString("addr")) 97 | log.Info(http.ListenAndServe(viper.GetString("addr"), g).Error()) 98 | } 99 | 100 | // pingServer pings the http server to make sure the router is working. 101 | func pingServer() error { 102 | for i := 0; i < viper.GetInt("max_ping_count"); i++ { 103 | // Ping the server by sending a GET request to `/health`. 104 | resp, err := http.Get(viper.GetString("url") + "/sd/health") 105 | if err == nil && resp.StatusCode == 200 { 106 | return nil 107 | } 108 | 109 | // Sleep for a second to continue the next ping. 110 | log.Info("Waiting for the router, retry in 1 second.") 111 | time.Sleep(time.Second) 112 | } 113 | return errors.New("Cannot connect to the router.") 114 | } 115 | -------------------------------------------------------------------------------- /model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lexkong/log" 7 | "github.com/spf13/viper" 8 | // MySQL driver. 9 | "github.com/jinzhu/gorm" 10 | _ "github.com/jinzhu/gorm/dialects/mysql" 11 | ) 12 | 13 | type Database struct { 14 | Self *gorm.DB 15 | Docker *gorm.DB 16 | } 17 | 18 | var DB *Database 19 | 20 | func openDB(username, password, addr, name string) *gorm.DB { 21 | config := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=%t&loc=%s", 22 | username, 23 | password, 24 | addr, 25 | name, 26 | true, 27 | //"Asia/Shanghai"), 28 | "Local") 29 | 30 | db, err := gorm.Open("mysql", config) 31 | if err != nil { 32 | log.Errorf(err, "Database connection failed. Database name: %s", name) 33 | } 34 | 35 | // set for db connection 36 | setupDB(db) 37 | 38 | return db 39 | } 40 | 41 | func setupDB(db *gorm.DB) { 42 | db.LogMode(viper.GetBool("gormlog")) 43 | //db.DB().SetMaxOpenConns(20000) // 用于设置最大打开的连接数,默认值为0表示不限制.设置最大的连接数,可以避免并发太高导致连接mysql出现too many connections的错误。 44 | db.DB().SetMaxIdleConns(0) // 用于设置闲置的连接数.设置闲置的连接数则当开启的一个连接使用完成后可以放在池里等候下一次使用。 45 | } 46 | 47 | // used for cli 48 | func InitSelfDB() *gorm.DB { 49 | return openDB(viper.GetString("db.username"), 50 | viper.GetString("db.password"), 51 | viper.GetString("db.addr"), 52 | viper.GetString("db.name")) 53 | } 54 | 55 | func GetSelfDB() *gorm.DB { 56 | return InitSelfDB() 57 | } 58 | 59 | func InitDockerDB() *gorm.DB { 60 | return openDB(viper.GetString("docker_db.username"), 61 | viper.GetString("docker_db.password"), 62 | viper.GetString("docker_db.addr"), 63 | viper.GetString("docker_db.name")) 64 | } 65 | 66 | func GetDockerDB() *gorm.DB { 67 | return InitDockerDB() 68 | } 69 | 70 | func (db *Database) Init() { 71 | DB = &Database{ 72 | Self: GetSelfDB(), 73 | Docker: GetDockerDB(), 74 | } 75 | } 76 | 77 | func (db *Database) Close() { 78 | DB.Self.Close() 79 | DB.Docker.Close() 80 | } 81 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type BaseModel struct { 9 | Id uint64 `gorm:"primary_key;AUTO_INCREMENT;column:id" json:"-"` 10 | CreatedAt time.Time `gorm:"column:createdAt" json:"-"` 11 | UpdatedAt time.Time `gorm:"column:updatedAt" json:"-"` 12 | DeletedAt *time.Time `gorm:"column:deletedAt" sql:"index" json:"-"` 13 | } 14 | 15 | type UserInfo struct { 16 | Id uint64 `json:"id"` 17 | Username string `json:"username"` 18 | SayHello string `json:"sayHello"` 19 | Password string `json:"password"` 20 | CreatedAt string `json:"createdAt"` 21 | UpdatedAt string `json:"updatedAt"` 22 | } 23 | 24 | type UserList struct { 25 | Lock *sync.Mutex 26 | IdMap map[uint64]*UserInfo 27 | } 28 | 29 | // Token represents a JSON web token. 30 | type Token struct { 31 | Token string `json:"token"` 32 | } 33 | -------------------------------------------------------------------------------- /model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/china-golang/tapi-blog/pkg/auth" 7 | "github.com/china-golang/tapi-blog/pkg/constvar" 8 | 9 | validator "gopkg.in/go-playground/validator.v9" 10 | ) 11 | 12 | // User represents a registered user. 13 | type UserModel struct { 14 | BaseModel 15 | Username string `json:"username" gorm:"column:username;not null" binding:"required" validate:"min=1,max=32"` 16 | Password string `json:"password" gorm:"column:password;not null" binding:"required" validate:"min=5,max=128"` 17 | } 18 | 19 | func (c *UserModel) TableName() string { 20 | return "tb_users" 21 | } 22 | 23 | // Create creates a new user account. 24 | func (u *UserModel) Create() error { 25 | return DB.Self.Create(&u).Error 26 | } 27 | 28 | // DeleteUser deletes the user by the user identifier. 29 | func DeleteUser(id uint64) error { 30 | user := UserModel{} 31 | user.BaseModel.Id = id 32 | return DB.Self.Delete(&user).Error 33 | } 34 | 35 | // Update updates an user account information. 36 | func (u *UserModel) Update() error { 37 | return DB.Self.Save(u).Error 38 | } 39 | 40 | // GetUser gets an user by the user identifier. 41 | func GetUser(username string) (*UserModel, error) { 42 | u := &UserModel{} 43 | d := DB.Self.Where("username = ?", username).First(&u) 44 | return u, d.Error 45 | } 46 | 47 | // ListUser List all users 48 | func ListUser(username string, offset, limit int) ([]*UserModel, uint64, error) { 49 | if limit == 0 { 50 | limit = constvar.DefaultLimit 51 | } 52 | 53 | users := make([]*UserModel, 0) 54 | var count uint64 55 | 56 | where := fmt.Sprintf("username like '%%%s%%'", username) 57 | if err := DB.Self.Model(&UserModel{}).Where(where).Count(&count).Error; err != nil { 58 | return users, count, err 59 | } 60 | 61 | if err := DB.Self.Where(where).Offset(offset).Limit(limit).Order("id desc").Find(&users).Error; err != nil { 62 | return users, count, err 63 | } 64 | 65 | return users, count, nil 66 | } 67 | 68 | // Compare with the plain text password. Returns true if it's the same as the encrypted one (in the `User` struct). 69 | func (u *UserModel) Compare(pwd string) (err error) { 70 | err = auth.Compare(u.Password, pwd) 71 | return 72 | } 73 | 74 | // Encrypt the user password. 75 | func (u *UserModel) Encrypt() (err error) { 76 | u.Password, err = auth.Encrypt(u.Password) 77 | return 78 | } 79 | 80 | // Validate the fields. 81 | func (u *UserModel) Validate() error { 82 | validate := validator.New() 83 | return validate.Struct(u) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | // Encrypt encrypts the plain text with bcrypt. 6 | func Encrypt(source string) (string, error) { 7 | hashedBytes, err := bcrypt.GenerateFromPassword([]byte(source), bcrypt.DefaultCost) 8 | return string(hashedBytes), err 9 | } 10 | 11 | // Compare compares the encrypted text with the plain text if it's the same. 12 | func Compare(hashedPassword, password string) error { 13 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/constvar/constvar.go: -------------------------------------------------------------------------------- 1 | package constvar 2 | 3 | const ( 4 | DefaultLimit = 50 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/errno/code.go: -------------------------------------------------------------------------------- 1 | package errno 2 | 3 | var ( 4 | // Common errors 5 | OK = &Errno{Code: 0, Message: "OK"} 6 | InternalServerError = &Errno{Code: 10001, Message: "Internal server error"} 7 | ErrBind = &Errno{Code: 10002, Message: "Error occurred while binding the request body to the struct."} 8 | 9 | ErrValidation = &Errno{Code: 20001, Message: "Validation failed."} 10 | ErrDatabase = &Errno{Code: 20002, Message: "Database error."} 11 | ErrToken = &Errno{Code: 20003, Message: "Error occurred while signing the JSON web token."} 12 | 13 | // user errors 14 | ErrEncrypt = &Errno{Code: 20101, Message: "Error occurred while encrypting the user password."} 15 | ErrUserNotFound = &Errno{Code: 20102, Message: "The user was not found."} 16 | ErrTokenInvalid = &Errno{Code: 20103, Message: "The token was invalid."} 17 | ErrPasswordIncorrect = &Errno{Code: 20104, Message: "The password was incorrect."} 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/errno/errno.go: -------------------------------------------------------------------------------- 1 | package errno 2 | 3 | import "fmt" 4 | 5 | type Errno struct { 6 | Code int 7 | Message string 8 | } 9 | 10 | func (err Errno) Error() string { 11 | return err.Message 12 | } 13 | 14 | // Err represents an error 15 | type Err struct { 16 | Code int 17 | Message string 18 | Err error 19 | } 20 | 21 | func New(errno *Errno, err error) *Err { 22 | return &Err{Code: errno.Code, Message: errno.Message, Err: err} 23 | } 24 | 25 | func (err *Err) Add(message string) error { 26 | err.Message += " " + message 27 | return err 28 | } 29 | 30 | func (err *Err) Addf(format string, args ...interface{}) error { 31 | err.Message += " " + fmt.Sprintf(format, args...) 32 | return err 33 | } 34 | 35 | func (err *Err) Error() string { 36 | return fmt.Sprintf("Err - code: %d, message: %s, error: %s", err.Code, err.Message, err.Err) 37 | } 38 | 39 | func IsErrUserNotFound(err error) bool { 40 | code, _ := DecodeErr(err) 41 | return code == ErrUserNotFound.Code 42 | } 43 | 44 | func DecodeErr(err error) (int, string) { 45 | if err == nil { 46 | return OK.Code, OK.Message 47 | } 48 | 49 | switch typed := err.(type) { 50 | case *Err: 51 | return typed.Code, typed.Message 52 | case *Errno: 53 | return typed.Code, typed.Message 54 | default: 55 | } 56 | 57 | return InternalServerError.Code, err.Error() 58 | } 59 | -------------------------------------------------------------------------------- /pkg/token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | jwt "github.com/dgrijalva/jwt-go" 9 | "github.com/gin-gonic/gin" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | var ( 14 | // ErrMissingHeader means the `Authorization` header was empty. 15 | ErrMissingHeader = errors.New("The length of the `Authorization` header is zero.") 16 | ) 17 | 18 | // Context is the context of the JSON web token. 19 | type Context struct { 20 | ID uint64 21 | Username string 22 | } 23 | 24 | // secretFunc validates the secret format. 25 | func secretFunc(secret string) jwt.Keyfunc { 26 | return func(token *jwt.Token) (interface{}, error) { 27 | // Make sure the `alg` is what we except. 28 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 29 | return nil, jwt.ErrSignatureInvalid 30 | } 31 | 32 | return []byte(secret), nil 33 | } 34 | } 35 | 36 | // Parse validates the token with the specified secret, 37 | // and returns the context if the token was valid. 38 | func Parse(tokenString string, secret string) (*Context, error) { 39 | ctx := &Context{} 40 | 41 | // Parse the token. 42 | token, err := jwt.Parse(tokenString, secretFunc(secret)) 43 | 44 | // Parse error. 45 | if err != nil { 46 | return ctx, err 47 | 48 | // Read the token if it's valid. 49 | } else if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 50 | ctx.ID = uint64(claims["id"].(float64)) 51 | ctx.Username = claims["username"].(string) 52 | return ctx, nil 53 | 54 | // Other errors. 55 | } else { 56 | return ctx, err 57 | } 58 | } 59 | 60 | // ParseRequest gets the token from the header and 61 | // pass it to the Parse function to parses the token. 62 | func ParseRequest(c *gin.Context) (*Context, error) { 63 | header := c.Request.Header.Get("Authorization") 64 | 65 | // Load the jwt secret from config 66 | secret := viper.GetString("jwt_secret") 67 | 68 | if len(header) == 0 { 69 | return &Context{}, ErrMissingHeader 70 | } 71 | 72 | var t string 73 | // Parse the header to get the token part. 74 | fmt.Sscanf(header, "Bearer %s", &t) 75 | return Parse(t, secret) 76 | } 77 | 78 | // Sign signs the context with the specified secret. 79 | func Sign(ctx *gin.Context, c Context, secret string) (tokenString string, err error) { 80 | // Load the jwt secret from the Gin config if the secret isn't specified. 81 | if secret == "" { 82 | secret = viper.GetString("jwt_secret") 83 | } 84 | // The token content. 85 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 86 | "id": c.ID, 87 | "username": c.Username, 88 | "nbf": time.Now().Unix(), 89 | "iat": time.Now().Unix(), 90 | }) 91 | // Sign the token with the specified secret. 92 | tokenString, err = token.SignedString([]byte(secret)) 93 | 94 | return 95 | } 96 | -------------------------------------------------------------------------------- /pkg/version/base.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | gitTag string = "" 5 | gitCommit string = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD) 6 | gitTreeState string = "not a git tree" // state of git tree, either "clean" or "dirty" 7 | buildDate string = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/version/doc.go: -------------------------------------------------------------------------------- 1 | package version 2 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | // Info contains versioning information. 9 | type Info struct { 10 | GitTag string `json:"gitTag"` 11 | GitCommit string `json:"gitCommit"` 12 | GitTreeState string `json:"gitTreeState"` 13 | BuildDate string `json:"buildDate"` 14 | GoVersion string `json:"goVersion"` 15 | Compiler string `json:"compiler"` 16 | Platform string `json:"platform"` 17 | } 18 | 19 | // String returns info as a human-friendly version string. 20 | func (info Info) String() string { 21 | return info.GitTag 22 | } 23 | 24 | func Get() Info { 25 | return Info{ 26 | GitTag: gitTag, 27 | GitCommit: gitCommit, 28 | GitTreeState: gitTreeState, 29 | BuildDate: buildDate, 30 | GoVersion: runtime.Version(), 31 | Compiler: runtime.Compiler, 32 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /router/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/china-golang/tapi-blog/handler" 5 | "github.com/china-golang/tapi-blog/pkg/errno" 6 | "github.com/china-golang/tapi-blog/pkg/token" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func AuthMiddleware() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | // Parse the json web token. 14 | if _, err := token.ParseRequest(c); err != nil { 15 | handler.SendResponse(c, errno.ErrTokenInvalid, nil) 16 | c.Abort() 17 | return 18 | } 19 | 20 | c.Next() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /router/middleware/header.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // NoCache is a middleware function that appends headers 11 | // to prevent the client from caching the HTTP response. 12 | func NoCache(c *gin.Context) { 13 | c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") 14 | c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") 15 | c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) 16 | c.Next() 17 | } 18 | 19 | // Options is a middleware function that appends headers 20 | // for options requests and aborts then exits the middleware 21 | // chain and ends the request. 22 | func Options(c *gin.Context) { 23 | if c.Request.Method != "OPTIONS" { 24 | c.Next() 25 | } else { 26 | c.Header("Access-Control-Allow-Origin", "*") 27 | c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") 28 | c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") 29 | c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") 30 | c.Header("Content-Type", "application/json") 31 | c.AbortWithStatus(200) 32 | } 33 | } 34 | 35 | // Secure is a middleware function that appends security 36 | // and resource access headers. 37 | func Secure(c *gin.Context) { 38 | c.Header("Access-Control-Allow-Origin", "*") 39 | c.Header("X-Frame-Options", "DENY") 40 | c.Header("X-Content-Type-Options", "nosniff") 41 | c.Header("X-XSS-Protection", "1; mode=block") 42 | if c.Request.TLS != nil { 43 | c.Header("Strict-Transport-Security", "max-age=31536000") 44 | } 45 | 46 | // Also consider adding Content-Security-Policy headers 47 | // c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com") 48 | } 49 | -------------------------------------------------------------------------------- /router/middleware/logging.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "regexp" 8 | "time" 9 | 10 | "github.com/china-golang/tapi-blog/handler" 11 | "github.com/china-golang/tapi-blog/pkg/errno" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/lexkong/log" 15 | "github.com/willf/pad" 16 | ) 17 | 18 | type bodyLogWriter struct { 19 | gin.ResponseWriter 20 | body *bytes.Buffer 21 | } 22 | 23 | func (w bodyLogWriter) Write(b []byte) (int, error) { 24 | w.body.Write(b) 25 | return w.ResponseWriter.Write(b) 26 | } 27 | 28 | // Logging is a middleware function that logs the each request. 29 | func Logging() gin.HandlerFunc { 30 | return func(c *gin.Context) { 31 | start := time.Now().UTC() 32 | path := c.Request.URL.Path 33 | 34 | reg := regexp.MustCompile("(/v1/user|/login)") 35 | if !reg.MatchString(path) { 36 | return 37 | } 38 | 39 | // Skip for the health check requests. 40 | if path == "/sd/health" || path == "/sd/ram" || path == "/sd/cpu" || path == "/sd/disk" { 41 | return 42 | } 43 | 44 | // Read the Body content 45 | var bodyBytes []byte 46 | if c.Request.Body != nil { 47 | bodyBytes, _ = ioutil.ReadAll(c.Request.Body) 48 | } 49 | 50 | // Restore the io.ReadCloser to its original state 51 | c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 52 | 53 | // The basic informations. 54 | method := c.Request.Method 55 | ip := c.ClientIP() 56 | 57 | //log.Debugf("New request come in, path: %s, Method: %s, body `%s`", path, method, string(bodyBytes)) 58 | blw := &bodyLogWriter{ 59 | body: bytes.NewBufferString(""), 60 | ResponseWriter: c.Writer, 61 | } 62 | c.Writer = blw 63 | 64 | // Continue. 65 | c.Next() 66 | 67 | // Calculates the latency. 68 | end := time.Now().UTC() 69 | latency := end.Sub(start) 70 | 71 | code, message := -1, "" 72 | 73 | // get code and message 74 | var response handler.Response 75 | if err := json.Unmarshal(blw.body.Bytes(), &response); err != nil { 76 | log.Errorf(err, "response body can not unmarshal to model.Response struct, body: `%s`", blw.body.Bytes()) 77 | code = errno.InternalServerError.Code 78 | message = err.Error() 79 | } else { 80 | code = response.Code 81 | message = response.Message 82 | } 83 | 84 | log.Infof("%-13s | %-12s | %s %s | {code: %d, message: %s}", latency, ip, pad.Right(method, 5, ""), path, code, message) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /router/middleware/requestid.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/satori/go.uuid" 6 | ) 7 | 8 | func RequestId() gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | // Check for incoming header, use it if exists 11 | requestId := c.Request.Header.Get("X-Request-Id") 12 | 13 | // Create request id with UUID4 14 | if requestId == "" { 15 | u4 := uuid.NewV4() 16 | requestId = u4.String() 17 | } 18 | 19 | // Expose it for use in the application 20 | c.Set("X-Request-Id", requestId) 21 | 22 | // Set X-Request-Id header 23 | c.Writer.Header().Set("X-Request-Id", requestId) 24 | c.Next() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | _ "github.com/china-golang/tapi-blog/docs" // docs is generated by Swag CLI, you have to import it. 7 | "github.com/china-golang/tapi-blog/handler/sd" 8 | "github.com/china-golang/tapi-blog/handler/user" 9 | "github.com/china-golang/tapi-blog/router/middleware" 10 | 11 | "github.com/gin-contrib/pprof" 12 | "github.com/gin-gonic/gin" 13 | "github.com/swaggo/gin-swagger" 14 | "github.com/swaggo/gin-swagger/swaggerFiles" 15 | ) 16 | 17 | // Load loads the middlewares, routes, handlers. 18 | func Load(g *gin.Engine, mw ...gin.HandlerFunc) *gin.Engine { 19 | // Middlewares. 20 | g.Use(gin.Recovery()) 21 | g.Use(middleware.NoCache) 22 | g.Use(middleware.Options) 23 | g.Use(middleware.Secure) 24 | g.Use(mw...) 25 | // 404 Handler. 26 | g.NoRoute(func(c *gin.Context) { 27 | c.String(http.StatusNotFound, "The incorrect API route.") 28 | }) 29 | 30 | // swagger api docs 31 | g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 32 | 33 | // pprof router 34 | pprof.Register(g,nil) 35 | 36 | // api for authentication functionalities 37 | g.POST("/login", user.Login) 38 | 39 | // The user handlers, requiring authentication 40 | u := g.Group("/v1/user") 41 | u.Use(middleware.AuthMiddleware()) 42 | { 43 | u.POST("", user.Create) 44 | u.DELETE("/:id", user.Delete) 45 | u.PUT("/:id", user.Update) 46 | u.GET("", user.List) 47 | u.GET("/:username", user.Get) 48 | } 49 | 50 | // The health check handlers 51 | svcd := g.Group("/sd") 52 | { 53 | svcd.GET("/health", sd.HealthCheck) 54 | svcd.GET("/disk", sd.DiskCheck) 55 | svcd.GET("/cpu", sd.CPUCheck) 56 | svcd.GET("/ram", sd.RAMCheck) 57 | } 58 | 59 | return g 60 | } 61 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/china-golang/tapi-blog/model" 8 | "github.com/china-golang/tapi-blog/util" 9 | ) 10 | 11 | func ListUser(username string, offset, limit int) ([]*model.UserInfo, uint64, error) { 12 | infos := make([]*model.UserInfo, 0) 13 | users, count, err := model.ListUser(username, offset, limit) 14 | if err != nil { 15 | return nil, count, err 16 | } 17 | 18 | ids := []uint64{} 19 | for _, user := range users { 20 | ids = append(ids, user.Id) 21 | } 22 | 23 | wg := sync.WaitGroup{} 24 | userList := model.UserList{ 25 | Lock: new(sync.Mutex), 26 | IdMap: make(map[uint64]*model.UserInfo, len(users)), 27 | } 28 | 29 | errChan := make(chan error, 1) 30 | finished := make(chan bool, 1) 31 | 32 | // Improve query efficiency in parallel 33 | for _, u := range users { 34 | wg.Add(1) 35 | go func(u *model.UserModel) { 36 | defer wg.Done() 37 | 38 | shortId, err := util.GenShortId() 39 | if err != nil { 40 | errChan <- err 41 | return 42 | } 43 | 44 | userList.Lock.Lock() 45 | defer userList.Lock.Unlock() 46 | userList.IdMap[u.Id] = &model.UserInfo{ 47 | Id: u.Id, 48 | Username: u.Username, 49 | SayHello: fmt.Sprintf("Hello %s", shortId), 50 | Password: u.Password, 51 | CreatedAt: u.CreatedAt.Format("2006-01-02 15:04:05"), 52 | UpdatedAt: u.UpdatedAt.Format("2006-01-02 15:04:05"), 53 | } 54 | }(u) 55 | } 56 | 57 | go func() { 58 | wg.Wait() 59 | close(finished) 60 | }() 61 | 62 | select { 63 | case <-finished: 64 | case err := <-errChan: 65 | return nil, count, err 66 | } 67 | 68 | for _, id := range ids { 69 | infos = append(infos, userList.IdMap[id]) 70 | } 71 | 72 | return infos, count, nil 73 | } 74 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/teris-io/shortid" 6 | ) 7 | 8 | func GenShortId() (string, error) { 9 | return shortid.Generate() 10 | } 11 | 12 | func GetReqID(c *gin.Context) string { 13 | v, ok := c.Get("X-Request-Id") 14 | if !ok { 15 | return "" 16 | } 17 | if requestId, ok := v.(string); ok { 18 | return requestId 19 | } 20 | return "" 21 | } 22 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGenShortId(t *testing.T) { 8 | shortId, err := GenShortId() 9 | if shortId == "" || err != nil { 10 | t.Error("GenShortId failed!") 11 | } 12 | 13 | t.Log("GenShortId test pass") 14 | } 15 | 16 | func BenchmarkGenShortId(b *testing.B) { 17 | for i := 0; i < b.N; i++ { 18 | GenShortId() 19 | } 20 | } 21 | 22 | func BenchmarkGenShortIdTimeConsuming(b *testing.B) { 23 | b.StopTimer() //调用该函数停止压力测试的时间计数 24 | 25 | shortId, err := GenShortId() 26 | if shortId == "" || err != nil { 27 | b.Error(err) 28 | } 29 | 30 | b.StartTimer() //重新开始时间 31 | 32 | for i := 0; i < b.N; i++ { 33 | GenShortId() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /wrktest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : << EOF 4 | API 性能测试脚本,会自动执行 wrk 命令,采集数据、分析数据并调用 gnuplot 画图 5 | 6 | 使用方式 ( 测试 API 性能): 7 | 1. 启动 tapi-blog (8080端口) 8 | 2. 执行测试脚本: ./wrktest.sh 9 | 10 | 脚本会生成 _wrk.dat 的数据文件,每列含义为: 11 | 12 | 并发数 QPS 平均响应时间 成功率 13 | 14 | 使用方式 (对比2次测试结果) 15 | 16 | 1. 执行命令: ./wrktest.sh diff tapi-blog1_wrk.dat http_wrk.dat 17 | 18 | > Note: 需要确保系统安装了 wrk 和 gnuplot 工具 19 | EOF 20 | 21 | t1="tapi-blog" # 对比图中红色线条名称 22 | t2="http" # 对比图中粉色线条名称 23 | jobname="tapi-blog" # 本次测试名称 24 | 25 | ## wrk 参数配置 26 | d="300s" 27 | concurrent="200 500 1000 3000 5000 10000 15000 20000 25000 50000 100000 200000 500000 1000000" 28 | threads=144 29 | 30 | if [ "$1" != "" ];then 31 | url="$1" 32 | else 33 | url="http://127.0.0.1:8080/sd/health" 34 | fi 35 | 36 | cmd="wrk --latency -t$threads -d$d -T30s $url" 37 | apiperformance="${jobname}_performance.png" 38 | apisuccessrate="${jobname}_success_rate.png" 39 | datfile="${jobname}_wrk.dat" 40 | 41 | # functions 42 | function convertPlotData() 43 | { 44 | echo "$1" | awk -v datfile="$datfile" ' { 45 | if ($0 ~ "Running") { 46 | common_time=$2 47 | } 48 | if ($0 ~ "connections") { 49 | connections=$4 50 | common_threads=$1 51 | } 52 | if ($0 ~ "Latency ") { 53 | avg_latency=convertLatency($2) 54 | } 55 | if ($0 ~ "50%") { 56 | p50=convertLatency($2) 57 | } 58 | if ($0 ~ "75%") { 59 | p75=convertLatency($2) 60 | } 61 | if ($0 ~ "90%") { 62 | p90=convertLatency($2) 63 | } 64 | if ($0 ~ "99%") { 65 | p99=convertLatency($2) 66 | } 67 | if ($0 ~ "Requests/sec") { 68 | qps=$2 69 | } 70 | if ($0 ~ "requests in") { 71 | allrequest=$1 72 | } 73 | if ($0 ~ "Socket errors") { 74 | err=$4+$6+$8+$10 75 | } 76 | 77 | } 78 | END { 79 | rate=sprintf("%.2f", (allrequest-err)*100/allrequest) 80 | print connections,qps,avg_latency,rate >> datfile 81 | } 82 | 83 | function convertLatency(s) { 84 | if (s ~ "us") { 85 | sub("us", "", s) 86 | return s/1000 87 | } 88 | if (s ~ "ms") { 89 | sub("ms", "", s) 90 | return s 91 | } 92 | if (s ~ "s") { 93 | sub("s", "", s) 94 | return s * 1000 95 | } 96 | } 97 | 98 | ' 99 | } 100 | 101 | function prepare() 102 | { 103 | rm -f $datfile 104 | } 105 | 106 | function plot() { 107 | gnuplot << EOF 108 | set terminal png enhanced #输出格式为png文件 109 | set output "$apiperformance" #指定数据文件名称 110 | set title "QPS & TTLB\nRunning: 300s\nThreads: $threads" 111 | set ylabel 'QPS' 112 | set xlabel 'Concurrent' 113 | set y2label 'Average Latency (ms)' 114 | set key top left vertical noreverse spacing 1.2 box 115 | set tics out nomirror 116 | set border 3 front 117 | set style line 1 linecolor rgb '#00ff00' linewidth 2 linetype 3 pointtype 2 118 | set style line 2 linecolor rgb '#ff0000' linewidth 1 linetype 3 pointtype 2 119 | set style data linespoints 120 | 121 | set grid #显示网格 122 | set xtics nomirror rotate #by 90#只需要一个x轴 123 | set mxtics 5 124 | set mytics 5 #可以增加分刻度 125 | set ytics nomirror 126 | set y2tics 127 | 128 | set autoscale y 129 | set autoscale y2 130 | 131 | plot "$datfile" using 2:xticlabels(1) w lp pt 7 ps 1 lc rgbcolor "#EE0000" axis x1y1 t "QPS","$datfile" using 3:xticlabels(1) w lp pt 5 ps 1 lc rgbcolor "#0000CD" axis x2y2 t "Avg Latency (ms)" 132 | 133 | unset y2tics 134 | unset y2label 135 | set ytics nomirror 136 | set yrange[0:100] 137 | set output "$apisuccessrate" #指定数据文件名称 138 | set title "Success Rate\nRunning: 300s\nThreads: $threads" 139 | plot "$datfile" using 4:xticlabels(1) w lp pt 7 ps 1 lc rgbcolor "#F62817" t "Success Rate" 140 | EOF 141 | } 142 | 143 | function plotDiff() 144 | { 145 | gnuplot << EOF 146 | set terminal png enhanced #输出格式为png文件 147 | set output "${t1}_$t2.qps.diff.png" #指定数据文件名称 148 | set title "QPS & TTLB\nRunning: 300s\nThreads: $threads" 149 | set xlabel 'Concurrent' 150 | set ylabel 'QPS' 151 | set y2label 'Average Latency (ms)' 152 | set key below left vertical noreverse spacing 1.2 box autotitle columnheader 153 | set tics out nomirror 154 | set border 3 front 155 | set style line 1 linecolor rgb '#00ff00' linewidth 2 linetype 3 pointtype 2 156 | set style line 2 linecolor rgb '#ff0000' linewidth 1 linetype 3 pointtype 2 157 | set style data linespoints 158 | 159 | #set border 3 lt 3 lw 2 #这会让你的坐标图的border更好看 160 | set grid #显示网格 161 | set xtics nomirror rotate #by 90#只需要一个x轴 162 | set mxtics 5 163 | set mytics 5 #可以增加分刻度 164 | set ytics nomirror 165 | set y2tics 166 | 167 | #set pointsize 0.4 #点的像素大小 168 | #set datafile separator '\t' #数据文件的字段用\t分开 169 | 170 | set autoscale y 171 | set autoscale y2 172 | 173 | #设置图像的大小 为标准大小的2倍 174 | #set size 2.3,2 175 | 176 | plot "/tmp/plot_diff.dat" using 2:xticlabels(1) w lp pt 7 ps 1 lc rgbcolor "#EE0000" axis x1y1 t "$t1 QPS","/tmp/plot_diff.dat" using 5:xticlabels(1) w lp pt 7 ps 1 lc rgbcolor "#EE82EE" axis x1y1 t "$t2 QPS","/tmp/plot_diff.dat" using 3:xticlabels(1) w lp pt 5 ps 1 lc rgbcolor "#0000CD" axis x2y2 t "$t1 Avg Latency (ms)", "/tmp/plot_diff.dat" using 6:xticlabels(1) w lp pt 5 ps 1 lc rgbcolor "#6495ED" axis x2y2 t "$t2 Avg Latency (ms)" 177 | 178 | unset y2tics 179 | unset y2label 180 | set ytics nomirror 181 | set yrange[0:100] 182 | set title "Success Rate\nRunning: 300s\nThreads: $threads" 183 | set output "${t1}_$t2.success_rate.diff.png" #指定数据文件名称 184 | plot "/tmp/plot_diff.dat" using 4:xticlabels(1) w lp pt 7 ps 1 lc rgbcolor "#EE0000" t "$t1 Success Rate","/tmp/plot_diff.dat" using 7:xticlabels(1) w lp pt 7 ps 1 lc rgbcolor "#EE82EE" t "$t2 Success Rate" 185 | EOF 186 | } 187 | 188 | if [ "$1" == "diff" ];then 189 | join $2 $3 > /tmp/plot_diff.dat 190 | plotDiff `basename $2` `basename $3` 191 | exit 0 192 | fi 193 | 194 | 195 | prepare 196 | 197 | for c in $concurrent 198 | do 199 | wrkcmd="$cmd -c $c" 200 | echo -e "\nRunning wrk command: $wrkcmd" 201 | result=`eval $wrkcmd` 202 | convertPlotData "$result" 203 | done 204 | 205 | echo -e "\nNow plot according to $datfile" 206 | plot &> /dev/null 207 | echo -e "QPS graphic file is: $apiperformance\nSuccess rate graphic file is: $apisuccessrate" 208 | --------------------------------------------------------------------------------