mirror of https://github.com/zhufuyi/sponge
init
This commit is contained in:
parent
1dab814b5b
commit
083a0499c1
|
@ -4,6 +4,7 @@
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
cmd/sponge/sponge
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
@ -12,4 +13,10 @@
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
vendor/
|
||||||
|
|
||||||
|
# idea
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
|
@ -0,0 +1,298 @@
|
||||||
|
# This file configures github.com/zhufuyi/pkg.
|
||||||
|
|
||||||
|
run:
|
||||||
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
|
timeout: 2m
|
||||||
|
# default concurrency is available CPU number
|
||||||
|
concurrency: 4
|
||||||
|
# include test files or not, default is true
|
||||||
|
tests: false
|
||||||
|
# which dirs to skip: issues from them won't be reported;
|
||||||
|
# can use regexp here: generated.*, regexp is applied on full path;
|
||||||
|
# default value is empty list, but default dirs are skipped independently
|
||||||
|
# from this option's value (see skip-dirs-use-default).
|
||||||
|
skip-dirs:
|
||||||
|
- pkg
|
||||||
|
- docs
|
||||||
|
# which files to skip: they will be analyzed, but issues from them
|
||||||
|
# won't be reported. Default value is empty list, but there is
|
||||||
|
# no need to include all autogenerated files, we confidently recognize
|
||||||
|
# autogenerated files. If it's not please let us know.
|
||||||
|
skip-files:
|
||||||
|
- _test.go
|
||||||
|
|
||||||
|
# exit code when at least one issue was found, default is 1
|
||||||
|
issues-exit-code: 1
|
||||||
|
|
||||||
|
# list of build tags, all linters use it. Default is empty list.
|
||||||
|
build-tags:
|
||||||
|
- mytag
|
||||||
|
|
||||||
|
# default is true. Enables skipping of directories:
|
||||||
|
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||||
|
skip-dirs-use-default: true
|
||||||
|
|
||||||
|
|
||||||
|
linters:
|
||||||
|
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
||||||
|
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
#- bodyclose
|
||||||
|
- deadcode
|
||||||
|
- depguard
|
||||||
|
- dogsled
|
||||||
|
#- dupl
|
||||||
|
- errcheck
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
- gocyclo
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- golint
|
||||||
|
#- goprintffuncname
|
||||||
|
#- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
#- ineffassign
|
||||||
|
- interfacer
|
||||||
|
- lll
|
||||||
|
- misspell
|
||||||
|
- rowserrcheck
|
||||||
|
- structcheck
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
#- unparam
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- whitespace
|
||||||
|
- staticcheck
|
||||||
|
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
dogsled:
|
||||||
|
# checks assignments with too many blank identifiers; default is 2
|
||||||
|
max-blank-identifiers: 2
|
||||||
|
|
||||||
|
dupl:
|
||||||
|
# tokens count to trigger issue, 150 by default
|
||||||
|
threshold: 100
|
||||||
|
|
||||||
|
errcheck:
|
||||||
|
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
|
||||||
|
# default is false: such cases aren't reported by default.
|
||||||
|
check-type-assertions: false
|
||||||
|
|
||||||
|
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||||
|
# default is false: such cases aren't reported by default.
|
||||||
|
check-blank: false
|
||||||
|
|
||||||
|
# [deprecated] comma-separated list of pairs of the form pkg:regex
|
||||||
|
# the regex is used to ignore names within pkg. (default "fmt:.*").
|
||||||
|
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
|
||||||
|
ignore: fmt:.*,io/ioutil:^Read.*
|
||||||
|
|
||||||
|
# path to a file containing a list of functions to exclude from checking
|
||||||
|
# see https://github.com/kisielk/errcheck#excluding-functions for details
|
||||||
|
# exclude: /path/to/file.txt
|
||||||
|
funlen:
|
||||||
|
lines: 60
|
||||||
|
statements: 40
|
||||||
|
|
||||||
|
gocognit:
|
||||||
|
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||||
|
min-complexity: 10
|
||||||
|
|
||||||
|
goconst:
|
||||||
|
# minimal length of string constant, 3 by default
|
||||||
|
min-len: 3
|
||||||
|
# minimal occurrences count to trigger, 3 by default
|
||||||
|
min-occurrences: 3
|
||||||
|
|
||||||
|
gocyclo:
|
||||||
|
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||||
|
min-complexity: 20
|
||||||
|
|
||||||
|
godox:
|
||||||
|
# report any comments starting with keywords, this is useful for TODO or FIXME comments that
|
||||||
|
# might be left in the code accidentally and should be resolved before merging
|
||||||
|
keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting
|
||||||
|
- NOTE
|
||||||
|
- OPTIMIZE # marks code that should be optimized before merging
|
||||||
|
- HACK # marks hack-arounds that should be removed before merging
|
||||||
|
|
||||||
|
gofmt:
|
||||||
|
# simplify code: gofmt with `-s` option, true by default
|
||||||
|
simplify: true
|
||||||
|
|
||||||
|
goimports:
|
||||||
|
# put imports beginning with prefix after 3rd-party packages;
|
||||||
|
# it's a comma-separated list of prefixes
|
||||||
|
local-prefixes: sponge
|
||||||
|
|
||||||
|
golint:
|
||||||
|
# minimal confidence for issues, default is 0.8
|
||||||
|
min-confidence: 0.8
|
||||||
|
|
||||||
|
gomnd:
|
||||||
|
settings:
|
||||||
|
mnd:
|
||||||
|
# the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description.
|
||||||
|
checks: argument,case,condition,operation,return,assign
|
||||||
|
|
||||||
|
govet:
|
||||||
|
# report about shadowed variables
|
||||||
|
check-shadowing: true
|
||||||
|
|
||||||
|
# settings per analyzer
|
||||||
|
settings:
|
||||||
|
printf: # analyzer name, run `go tool vet help` to see all analyzers
|
||||||
|
funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
|
||||||
|
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
|
||||||
|
|
||||||
|
# enable or disable analyzers by name
|
||||||
|
enable:
|
||||||
|
- atomicalign
|
||||||
|
enable-all: false
|
||||||
|
disable:
|
||||||
|
- shadow
|
||||||
|
disable-all: false
|
||||||
|
|
||||||
|
depguard:
|
||||||
|
list-type: blacklist
|
||||||
|
include-go-root: false
|
||||||
|
#packages:
|
||||||
|
# - github.com/user/name
|
||||||
|
#packages-with-error-message:
|
||||||
|
# specify an error message to output when a blacklisted package is used
|
||||||
|
# - github.com/user/name: "logging is allowed only by logutils.Log"
|
||||||
|
|
||||||
|
lll:
|
||||||
|
# max line length, lines longer will be reported. Default is 120.
|
||||||
|
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
||||||
|
line-length: 200
|
||||||
|
# tab width in spaces. Default to 1.
|
||||||
|
tab-width: 1
|
||||||
|
|
||||||
|
maligned:
|
||||||
|
# print struct with more effective memory layout or not, false by default
|
||||||
|
suggest-new: true
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
# Correct spellings using locale preferences for US or UK.
|
||||||
|
# Default is to use a neutral variety of English.
|
||||||
|
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||||
|
locale: US
|
||||||
|
ignore-words:
|
||||||
|
- someword
|
||||||
|
|
||||||
|
nakedret:
|
||||||
|
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
|
||||||
|
max-func-lines: 30
|
||||||
|
|
||||||
|
prealloc:
|
||||||
|
# XXX: we don't recommend using this linter before doing performance profiling.
|
||||||
|
# For most programs usage of prealloc will be a premature optimization.
|
||||||
|
|
||||||
|
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
|
||||||
|
# True by default.
|
||||||
|
simple: true
|
||||||
|
range-loops: true # Report preallocation suggestions on range loops, true by default
|
||||||
|
for-loops: false # Report preallocation suggestions on for loops, false by default
|
||||||
|
|
||||||
|
#rowserrcheck:
|
||||||
|
# packages:
|
||||||
|
# - github.com/user/name
|
||||||
|
|
||||||
|
unparam:
|
||||||
|
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
|
||||||
|
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
|
||||||
|
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
|
||||||
|
# with golangci-lint call it on a directory with the changed file.
|
||||||
|
check-exported: false
|
||||||
|
|
||||||
|
unused:
|
||||||
|
# treat code as a program (not a library) and report unused exported identifiers; default is false.
|
||||||
|
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
|
||||||
|
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
|
||||||
|
# with golangci-lint call it on a directory with the changed file.
|
||||||
|
check-exported: false
|
||||||
|
|
||||||
|
whitespace:
|
||||||
|
multi-if: false # Enforces newlines (or comments) after every multi-line if statement
|
||||||
|
multi-func: false # Enforces newlines (or comments) after every multi-line function signature
|
||||||
|
|
||||||
|
wsl:
|
||||||
|
# If true append is only allowed to be cuddled if appending value is
|
||||||
|
# matching variables, fields or types on line above. Default is true.
|
||||||
|
strict-append: true
|
||||||
|
# Allow calls and assignments to be cuddled as long as the lines have any
|
||||||
|
# matching variables, fields or types. Default is true.
|
||||||
|
allow-assign-and-call: true
|
||||||
|
# Allow multiline assignments to be cuddled. Default is true.
|
||||||
|
allow-multiline-assign: true
|
||||||
|
# Allow declarations (var) to be cuddled.
|
||||||
|
allow-cuddle-declarations: false
|
||||||
|
# Allow trailing comments in ending of blocks
|
||||||
|
allow-trailing-comment: false
|
||||||
|
# Force newlines in end of case at this limit (0 = never).
|
||||||
|
force-case-trailing-whitespace: 0
|
||||||
|
|
||||||
|
issues:
|
||||||
|
# List of regexps of issue texts to exclude, empty list by default.
|
||||||
|
# But independently from this option we use default exclude patterns,
|
||||||
|
# it can be disabled by `exclude-use-default: false`. To list all
|
||||||
|
# excluded by default patterns execute `golangci-lint run --help`
|
||||||
|
exclude:
|
||||||
|
- abcdef
|
||||||
|
|
||||||
|
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||||
|
exclude-rules:
|
||||||
|
# Exclude some linters from running on tests files.
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- gocyclo
|
||||||
|
- errcheck
|
||||||
|
- dupl
|
||||||
|
- gosec
|
||||||
|
|
||||||
|
# Exclude known linters from partially hard-vendored code,
|
||||||
|
# which is impossible to exclude via "nolint" comments.
|
||||||
|
- path: internal/hmac/
|
||||||
|
text: "weak cryptographic primitive"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
|
||||||
|
# Exclude lll issues for long lines with go:generate
|
||||||
|
- linters:
|
||||||
|
- lll
|
||||||
|
source: "^//go:generate "
|
||||||
|
|
||||||
|
# Independently from option `exclude` we use default exclude patterns,
|
||||||
|
# it can be disabled by this option. To list all
|
||||||
|
# excluded by default patterns execute `golangci-lint run --help`.
|
||||||
|
# Default value for this option is true.
|
||||||
|
exclude-use-default: false
|
||||||
|
|
||||||
|
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
|
||||||
|
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||||
|
max-same-issues: 0
|
||||||
|
|
||||||
|
# Show only new issues: if there are unstaged changes or untracked files,
|
||||||
|
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
|
||||||
|
# It's a super-useful option for integration of golangci-lint into existing
|
||||||
|
# large codebase. It's not practical to fix all existing issues at the moment
|
||||||
|
# of integration: much better don't allow issues in new code.
|
||||||
|
# Default is false.
|
||||||
|
new: false
|
||||||
|
|
||||||
|
# Show only new issues created after git revision `REV`
|
||||||
|
new-from-rev: ""
|
||||||
|
|
||||||
|
service:
|
||||||
|
golangci-lint-version: 1.48.0 # use the fixed version to not introduce new linters unexpectedly
|
|
@ -0,0 +1,140 @@
|
||||||
|
SHELL := /bin/bash
|
||||||
|
|
||||||
|
PROJECT_NAME := "github.com/zhufuyi/sponge"
|
||||||
|
PKG := "$(PROJECT_NAME)"
|
||||||
|
PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/)
|
||||||
|
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: init
|
||||||
|
# init env
|
||||||
|
init:
|
||||||
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.0
|
||||||
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0
|
||||||
|
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.10.0
|
||||||
|
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.10.0
|
||||||
|
go install github.com/envoyproxy/protoc-gen-validate@v0.6.7
|
||||||
|
go install github.com/mohuishou/protoc-gen-go-gin@v0.1.0
|
||||||
|
go install github.com/srikrsna/protoc-gen-gotag@v0.6.2
|
||||||
|
go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@v1.5.1
|
||||||
|
go install github.com/golang/mock/mockgen@v1.6.0
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.49.0
|
||||||
|
go install github.com/swaggo/swag/cmd/swag@v1.8.6
|
||||||
|
go install github.com/ofabry/go-callvis@v0.6.1
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: ci-lint
|
||||||
|
# make ci-lint
|
||||||
|
ci-lint:
|
||||||
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
# make build, Build the binary file
|
||||||
|
build:
|
||||||
|
@cd cmd/sponge && go build
|
||||||
|
@echo "build finished, binary file in path 'cmd/sponge'"
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
# make run, run app
|
||||||
|
run:
|
||||||
|
@bash scripts/run.sh
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: dep
|
||||||
|
# make dep Get the dependencies
|
||||||
|
dep:
|
||||||
|
@go mod download
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
# make fmt
|
||||||
|
fmt:
|
||||||
|
@gofmt -s -w .
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
# make test
|
||||||
|
test:
|
||||||
|
go test -cover ./... | grep -v vendor;true
|
||||||
|
go vet ./... | grep -v vendor;true
|
||||||
|
go test -short ${PKG_LIST}
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: cover
|
||||||
|
# make cover
|
||||||
|
cover:
|
||||||
|
go test -short -coverprofile coverage.txt -covermode=atomic ${PKG_LIST}
|
||||||
|
go tool cover -html=coverage.txt
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: docker
|
||||||
|
# generate docker image
|
||||||
|
docker:
|
||||||
|
docker build -t sponge:latest -f Dockerfile .
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
# make clean
|
||||||
|
clean:
|
||||||
|
@-rm -vrf sponge
|
||||||
|
@-rm -vrf cover.out
|
||||||
|
@-rm -vrf coverage.txt
|
||||||
|
@go mod tidy
|
||||||
|
@echo "clean finished"
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: docs
|
||||||
|
# gen swagger doc
|
||||||
|
docs:
|
||||||
|
@swag init -g cmd/sponge/main.go
|
||||||
|
@echo "see docs by: http://localhost:8080/swagger/index.html"
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: graph
|
||||||
|
# make graph 生成交互式的可视化Go程序调用图,生成完毕后会在浏览器自动打开
|
||||||
|
graph:
|
||||||
|
@echo "generating graph ......"
|
||||||
|
@go-callvis github.com/zhufuyi/sponge
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: mockgen
|
||||||
|
# make mockgen gen mock file
|
||||||
|
mockgen:
|
||||||
|
cd ./internal && for file in `egrep -rnl "type.*?interface" ./repository | grep -v "_test" `; do \
|
||||||
|
echo $$file ; \
|
||||||
|
cd .. && mockgen -destination="./internal/mock/$$file" -source="./internal/$$file" && cd ./internal ; \
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: proto
|
||||||
|
# generate proto struct only
|
||||||
|
proto:
|
||||||
|
@bash scripts/protoc.sh
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: proto-doc
|
||||||
|
# generate proto doc
|
||||||
|
proto-doc:
|
||||||
|
@bash scripts/proto-doc.sh
|
||||||
|
|
||||||
|
|
||||||
|
# show help
|
||||||
|
help:
|
||||||
|
@echo ''
|
||||||
|
@echo 'Usage:'
|
||||||
|
@echo ' make [target]'
|
||||||
|
@echo ''
|
||||||
|
@echo 'Targets:'
|
||||||
|
@awk '/^[a-zA-Z\-\_0-9]+:/ { \
|
||||||
|
helpMessage = match(lastLine, /^# (.*)/); \
|
||||||
|
if (helpMessage) { \
|
||||||
|
helpCommand = substr($$1, 0, index($$1, ":")-1); \
|
||||||
|
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
|
||||||
|
printf "\033[36m %-22s\033[0m %s\n", helpCommand,helpMessage; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
{ lastLine = $$0 }' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := all
|
67
README.md
67
README.md
|
@ -1,2 +1,65 @@
|
||||||
# sponge
|
## sponge
|
||||||
microservice framework.
|
|
||||||
|
sponge 是一个微服务框架,支持http和grpc及服务治理,结合[goctl](https://github.com/zhufuyi/goctl)工具自动生成框架代码。
|
||||||
|
|
||||||
|
功能:
|
||||||
|
|
||||||
|
- web框架 [gin](https://github.com/gin-gonic/gin)
|
||||||
|
- rpc框架 [grpc](https://github.com/grpc/grpc-go)
|
||||||
|
- 配置文件解析 [viper](https://github.com/spf13/viper)
|
||||||
|
- 日志 [zap](go.uber.org/zap)
|
||||||
|
- 数据库组件 [gorm](gorm.io/gorm)
|
||||||
|
- 缓存组件 [go-redis](github.com/go-redis/redis)
|
||||||
|
- 生成文档 [swagger](github.com/swaggo/swag)
|
||||||
|
- 校验器 [validator](github.com/go-playground/validator)
|
||||||
|
- 链路跟踪 [opentelemetry](go.opentelemetry.io/otel)
|
||||||
|
- 指标采集 [prometheus](github.com/prometheus/client_golang/prometheus)
|
||||||
|
- 限流 [ratelimiter](golang.org/x/time/rate)
|
||||||
|
- 熔断 [hystrix](github.com/afex/hystrix-go)
|
||||||
|
- 包管理工具 [go modules](https://github.com/golang/go/wiki/Modules)
|
||||||
|
- 性能分析 [go profile](https://go.dev/blog/pprof)
|
||||||
|
- 代码检测 [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
|
||||||
|
目录结构遵循[golang-standards/project-layout](https://github.com/golang-standards/project-layout)。
|
||||||
|
|
||||||
|
```
|
||||||
|
├── cmd # 应用程序的目录
|
||||||
|
├── config # 配置文件目录
|
||||||
|
├── docs # 设计和用户文档
|
||||||
|
├── internal # 私有应用程序和库代码
|
||||||
|
│ ├── cache # 基于业务封装的cache
|
||||||
|
│ ├── dao # 数据访问
|
||||||
|
│ ├── ecode # 自定义业务错误码
|
||||||
|
│ ├── handler # http的业务功能实现
|
||||||
|
│ ├── model # 数据库 model
|
||||||
|
│ ├── routers # http 路由
|
||||||
|
│ ├── server # 服务入口,包括http和grpc服务入口
|
||||||
|
│ └── service # grpc的业务功能实现
|
||||||
|
├── pkg # 外部应用程序可以使用的库代码
|
||||||
|
├── scripts # 存放用于执行各种构建,安装,分析等操作的脚本
|
||||||
|
├── third_party # 外部辅助工具,分叉代码和其他第三方工具
|
||||||
|
├── test # 额外的外部测试应用程序和测试数据
|
||||||
|
├── build # 打包和持续集成
|
||||||
|
└── deployments # IaaS、PaaS、系统和容器编排部署配置和模板
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
### 运行
|
||||||
|
|
||||||
|
根据配置文件设置是否开启服务治理功能,例如链路跟踪、指标采集、限流、性能分析。
|
||||||
|
|
||||||
|
启动web服务
|
||||||
|
|
||||||
|
> run.sh
|
||||||
|
|
||||||
|
在浏览器打开 `http://ip:port/swagger/index.html` 可以看到接口文档,可以把swagger的json文档导入 [yapi](https://github.com/YMFE/yapi) 测试。
|
||||||
|
|
||||||
|
注:每次更新都要执行swag init,并且重启服务生效。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
|
@ -0,0 +1,263 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.28.0
|
||||||
|
// protoc v3.20.1
|
||||||
|
// source: api/types/types.proto
|
||||||
|
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // 列名
|
||||||
|
Exp string `protobuf:"bytes,2,opt,name=exp,proto3" json:"exp,omitempty"` // 表达式,值为空时默认为=,有=、!=、>、>=、<、<=、like七种类型
|
||||||
|
Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` // 列值
|
||||||
|
Logic string `protobuf:"bytes,4,opt,name=logic,proto3" json:"logic,omitempty"` // 逻辑类型,值为空时默认为and,有&(and)、||(or)两种类型
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Column) Reset() {
|
||||||
|
*x = Column{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_api_types_types_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Column) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Column) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Column) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_api_types_types_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Column.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Column) Descriptor() ([]byte, []int) {
|
||||||
|
return file_api_types_types_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Column) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Column) GetExp() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Exp
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Column) GetValue() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Column) GetLogic() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Logic
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` // 页码,从0开始
|
||||||
|
Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` // 每页行数
|
||||||
|
Sort string `protobuf:"bytes,3,opt,name=sort,proto3" json:"sort,omitempty"` // 排序字段,多列排序用逗号分隔
|
||||||
|
Columns []*Column `protobuf:"bytes,4,rep,name=columns,proto3" json:"columns,omitempty"` // 查询条件
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Params) Reset() {
|
||||||
|
*x = Params{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_api_types_types_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Params) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Params) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Params) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_api_types_types_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Params.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Params) Descriptor() ([]byte, []int) {
|
||||||
|
return file_api_types_types_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Params) GetPage() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Page
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Params) GetLimit() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Limit
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Params) GetSort() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Sort
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Params) GetColumns() []*Column {
|
||||||
|
if x != nil {
|
||||||
|
return x.Columns
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_api_types_types_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_api_types_types_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x15, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x74, 0x79, 0x70, 0x65,
|
||||||
|
0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, 0x5a,
|
||||||
|
0x0a, 0x06, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||||
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03,
|
||||||
|
0x65, 0x78, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x78, 0x70, 0x12, 0x14,
|
||||||
|
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76,
|
||||||
|
0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x18, 0x04, 0x20,
|
||||||
|
0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x22, 0x6f, 0x0a, 0x06, 0x50, 0x61,
|
||||||
|
0x72, 0x61, 0x6d, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||||
|
0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69,
|
||||||
|
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x12,
|
||||||
|
0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6f,
|
||||||
|
0x72, 0x74, 0x12, 0x27, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x04, 0x20,
|
||||||
|
0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x6c, 0x75,
|
||||||
|
0x6d, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x42, 0x2b, 0x5a, 0x29, 0x67,
|
||||||
|
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x7a, 0x68, 0x75, 0x66, 0x75, 0x79,
|
||||||
|
0x69, 0x2f, 0x73, 0x70, 0x6f, 0x6e, 0x67, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70,
|
||||||
|
0x65, 0x73, 0x3b, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_api_types_types_proto_rawDescOnce sync.Once
|
||||||
|
file_api_types_types_proto_rawDescData = file_api_types_types_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_api_types_types_proto_rawDescGZIP() []byte {
|
||||||
|
file_api_types_types_proto_rawDescOnce.Do(func() {
|
||||||
|
file_api_types_types_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_types_types_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_api_types_types_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_api_types_types_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
|
var file_api_types_types_proto_goTypes = []interface{}{
|
||||||
|
(*Column)(nil), // 0: types.Column
|
||||||
|
(*Params)(nil), // 1: types.Params
|
||||||
|
}
|
||||||
|
var file_api_types_types_proto_depIdxs = []int32{
|
||||||
|
0, // 0: types.Params.columns:type_name -> types.Column
|
||||||
|
1, // [1:1] is the sub-list for method output_type
|
||||||
|
1, // [1:1] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_api_types_types_proto_init() }
|
||||||
|
func file_api_types_types_proto_init() {
|
||||||
|
if File_api_types_types_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_api_types_types_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*Column); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_api_types_types_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*Params); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_api_types_types_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 2,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 0,
|
||||||
|
},
|
||||||
|
GoTypes: file_api_types_types_proto_goTypes,
|
||||||
|
DependencyIndexes: file_api_types_types_proto_depIdxs,
|
||||||
|
MessageInfos: file_api_types_types_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_api_types_types_proto = out.File
|
||||||
|
file_api_types_types_proto_rawDesc = nil
|
||||||
|
file_api_types_types_proto_goTypes = nil
|
||||||
|
file_api_types_types_proto_depIdxs = nil
|
||||||
|
}
|
|
@ -0,0 +1,280 @@
|
||||||
|
// Code generated by protoc-gen-validate. DO NOT EDIT.
|
||||||
|
// source: api/types/types.proto
|
||||||
|
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ensure the imports are used
|
||||||
|
var (
|
||||||
|
_ = bytes.MinRead
|
||||||
|
_ = errors.New("")
|
||||||
|
_ = fmt.Print
|
||||||
|
_ = utf8.UTFMax
|
||||||
|
_ = (*regexp.Regexp)(nil)
|
||||||
|
_ = (*strings.Reader)(nil)
|
||||||
|
_ = net.IPv4len
|
||||||
|
_ = time.Duration(0)
|
||||||
|
_ = (*url.URL)(nil)
|
||||||
|
_ = (*mail.Address)(nil)
|
||||||
|
_ = anypb.Any{}
|
||||||
|
_ = sort.Sort
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate checks the field values on Column with the rules defined in the
|
||||||
|
// proto definition for this message. If any rules are violated, the first
|
||||||
|
// error encountered is returned, or nil if there are no violations.
|
||||||
|
func (m *Column) Validate() error {
|
||||||
|
return m.validate(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAll checks the field values on Column with the rules defined in the
|
||||||
|
// proto definition for this message. If any rules are violated, the result is
|
||||||
|
// a list of violation errors wrapped in ColumnMultiError, or nil if none found.
|
||||||
|
func (m *Column) ValidateAll() error {
|
||||||
|
return m.validate(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Column) validate(all bool) error {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors []error
|
||||||
|
|
||||||
|
// no validation rules for Name
|
||||||
|
|
||||||
|
// no validation rules for Exp
|
||||||
|
|
||||||
|
// no validation rules for Value
|
||||||
|
|
||||||
|
// no validation rules for Logic
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return ColumnMultiError(errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnMultiError is an error wrapping multiple validation errors returned by
|
||||||
|
// Column.ValidateAll() if the designated constraints aren't met.
|
||||||
|
type ColumnMultiError []error
|
||||||
|
|
||||||
|
// Error returns a concatenation of all the error messages it wraps.
|
||||||
|
func (m ColumnMultiError) Error() string {
|
||||||
|
var msgs []string
|
||||||
|
for _, err := range m {
|
||||||
|
msgs = append(msgs, err.Error())
|
||||||
|
}
|
||||||
|
return strings.Join(msgs, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllErrors returns a list of validation violation errors.
|
||||||
|
func (m ColumnMultiError) AllErrors() []error { return m }
|
||||||
|
|
||||||
|
// ColumnValidationError is the validation error returned by Column.Validate if
|
||||||
|
// the designated constraints aren't met.
|
||||||
|
type ColumnValidationError struct {
|
||||||
|
field string
|
||||||
|
reason string
|
||||||
|
cause error
|
||||||
|
key bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field function returns field value.
|
||||||
|
func (e ColumnValidationError) Field() string { return e.field }
|
||||||
|
|
||||||
|
// Reason function returns reason value.
|
||||||
|
func (e ColumnValidationError) Reason() string { return e.reason }
|
||||||
|
|
||||||
|
// Cause function returns cause value.
|
||||||
|
func (e ColumnValidationError) Cause() error { return e.cause }
|
||||||
|
|
||||||
|
// Key function returns key value.
|
||||||
|
func (e ColumnValidationError) Key() bool { return e.key }
|
||||||
|
|
||||||
|
// ErrorName returns error name.
|
||||||
|
func (e ColumnValidationError) ErrorName() string { return "ColumnValidationError" }
|
||||||
|
|
||||||
|
// Error satisfies the builtin error interface
|
||||||
|
func (e ColumnValidationError) Error() string {
|
||||||
|
cause := ""
|
||||||
|
if e.cause != nil {
|
||||||
|
cause = fmt.Sprintf(" | caused by: %v", e.cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := ""
|
||||||
|
if e.key {
|
||||||
|
key = "key for "
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"invalid %sColumn.%s: %s%s",
|
||||||
|
key,
|
||||||
|
e.field,
|
||||||
|
e.reason,
|
||||||
|
cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ error = ColumnValidationError{}
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
Field() string
|
||||||
|
Reason() string
|
||||||
|
Key() bool
|
||||||
|
Cause() error
|
||||||
|
ErrorName() string
|
||||||
|
} = ColumnValidationError{}
|
||||||
|
|
||||||
|
// Validate checks the field values on Params with the rules defined in the
|
||||||
|
// proto definition for this message. If any rules are violated, the first
|
||||||
|
// error encountered is returned, or nil if there are no violations.
|
||||||
|
func (m *Params) Validate() error {
|
||||||
|
return m.validate(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAll checks the field values on Params with the rules defined in the
|
||||||
|
// proto definition for this message. If any rules are violated, the result is
|
||||||
|
// a list of violation errors wrapped in ParamsMultiError, or nil if none found.
|
||||||
|
func (m *Params) ValidateAll() error {
|
||||||
|
return m.validate(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Params) validate(all bool) error {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors []error
|
||||||
|
|
||||||
|
// no validation rules for Page
|
||||||
|
|
||||||
|
// no validation rules for Limit
|
||||||
|
|
||||||
|
// no validation rules for Sort
|
||||||
|
|
||||||
|
for idx, item := range m.GetColumns() {
|
||||||
|
_, _ = idx, item
|
||||||
|
|
||||||
|
if all {
|
||||||
|
switch v := interface{}(item).(type) {
|
||||||
|
case interface{ ValidateAll() error }:
|
||||||
|
if err := v.ValidateAll(); err != nil {
|
||||||
|
errors = append(errors, ParamsValidationError{
|
||||||
|
field: fmt.Sprintf("Columns[%v]", idx),
|
||||||
|
reason: "embedded message failed validation",
|
||||||
|
cause: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case interface{ Validate() error }:
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
errors = append(errors, ParamsValidationError{
|
||||||
|
field: fmt.Sprintf("Columns[%v]", idx),
|
||||||
|
reason: "embedded message failed validation",
|
||||||
|
cause: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v, ok := interface{}(item).(interface{ Validate() error }); ok {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return ParamsValidationError{
|
||||||
|
field: fmt.Sprintf("Columns[%v]", idx),
|
||||||
|
reason: "embedded message failed validation",
|
||||||
|
cause: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return ParamsMultiError(errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParamsMultiError is an error wrapping multiple validation errors returned by
|
||||||
|
// Params.ValidateAll() if the designated constraints aren't met.
|
||||||
|
type ParamsMultiError []error
|
||||||
|
|
||||||
|
// Error returns a concatenation of all the error messages it wraps.
|
||||||
|
func (m ParamsMultiError) Error() string {
|
||||||
|
var msgs []string
|
||||||
|
for _, err := range m {
|
||||||
|
msgs = append(msgs, err.Error())
|
||||||
|
}
|
||||||
|
return strings.Join(msgs, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllErrors returns a list of validation violation errors.
|
||||||
|
func (m ParamsMultiError) AllErrors() []error { return m }
|
||||||
|
|
||||||
|
// ParamsValidationError is the validation error returned by Params.Validate if
|
||||||
|
// the designated constraints aren't met.
|
||||||
|
type ParamsValidationError struct {
|
||||||
|
field string
|
||||||
|
reason string
|
||||||
|
cause error
|
||||||
|
key bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field function returns field value.
|
||||||
|
func (e ParamsValidationError) Field() string { return e.field }
|
||||||
|
|
||||||
|
// Reason function returns reason value.
|
||||||
|
func (e ParamsValidationError) Reason() string { return e.reason }
|
||||||
|
|
||||||
|
// Cause function returns cause value.
|
||||||
|
func (e ParamsValidationError) Cause() error { return e.cause }
|
||||||
|
|
||||||
|
// Key function returns key value.
|
||||||
|
func (e ParamsValidationError) Key() bool { return e.key }
|
||||||
|
|
||||||
|
// ErrorName returns error name.
|
||||||
|
func (e ParamsValidationError) ErrorName() string { return "ParamsValidationError" }
|
||||||
|
|
||||||
|
// Error satisfies the builtin error interface
|
||||||
|
func (e ParamsValidationError) Error() string {
|
||||||
|
cause := ""
|
||||||
|
if e.cause != nil {
|
||||||
|
cause = fmt.Sprintf(" | caused by: %v", e.cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := ""
|
||||||
|
if e.key {
|
||||||
|
key = "key for "
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"invalid %sParams.%s: %s%s",
|
||||||
|
key,
|
||||||
|
e.field,
|
||||||
|
e.reason,
|
||||||
|
cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ error = ParamsValidationError{}
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
Field() string
|
||||||
|
Reason() string
|
||||||
|
Key() bool
|
||||||
|
Cause() error
|
||||||
|
ErrorName() string
|
||||||
|
} = ParamsValidationError{}
|
|
@ -0,0 +1,19 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package types;
|
||||||
|
|
||||||
|
option go_package = "github.com/zhufuyi/sponge/api/types;types";
|
||||||
|
|
||||||
|
message Column {
|
||||||
|
string name=1; // 列名
|
||||||
|
string exp=2; // 表达式,值为空时默认为=,有=、!=、>、>=、<、<=、like七种类型
|
||||||
|
string value=3; // 列值
|
||||||
|
string logic=4; // 逻辑类型,值为空时默认为and,有&(and)、||(or)两种类型
|
||||||
|
}
|
||||||
|
|
||||||
|
message Params {
|
||||||
|
int32 page = 1; // 页码,从0开始
|
||||||
|
int32 limit = 2; // 每页行数
|
||||||
|
string sort = 3; // 排序字段,多列排序用逗号分隔
|
||||||
|
repeated Column columns = 4; // 查询条件
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,97 @@
|
||||||
|
// todo generate the protobuf code here
|
||||||
|
// delete the templates code start
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package api.userExample.v1;
|
||||||
|
|
||||||
|
import "validate/validate.proto";
|
||||||
|
import "api/types/types.proto";
|
||||||
|
|
||||||
|
option go_package = "github.com/zhufuyi/sponge/api/userExample/v1;v1";
|
||||||
|
|
||||||
|
service userExampleService {
|
||||||
|
rpc Create(CreateUserExampleRequest) returns (CreateUserExampleReply) {}
|
||||||
|
rpc DeleteByID(DeleteUserExampleByIDRequest) returns (DeleteUserExampleByIDReply) {}
|
||||||
|
rpc UpdateByID(UpdateUserExampleByIDRequest) returns (UpdateUserExampleByIDReply) {}
|
||||||
|
rpc GetByID(GetUserExampleByIDRequest) returns (GetUserExampleByIDReply) {}
|
||||||
|
rpc List(ListUserExampleRequest) returns (ListUserExampleReply) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GenderType {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
MALE = 1;
|
||||||
|
FEMALE = 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
message CreateUserExampleRequest {
|
||||||
|
string name = 1 [(validate.rules).string.min_len = 2]; // 名称
|
||||||
|
string email = 2 [(validate.rules).string.email = true]; // 邮件
|
||||||
|
string password = 3 [(validate.rules).string.min_len = 10]; // 密码
|
||||||
|
string phone=4 [(validate.rules).string = {pattern: "^1[3456789]\\d{9}$"}]; // 手机号码
|
||||||
|
string avatar=5 [(validate.rules).string.uri = true]; // 头像
|
||||||
|
int32 age=6 [(validate.rules).int32 = {gte:0, lte: 120}]; // 年龄
|
||||||
|
GenderType gender=7 [(validate.rules).enum.defined_only = true]; // 性别,1:男,2:女
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateUserExampleReply {
|
||||||
|
uint64 id = 1 [(validate.rules).uint64.gte = 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteUserExampleByIDRequest {
|
||||||
|
uint64 id = 1 [(validate.rules).uint64.gte = 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteUserExampleByIDReply {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserExampleByIDRequest {
|
||||||
|
uint64 id = 1 [(validate.rules).uint64.gte = 1];
|
||||||
|
string name = 2; // 名称
|
||||||
|
string email = 3; // 邮件
|
||||||
|
string password = 4; // 密码
|
||||||
|
string phone=5; // 手机号码,必须在前加'+86'
|
||||||
|
string avatar=6; // 头像
|
||||||
|
int32 age=7; // 年龄
|
||||||
|
GenderType gender=8; // 性别,1:男,2:女
|
||||||
|
int32 status=9; // 账号状态
|
||||||
|
int64 login_at=10; // 登录时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserExampleByIDReply {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserExample {
|
||||||
|
uint64 id = 1;
|
||||||
|
string name = 2; // 名称
|
||||||
|
string email = 3; // 邮件
|
||||||
|
string phone=4; // 手机号码,必须在前加'+86'
|
||||||
|
string avatar=5; // 头像
|
||||||
|
int32 age=6; // 年龄
|
||||||
|
GenderType gender=7; // 性别,1:男,2:女
|
||||||
|
int32 status=8; // 账号状态
|
||||||
|
int64 login_at=9; // 登录时间戳
|
||||||
|
int64 created_at=10; // 创建时间
|
||||||
|
int64 updated_at=11; // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserExampleByIDRequest {
|
||||||
|
uint64 id = 1 [(validate.rules).uint64.gte = 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserExampleByIDReply {
|
||||||
|
UserExample userExample = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListUserExampleRequest {
|
||||||
|
types.Params params = 1 [(validate.rules).message.required = true];
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListUserExampleReply {
|
||||||
|
int64 total =1;
|
||||||
|
repeated UserExample userExamples = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the templates code end
|
|
@ -0,0 +1,249 @@
|
||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.2.0
|
||||||
|
// - protoc v3.20.1
|
||||||
|
// source: api/userExample/v1/userExample.proto
|
||||||
|
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.32.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
// UserExampleServiceClient is the client API for UserExampleService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type UserExampleServiceClient interface {
|
||||||
|
Create(ctx context.Context, in *CreateUserExampleRequest, opts ...grpc.CallOption) (*CreateUserExampleReply, error)
|
||||||
|
DeleteByID(ctx context.Context, in *DeleteUserExampleByIDRequest, opts ...grpc.CallOption) (*DeleteUserExampleByIDReply, error)
|
||||||
|
UpdateByID(ctx context.Context, in *UpdateUserExampleByIDRequest, opts ...grpc.CallOption) (*UpdateUserExampleByIDReply, error)
|
||||||
|
GetByID(ctx context.Context, in *GetUserExampleByIDRequest, opts ...grpc.CallOption) (*GetUserExampleByIDReply, error)
|
||||||
|
List(ctx context.Context, in *ListUserExampleRequest, opts ...grpc.CallOption) (*ListUserExampleReply, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type userExampleServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserExampleServiceClient(cc grpc.ClientConnInterface) UserExampleServiceClient {
|
||||||
|
return &userExampleServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userExampleServiceClient) Create(ctx context.Context, in *CreateUserExampleRequest, opts ...grpc.CallOption) (*CreateUserExampleReply, error) {
|
||||||
|
out := new(CreateUserExampleReply)
|
||||||
|
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/Create", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userExampleServiceClient) DeleteByID(ctx context.Context, in *DeleteUserExampleByIDRequest, opts ...grpc.CallOption) (*DeleteUserExampleByIDReply, error) {
|
||||||
|
out := new(DeleteUserExampleByIDReply)
|
||||||
|
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/DeleteByID", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userExampleServiceClient) UpdateByID(ctx context.Context, in *UpdateUserExampleByIDRequest, opts ...grpc.CallOption) (*UpdateUserExampleByIDReply, error) {
|
||||||
|
out := new(UpdateUserExampleByIDReply)
|
||||||
|
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/UpdateByID", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userExampleServiceClient) GetByID(ctx context.Context, in *GetUserExampleByIDRequest, opts ...grpc.CallOption) (*GetUserExampleByIDReply, error) {
|
||||||
|
out := new(GetUserExampleByIDReply)
|
||||||
|
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/GetByID", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userExampleServiceClient) List(ctx context.Context, in *ListUserExampleRequest, opts ...grpc.CallOption) (*ListUserExampleReply, error) {
|
||||||
|
out := new(ListUserExampleReply)
|
||||||
|
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/List", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserExampleServiceServer is the server API for UserExampleService service.
|
||||||
|
// All implementations must embed UnimplementedUserExampleServiceServer
|
||||||
|
// for forward compatibility
|
||||||
|
type UserExampleServiceServer interface {
|
||||||
|
Create(context.Context, *CreateUserExampleRequest) (*CreateUserExampleReply, error)
|
||||||
|
DeleteByID(context.Context, *DeleteUserExampleByIDRequest) (*DeleteUserExampleByIDReply, error)
|
||||||
|
UpdateByID(context.Context, *UpdateUserExampleByIDRequest) (*UpdateUserExampleByIDReply, error)
|
||||||
|
GetByID(context.Context, *GetUserExampleByIDRequest) (*GetUserExampleByIDReply, error)
|
||||||
|
List(context.Context, *ListUserExampleRequest) (*ListUserExampleReply, error)
|
||||||
|
mustEmbedUnimplementedUserExampleServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedUserExampleServiceServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedUserExampleServiceServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedUserExampleServiceServer) Create(context.Context, *CreateUserExampleRequest) (*CreateUserExampleReply, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Create not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserExampleServiceServer) DeleteByID(context.Context, *DeleteUserExampleByIDRequest) (*DeleteUserExampleByIDReply, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method DeleteByID not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserExampleServiceServer) UpdateByID(context.Context, *UpdateUserExampleByIDRequest) (*UpdateUserExampleByIDReply, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method UpdateByID not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserExampleServiceServer) GetByID(context.Context, *GetUserExampleByIDRequest) (*GetUserExampleByIDReply, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetByID not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserExampleServiceServer) List(context.Context, *ListUserExampleRequest) (*ListUserExampleReply, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserExampleServiceServer) mustEmbedUnimplementedUserExampleServiceServer() {}
|
||||||
|
|
||||||
|
// UnsafeUserExampleServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to UserExampleServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeUserExampleServiceServer interface {
|
||||||
|
mustEmbedUnimplementedUserExampleServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterUserExampleServiceServer(s grpc.ServiceRegistrar, srv UserExampleServiceServer) {
|
||||||
|
s.RegisterService(&UserExampleService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserExampleService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateUserExampleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserExampleServiceServer).Create(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/api.userExample.v1.userExampleService/Create",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserExampleServiceServer).Create(ctx, req.(*CreateUserExampleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserExampleService_DeleteByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(DeleteUserExampleByIDRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserExampleServiceServer).DeleteByID(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/api.userExample.v1.userExampleService/DeleteByID",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserExampleServiceServer).DeleteByID(ctx, req.(*DeleteUserExampleByIDRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserExampleService_UpdateByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpdateUserExampleByIDRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserExampleServiceServer).UpdateByID(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/api.userExample.v1.userExampleService/UpdateByID",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserExampleServiceServer).UpdateByID(ctx, req.(*UpdateUserExampleByIDRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserExampleService_GetByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetUserExampleByIDRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserExampleServiceServer).GetByID(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/api.userExample.v1.userExampleService/GetByID",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserExampleServiceServer).GetByID(ctx, req.(*GetUserExampleByIDRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserExampleService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListUserExampleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserExampleServiceServer).List(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/api.userExample.v1.userExampleService/List",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserExampleServiceServer).List(ctx, req.(*ListUserExampleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserExampleService_ServiceDesc is the grpc.ServiceDesc for UserExampleService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var UserExampleService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "api.userExample.v1.userExampleService",
|
||||||
|
HandlerType: (*UserExampleServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "Create",
|
||||||
|
Handler: _UserExampleService_Create_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DeleteByID",
|
||||||
|
Handler: _UserExampleService_DeleteByID_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateByID",
|
||||||
|
Handler: _UserExampleService_UpdateByID_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetByID",
|
||||||
|
Handler: _UserExampleService_GetByID_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "List",
|
||||||
|
Handler: _UserExampleService_List_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "api/userExample/v1/userExample.proto",
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/config"
|
||||||
|
"github.com/zhufuyi/sponge/internal/model"
|
||||||
|
"github.com/zhufuyi/sponge/internal/server"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/app"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/registry"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/registry/etcd"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/tracer"
|
||||||
|
|
||||||
|
clientv3 "go.etcd.io/etcd/client/v3"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
version string
|
||||||
|
configFile string
|
||||||
|
)
|
||||||
|
|
||||||
|
// @title sponge api docs
|
||||||
|
// @description http server api docs
|
||||||
|
// @schemes http https
|
||||||
|
// @version v0.0.0
|
||||||
|
// @host localhost:8080
|
||||||
|
func main() {
|
||||||
|
inits := registerInits()
|
||||||
|
servers := registerServers()
|
||||||
|
closes := registerCloses(servers)
|
||||||
|
|
||||||
|
s := app.New(inits, servers, closes)
|
||||||
|
s.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------- 注册app初始化 ---------------------------------
|
||||||
|
func registerInits() []app.Init {
|
||||||
|
// 初始化配置文件,必须优先执行,后面的初始化需要依赖配置
|
||||||
|
func() {
|
||||||
|
flag.StringVar(&configFile, "c", "", "配置文件")
|
||||||
|
flag.StringVar(&version, "version", "", "服务版本号")
|
||||||
|
flag.Parse()
|
||||||
|
if configFile == "" {
|
||||||
|
configFile = config.Path("conf.yml") // 默认配置文件config/conf.yml
|
||||||
|
}
|
||||||
|
err := config.Init(configFile)
|
||||||
|
if err != nil {
|
||||||
|
panic("init config error: " + err.Error())
|
||||||
|
}
|
||||||
|
if version != "" {
|
||||||
|
config.Get().App.Version = version
|
||||||
|
}
|
||||||
|
//config.Show()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 执行初始化日志
|
||||||
|
func() {
|
||||||
|
_, err := logger.Init(
|
||||||
|
logger.WithLevel(config.Get().Logger.Level),
|
||||||
|
logger.WithFormat(config.Get().Logger.Format),
|
||||||
|
logger.WithSave(config.Get().Logger.IsSave,
|
||||||
|
logger.WithFileName(config.Get().Logger.LogFileConfig.Filename),
|
||||||
|
logger.WithFileMaxSize(config.Get().Logger.LogFileConfig.MaxSize),
|
||||||
|
logger.WithFileMaxBackups(config.Get().Logger.LogFileConfig.MaxBackups),
|
||||||
|
logger.WithFileMaxAge(config.Get().Logger.LogFileConfig.MaxAge),
|
||||||
|
logger.WithFileIsCompression(config.Get().Logger.LogFileConfig.IsCompression),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic("init logger error: " + err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var inits []app.Init
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
inits = append(inits, func() {
|
||||||
|
model.InitMysql()
|
||||||
|
model.InitRedis()
|
||||||
|
})
|
||||||
|
|
||||||
|
if config.Get().App.EnableTracing { // 根据配置是否开启链路跟踪
|
||||||
|
inits = append(inits, func() {
|
||||||
|
// 初始化链路跟踪
|
||||||
|
exporter, err := tracer.NewJaegerAgentExporter(config.Get().Jaeger.AgentHost, config.Get().Jaeger.AgentPort)
|
||||||
|
if err != nil {
|
||||||
|
panic("init trace error:" + err.Error())
|
||||||
|
}
|
||||||
|
resource := tracer.NewResource(
|
||||||
|
tracer.WithServiceName(config.Get().App.Name),
|
||||||
|
tracer.WithEnvironment(config.Get().App.Env),
|
||||||
|
tracer.WithServiceVersion(config.Get().App.Version),
|
||||||
|
)
|
||||||
|
|
||||||
|
tracer.Init(exporter, resource, config.Get().Jaeger.SamplingRate) // 如果SamplingRate=0.5表示只采样50%
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return inits
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------- 注册app服务 ---------------------------------
|
||||||
|
// todo generate the code to register http and grpc services here
|
||||||
|
// delete the templates code start
|
||||||
|
|
||||||
|
func registerServers() []app.IServer {
|
||||||
|
var servers []app.IServer
|
||||||
|
|
||||||
|
// 创建http服务
|
||||||
|
httpAddr := ":" + strconv.Itoa(config.Get().HTTP.Port)
|
||||||
|
httpServer := server.NewHTTPServer(httpAddr,
|
||||||
|
server.WithHTTPReadTimeout(time.Second*time.Duration(config.Get().HTTP.ReadTimeout)),
|
||||||
|
server.WithHTTPWriteTimeout(time.Second*time.Duration(config.Get().HTTP.WriteTimeout)),
|
||||||
|
server.WithHTTPIsProd(config.Get().App.Env == "prod"),
|
||||||
|
)
|
||||||
|
servers = append(servers, httpServer)
|
||||||
|
|
||||||
|
// 创建grpc服务
|
||||||
|
grpcAddr := ":" + strconv.Itoa(config.Get().Grpc.Port)
|
||||||
|
grpcServer := server.NewGRPCServer(grpcAddr, grpcOptions()...)
|
||||||
|
servers = append(servers, grpcServer)
|
||||||
|
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
func grpcOptions() []server.GRPCOption {
|
||||||
|
var opts []server.GRPCOption
|
||||||
|
|
||||||
|
if config.Get().App.EnableRegistryDiscovery {
|
||||||
|
iRegistry, instance := getETCDRegistry(
|
||||||
|
config.Get().Etcd.Addrs,
|
||||||
|
config.Get().App.Name,
|
||||||
|
[]string{fmt.Sprintf("grpc://%s:%d", config.Get().App.Host, config.Get().Grpc.Port)},
|
||||||
|
)
|
||||||
|
opts = append(opts, server.WithRegistry(iRegistry, instance))
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func getETCDRegistry(etcdEndpoints []string, instanceName string, instanceEndpoints []string) (registry.Registry, *registry.ServiceInstance) {
|
||||||
|
serviceInstance := registry.NewServiceInstance(instanceName, instanceEndpoints)
|
||||||
|
|
||||||
|
cli, err := clientv3.New(clientv3.Config{
|
||||||
|
Endpoints: etcdEndpoints,
|
||||||
|
DialTimeout: 5 * time.Second,
|
||||||
|
DialOptions: []grpc.DialOption{
|
||||||
|
grpc.WithBlock(),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
iRegistry := etcd.New(cli)
|
||||||
|
|
||||||
|
return iRegistry, serviceInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the templates code end
|
||||||
|
|
||||||
|
// -------------------------- 注册app需要释放的资源 -------------------------------------------
|
||||||
|
func registerCloses(servers []app.IServer) []app.Close {
|
||||||
|
var closes []app.Close
|
||||||
|
|
||||||
|
// 关闭服务
|
||||||
|
for _, s := range servers {
|
||||||
|
closes = append(closes, s.Stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭mysql
|
||||||
|
closes = append(closes, func() error {
|
||||||
|
return model.CloseMysql()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关闭redis
|
||||||
|
closes = append(closes, func() error {
|
||||||
|
return model.CloseRedis()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关闭trace
|
||||||
|
if config.Get().App.EnableTracing {
|
||||||
|
closes = append(closes, func() error {
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) //nolint
|
||||||
|
return tracer.Close(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return closes
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
// nolint
|
||||||
|
// code generated from config file.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "github.com/zhufuyi/sponge/pkg/conf"
|
||||||
|
|
||||||
|
type Config = GenerateName
|
||||||
|
|
||||||
|
var config *Config
|
||||||
|
|
||||||
|
// Init parsing configuration files to struct, including yaml, toml, json, etc.
|
||||||
|
func Init(configFile string) error {
|
||||||
|
config = &Config{}
|
||||||
|
return conf.Parse(configFile, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Show() {
|
||||||
|
conf.Show(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get() *Config {
|
||||||
|
if config == nil {
|
||||||
|
panic("config is nil")
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenerateName struct {
|
||||||
|
App App `yaml:"app" json:"app"`
|
||||||
|
Etcd Etcd `yaml:"etcd" json:"etcd"`
|
||||||
|
Grpc Grpc `yaml:"grpc" json:"grpc"`
|
||||||
|
HTTP HTTP `yaml:"http" json:"http"`
|
||||||
|
Jaeger Jaeger `yaml:"jaeger" json:"jaeger"`
|
||||||
|
Logger Logger `yaml:"logger" json:"logger"`
|
||||||
|
Metrics Metrics `yaml:"metrics" json:"metrics"`
|
||||||
|
Mysql Mysql `yaml:"mysql" json:"mysql"`
|
||||||
|
RateLimiter RateLimiter `yaml:"rateLimiter" json:"rateLimiter"`
|
||||||
|
Redis Redis `yaml:"redis" json:"redis"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Redis struct {
|
||||||
|
Addr string `yaml:"addr" json:"addr"`
|
||||||
|
DB int `yaml:"dB" json:"dB"`
|
||||||
|
DialTimeout int `yaml:"dialTimeout" json:"dialTimeout"`
|
||||||
|
Dsn string `yaml:"dsn" json:"dsn"`
|
||||||
|
MinIdleConn int `yaml:"minIdleConn" json:"minIdleConn"`
|
||||||
|
Password string `yaml:"password" json:"password"`
|
||||||
|
PoolSize int `yaml:"poolSize" json:"poolSize"`
|
||||||
|
PoolTimeout int `yaml:"poolTimeout" json:"poolTimeout"`
|
||||||
|
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
|
||||||
|
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Etcd struct {
|
||||||
|
Addrs []string `yaml:"addrs" json:"addrs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Jaeger struct {
|
||||||
|
AgentHost string `yaml:"agentHost" json:"agentHost"`
|
||||||
|
AgentPort string `yaml:"agentPort" json:"agentPort"`
|
||||||
|
SamplingRate float64 `yaml:"samplingRate" json:"samplingRate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mysql struct {
|
||||||
|
ConnMaxLifetime int `yaml:"connMaxLifetime" json:"connMaxLifetime"`
|
||||||
|
Dsn string `yaml:"dsn" json:"dsn"`
|
||||||
|
EnableLog bool `yaml:"enableLog" json:"enableLog"`
|
||||||
|
MaxIdleConns int `yaml:"maxIdleConns" json:"maxIdleConns"`
|
||||||
|
MaxOpenConns int `yaml:"maxOpenConns" json:"maxOpenConns"`
|
||||||
|
SlowThreshold int `yaml:"slowThreshold" json:"slowThreshold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateLimiter struct {
|
||||||
|
Dimension string `yaml:"dimension" json:"dimension"`
|
||||||
|
MaxLimit int `yaml:"maxLimit" json:"maxLimit"`
|
||||||
|
QPSLimit int `yaml:"qpsLimit" json:"qpsLimit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
EnableRegistryDiscovery bool `yaml:"enableRegistryDiscovery" json:"enableRegistryDiscovery"`
|
||||||
|
EnableLimit bool `yaml:"enableLimit" json:"enableLimit"`
|
||||||
|
EnableMetrics bool `yaml:"enableMetrics" json:"enableMetrics"`
|
||||||
|
EnableProfile bool `yaml:"enableProfile" json:"enableProfile"`
|
||||||
|
EnableTracing bool `yaml:"enableTracing" json:"enableTracing"`
|
||||||
|
Env string `yaml:"env" json:"env"`
|
||||||
|
Host string `yaml:"hostIP" json:"hostIP"`
|
||||||
|
Name string `yaml:"name" json:"name"`
|
||||||
|
Version string `yaml:"version" json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogFileConfig struct {
|
||||||
|
Filename string `yaml:"filename" json:"filename"`
|
||||||
|
IsCompression bool `yaml:"isCompression" json:"isCompression"`
|
||||||
|
MaxAge int `yaml:"maxAge" json:"maxAge"`
|
||||||
|
MaxBackups int `yaml:"maxBackups" json:"maxBackups"`
|
||||||
|
MaxSize int `yaml:"maxSize" json:"maxSize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
Format string `yaml:"format" json:"format"`
|
||||||
|
IsSave bool `yaml:"isSave" json:"isSave"`
|
||||||
|
Level string `yaml:"level" json:"level"`
|
||||||
|
LogFileConfig LogFileConfig `yaml:"logFileConfig" json:"logFileConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTP struct {
|
||||||
|
Port int `yaml:"port" json:"port"`
|
||||||
|
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
|
||||||
|
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
|
||||||
|
ServiceName string `yaml:"serviceName" json:"serviceName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Grpc struct {
|
||||||
|
Port int `yaml:"port" json:"port"`
|
||||||
|
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
|
||||||
|
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
|
||||||
|
ServiceName string `yaml:"serviceName" json:"serviceName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metrics struct {
|
||||||
|
Port int `yaml:"port" json:"port"`
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
# 使用工具自动生成go struct
|
||||||
|
# goctl covert yaml --file=conf.yml --tags=json
|
||||||
|
|
||||||
|
# app 设置
|
||||||
|
app:
|
||||||
|
name: "userExample" # 服务名称
|
||||||
|
env: "dev" # 运行环境,dev:开发环境,prod:生产环境,pre:预生产环境
|
||||||
|
version: "v0.0.0" # 版本
|
||||||
|
host: "127.0.0.1" # 主机ip或域名
|
||||||
|
enableProfile: false # 是否开启性能分析功能,true:开启,false:关闭
|
||||||
|
enableMetrics: true # 是否开启指标采集,true:开启,false:关闭
|
||||||
|
enableLimit: false # 是否开启限流,true:开启,false:关闭
|
||||||
|
enableTracing: false # 是否开启链路跟踪,true:开启,false:关闭
|
||||||
|
enableRegistryDiscovery: true # 是否开启注册与发现,true:开启,false:关闭
|
||||||
|
|
||||||
|
|
||||||
|
# http 设置
|
||||||
|
http:
|
||||||
|
port: 8080 # 监听端口
|
||||||
|
readTimeout: 3 # 读超时,单位(秒)
|
||||||
|
writeTimeout: 90 # 写超时,单位(秒),如果enableProfile为true,需要大于60s,pprof做profiling的默认值是60s
|
||||||
|
|
||||||
|
|
||||||
|
# grpc 服务设置
|
||||||
|
grpc:
|
||||||
|
port: 9090 # 监听端口
|
||||||
|
readTimeout: 3 # 读超时,单位(秒)
|
||||||
|
writeTimeout: 3 # 写超时,单位(秒)
|
||||||
|
|
||||||
|
|
||||||
|
# logger 设置
|
||||||
|
logger:
|
||||||
|
level: "info" # 输出日志级别 debug, info, warn, error,默认是debug
|
||||||
|
format: "console" # 输出格式,console或json,默认是console
|
||||||
|
isSave: false # false:输出到终端,true:输出到文件,默认是false
|
||||||
|
logFileConfig: # isSave=true时有效
|
||||||
|
filename: "out.log" # 文件名称,默认值out.log
|
||||||
|
maxSize: 20 # 最大文件大小(MB),默认值10MB
|
||||||
|
maxBackups: 50 # 保留旧文件的最大个数,默认值100个
|
||||||
|
maxAge: 15 # 保留旧文件的最大天数,默认值30天
|
||||||
|
isCompression: true # 是否压缩/归档旧文件,默认值false
|
||||||
|
|
||||||
|
|
||||||
|
# mysql 设置
|
||||||
|
mysql:
|
||||||
|
# dsn格式:<user>:<pass>@(127.0.0.1:3306)/<db>?[k=v& ......]
|
||||||
|
dsn: "root:123456@(192.168.3.37:3306)/account?parseTime=true&loc=Local&charset=utf8,utf8mb4"
|
||||||
|
enableLog: true # 是否开启打印所有日志
|
||||||
|
slowThreshold: 0 # 如果大于0,只打印时间大于阈值的日志,优先级比enableLog高,单位(毫秒)
|
||||||
|
maxIdleConns: 3 #设置空闲连接池中连接的最大数量
|
||||||
|
maxOpenConns: 50 # 设置打开数据库连接的最大数量
|
||||||
|
connMaxLifetime: 30 # 设置了连接可复用的最大时间,单位(分钟)
|
||||||
|
|
||||||
|
|
||||||
|
# redis 设置
|
||||||
|
redis:
|
||||||
|
# dsn只适合redis6版本以上,默认用户为default,url格式 [user]:<pass>@]127.0.0.1:6379/[db]
|
||||||
|
dsn: "default:123456@192.168.3.37:6379"
|
||||||
|
# 适合各个版本redis
|
||||||
|
addr: 127.0.0.1:6379
|
||||||
|
password: "123456"
|
||||||
|
dB: 0
|
||||||
|
minIdleConn: 20
|
||||||
|
dialTimeout: 30 # 链接超时,单位(秒)
|
||||||
|
readTimeout: 500 # 读超时,单位(毫秒)
|
||||||
|
writeTimeout: 500 # 写超时,单位(毫秒)
|
||||||
|
poolSize: 100
|
||||||
|
poolTimeout: 200 # 连接池超时,单位(秒)
|
||||||
|
|
||||||
|
|
||||||
|
# jaeger配置
|
||||||
|
jaeger:
|
||||||
|
agentHost: "192.168.3.37"
|
||||||
|
agentPort: "6831"
|
||||||
|
samplingRate: 1.0 # 采样率,0~1之间,0表示禁止采样,大于等于1表示采样所有链路
|
||||||
|
|
||||||
|
|
||||||
|
# limit配置
|
||||||
|
rateLimiter:
|
||||||
|
dimension: "path" # 限流维度,支持path和ip两种,默认是path
|
||||||
|
qpsLimit: 1000 # 持续每秒允许成功请求数,默认是500
|
||||||
|
maxLimit: 2000 # 瞬时最大允许峰值,默认是1000,通常大于qpsLimit
|
||||||
|
|
||||||
|
|
||||||
|
# metrics配置
|
||||||
|
metrics:
|
||||||
|
port: 9082
|
||||||
|
|
||||||
|
|
||||||
|
# etcd配置
|
||||||
|
etcd:
|
||||||
|
addrs: ["192.168.3.37:2379"]
|
|
@ -0,0 +1,25 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 用来定位目录
|
||||||
|
|
||||||
|
var basePath string
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
func init() {
|
||||||
|
_, currentFile, _, _ := runtime.Caller(0)
|
||||||
|
basePath = filepath.Dir(currentFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path 返回绝对路径
|
||||||
|
func Path(rel string) string {
|
||||||
|
if filepath.IsAbs(rel) {
|
||||||
|
return rel
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(basePath, rel)
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/*
|
||||||
|
sponge 是一个微服务框架,支持http和grpc及服务治理.
|
||||||
|
*/
|
||||||
|
package sponge
|
|
@ -0,0 +1,392 @@
|
||||||
|
// Package docs GENERATED BY SWAG; DO NOT EDIT
|
||||||
|
// This file was generated by swaggo/swag
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import "github.com/swaggo/swag"
|
||||||
|
|
||||||
|
const docTemplate = `{
|
||||||
|
"schemes": {{ marshal .Schemes }},
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "{{escape .Description}}",
|
||||||
|
"title": "{{.Title}}",
|
||||||
|
"contact": {},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/userExample": {
|
||||||
|
"post": {
|
||||||
|
"description": "提交信息创建userExample",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "创建userExample",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "userExample信息",
|
||||||
|
"name": "data",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.CreateUserExampleRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/userExample/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "根据id获取userExample详情",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "获取userExample详情",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"description": "根据id更新userExample信息",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "更新userExample信息",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "userExample信息",
|
||||||
|
"name": "data",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.UpdateUserExampleByIDRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "根据id删除userExample",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "删除userExample",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/userExamples": {
|
||||||
|
"post": {
|
||||||
|
"description": "使用post请求获取userExample列表",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "获取userExample列表",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "查询条件",
|
||||||
|
"name": "data",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Params"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"description": "check health",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"summary": "check health",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlerfunc.checkHealthResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ping": {
|
||||||
|
"get": {
|
||||||
|
"description": "ping",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"summary": "ping",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"handler.Column": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exp": {
|
||||||
|
"description": "表达式,值为空时默认为=,有=、!=、\u003e、\u003e=、\u003c、\u003c=、like七种类型",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"logic": {
|
||||||
|
"description": "逻辑类型,值为空时默认为and,有\u0026(and)、||(or)两种类型",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "列名",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"description": "列值"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.CreateUserExampleRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"age": {
|
||||||
|
"description": "年龄",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"description": "头像",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 5
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"description": "邮件",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"description": "性别,1:男,2:女",
|
||||||
|
"type": "integer",
|
||||||
|
"maximum": 2,
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "名称",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 2
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"description": "密码",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"description": "手机号码,必须在前加'+86'",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.Params": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"columns": {
|
||||||
|
"description": "列查询条件",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handler.Column"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "页码",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"description": "每页行数",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"description": "排序字段,默认值为-id,字段前面有-号表示倒序,否则升序,多个字段用逗号分隔",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.Result": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"description": "返回码",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "返回数据"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"description": "返回信息说明",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.UpdateUserExampleByIDRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"age": {
|
||||||
|
"description": "年龄",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"description": "头像",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"description": "邮件",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"description": "性别,1:男,2:女",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "id",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "名称",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"description": "密码",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"description": "手机号码,必须在前加'+86'",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlerfunc.checkHealthResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hostname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
|
var SwaggerInfo = &swag.Spec{
|
||||||
|
Version: "v0.0.0",
|
||||||
|
Host: "localhost:8080",
|
||||||
|
BasePath: "",
|
||||||
|
Schemes: []string{"http", "https"},
|
||||||
|
Title: "sponge api docs",
|
||||||
|
Description: "http server api docs",
|
||||||
|
InfoInstanceName: "swagger",
|
||||||
|
SwaggerTemplate: docTemplate,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,372 @@
|
||||||
|
{
|
||||||
|
"schemes": [
|
||||||
|
"http",
|
||||||
|
"https"
|
||||||
|
],
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "http server api docs",
|
||||||
|
"title": "sponge api docs",
|
||||||
|
"contact": {},
|
||||||
|
"version": "v0.0.0"
|
||||||
|
},
|
||||||
|
"host": "localhost:8080",
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/userExample": {
|
||||||
|
"post": {
|
||||||
|
"description": "提交信息创建userExample",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "创建userExample",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "userExample信息",
|
||||||
|
"name": "data",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.CreateUserExampleRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/userExample/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "根据id获取userExample详情",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "获取userExample详情",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"description": "根据id更新userExample信息",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "更新userExample信息",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "userExample信息",
|
||||||
|
"name": "data",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.UpdateUserExampleByIDRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "根据id删除userExample",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "删除userExample",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/userExamples": {
|
||||||
|
"post": {
|
||||||
|
"description": "使用post请求获取userExample列表",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"userExample"
|
||||||
|
],
|
||||||
|
"summary": "获取userExample列表",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "查询条件",
|
||||||
|
"name": "data",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Params"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"description": "check health",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"summary": "check health",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlerfunc.checkHealthResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ping": {
|
||||||
|
"get": {
|
||||||
|
"description": "ping",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"summary": "ping",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"handler.Column": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exp": {
|
||||||
|
"description": "表达式,值为空时默认为=,有=、!=、\u003e、\u003e=、\u003c、\u003c=、like七种类型",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"logic": {
|
||||||
|
"description": "逻辑类型,值为空时默认为and,有\u0026(and)、||(or)两种类型",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "列名",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"description": "列值"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.CreateUserExampleRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"age": {
|
||||||
|
"description": "年龄",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"description": "头像",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 5
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"description": "邮件",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"description": "性别,1:男,2:女",
|
||||||
|
"type": "integer",
|
||||||
|
"maximum": 2,
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "名称",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 2
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"description": "密码",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"description": "手机号码,必须在前加'+86'",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.Params": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"columns": {
|
||||||
|
"description": "列查询条件",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handler.Column"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"description": "页码",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"description": "每页行数",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"description": "排序字段,默认值为-id,字段前面有-号表示倒序,否则升序,多个字段用逗号分隔",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.Result": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"description": "返回码",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"description": "返回数据"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"description": "返回信息说明",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.UpdateUserExampleByIDRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"age": {
|
||||||
|
"description": "年龄",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"description": "头像",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"description": "邮件",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"description": "性别,1:男,2:女",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "id",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "名称",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"description": "密码",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"description": "手机号码,必须在前加'+86'",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlerfunc.checkHealthResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hostname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
definitions:
|
||||||
|
handler.Column:
|
||||||
|
properties:
|
||||||
|
exp:
|
||||||
|
description: 表达式,值为空时默认为=,有=、!=、>、>=、<、<=、like七种类型
|
||||||
|
type: string
|
||||||
|
logic:
|
||||||
|
description: 逻辑类型,值为空时默认为and,有&(and)、||(or)两种类型
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: 列名
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
description: 列值
|
||||||
|
type: object
|
||||||
|
handler.CreateUserExampleRequest:
|
||||||
|
properties:
|
||||||
|
age:
|
||||||
|
description: 年龄
|
||||||
|
type: integer
|
||||||
|
avatar:
|
||||||
|
description: 头像
|
||||||
|
minLength: 5
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
description: 邮件
|
||||||
|
type: string
|
||||||
|
gender:
|
||||||
|
description: 性别,1:男,2:女
|
||||||
|
maximum: 2
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
description: 名称
|
||||||
|
minLength: 2
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
description: 密码
|
||||||
|
type: string
|
||||||
|
phone:
|
||||||
|
description: 手机号码,必须在前加'+86'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handler.Params:
|
||||||
|
properties:
|
||||||
|
columns:
|
||||||
|
description: 列查询条件
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/handler.Column'
|
||||||
|
type: array
|
||||||
|
page:
|
||||||
|
description: 页码
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
size:
|
||||||
|
description: 每页行数
|
||||||
|
type: integer
|
||||||
|
sort:
|
||||||
|
description: 排序字段,默认值为-id,字段前面有-号表示倒序,否则升序,多个字段用逗号分隔
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handler.Result:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
description: 返回码
|
||||||
|
type: integer
|
||||||
|
data:
|
||||||
|
description: 返回数据
|
||||||
|
msg:
|
||||||
|
description: 返回信息说明
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handler.UpdateUserExampleByIDRequest:
|
||||||
|
properties:
|
||||||
|
age:
|
||||||
|
description: 年龄
|
||||||
|
type: integer
|
||||||
|
avatar:
|
||||||
|
description: 头像
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
description: 邮件
|
||||||
|
type: string
|
||||||
|
gender:
|
||||||
|
description: 性别,1:男,2:女
|
||||||
|
type: integer
|
||||||
|
id:
|
||||||
|
description: id
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
description: 名称
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
description: 密码
|
||||||
|
type: string
|
||||||
|
phone:
|
||||||
|
description: 手机号码,必须在前加'+86'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlerfunc.checkHealthResponse:
|
||||||
|
properties:
|
||||||
|
hostname:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
host: localhost:8080
|
||||||
|
info:
|
||||||
|
contact: {}
|
||||||
|
description: http server api docs
|
||||||
|
title: sponge api docs
|
||||||
|
version: v0.0.0
|
||||||
|
paths:
|
||||||
|
/api/v1/userExample:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 提交信息创建userExample
|
||||||
|
parameters:
|
||||||
|
- description: userExample信息
|
||||||
|
in: body
|
||||||
|
name: data
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.CreateUserExampleRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.Result'
|
||||||
|
summary: 创建userExample
|
||||||
|
tags:
|
||||||
|
- userExample
|
||||||
|
/api/v1/userExample/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 根据id删除userExample
|
||||||
|
parameters:
|
||||||
|
- description: id
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.Result'
|
||||||
|
summary: 删除userExample
|
||||||
|
tags:
|
||||||
|
- userExample
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 根据id获取userExample详情
|
||||||
|
parameters:
|
||||||
|
- description: id
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.Result'
|
||||||
|
summary: 获取userExample详情
|
||||||
|
tags:
|
||||||
|
- userExample
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 根据id更新userExample信息
|
||||||
|
parameters:
|
||||||
|
- description: id
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: userExample信息
|
||||||
|
in: body
|
||||||
|
name: data
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.UpdateUserExampleByIDRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.Result'
|
||||||
|
summary: 更新userExample信息
|
||||||
|
tags:
|
||||||
|
- userExample
|
||||||
|
/api/v1/userExamples:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 使用post请求获取userExample列表
|
||||||
|
parameters:
|
||||||
|
- description: 查询条件
|
||||||
|
in: body
|
||||||
|
name: data
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.Params'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.Result'
|
||||||
|
summary: 获取userExample列表
|
||||||
|
tags:
|
||||||
|
- userExample
|
||||||
|
/health:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: check health
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlerfunc.checkHealthResponse'
|
||||||
|
summary: check health
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
/ping:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: ping
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses: {}
|
||||||
|
summary: ping
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
schemes:
|
||||||
|
- http
|
||||||
|
- https
|
||||||
|
swagger: "2.0"
|
|
@ -0,0 +1,164 @@
|
||||||
|
module github.com/zhufuyi/sponge
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
|
||||||
|
github.com/alicebob/miniredis/v2 v2.23.0
|
||||||
|
github.com/bojand/ghz v0.110.0
|
||||||
|
github.com/dgraph-io/ristretto v0.1.0
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.6.2
|
||||||
|
github.com/fsnotify/fsnotify v1.5.4
|
||||||
|
github.com/gin-contrib/cors v1.3.1
|
||||||
|
github.com/gin-contrib/pprof v1.4.0
|
||||||
|
github.com/gin-gonic/gin v1.8.1
|
||||||
|
github.com/go-playground/validator/v10 v10.11.0
|
||||||
|
github.com/go-redis/redis/extra/redisotel v0.3.0
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
|
github.com/gogo/protobuf v1.3.2
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
|
github.com/golang/protobuf v1.5.2
|
||||||
|
github.com/golang/snappy v0.0.3
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||||
|
github.com/gunerhuseyin/goprometheus v0.0.2
|
||||||
|
github.com/hashicorp/consul/api v1.12.0
|
||||||
|
github.com/huandu/xstrings v1.3.1
|
||||||
|
github.com/jinzhu/copier v0.3.5
|
||||||
|
github.com/nacos-group/nacos-sdk-go v1.1.2
|
||||||
|
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||||
|
github.com/prometheus/client_golang v1.13.0
|
||||||
|
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad
|
||||||
|
github.com/spf13/cast v1.5.0
|
||||||
|
github.com/spf13/viper v1.12.0
|
||||||
|
github.com/stretchr/testify v1.8.0
|
||||||
|
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
|
||||||
|
github.com/swaggo/gin-swagger v1.5.2
|
||||||
|
github.com/swaggo/swag v1.8.1
|
||||||
|
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.1.15
|
||||||
|
github.com/vmihailenco/msgpack v4.0.4+incompatible
|
||||||
|
go.etcd.io/etcd/client/v3 v3.5.4
|
||||||
|
go.opentelemetry.io/contrib v1.9.0
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0
|
||||||
|
go.opentelemetry.io/otel v1.9.0
|
||||||
|
go.opentelemetry.io/otel/exporters/jaeger v1.9.0
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.9.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.9.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.9.0
|
||||||
|
go.uber.org/zap v1.21.0
|
||||||
|
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||||
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||||
|
google.golang.org/grpc v1.48.0
|
||||||
|
google.golang.org/protobuf v1.28.1
|
||||||
|
gorm.io/driver/mysql v1.3.5
|
||||||
|
gorm.io/gorm v1.23.8
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/compute v1.6.1 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||||
|
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||||
|
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||||
|
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
|
||||||
|
github.com/armon/go-metrics v0.3.10 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
|
github.com/cactus/go-statsd-client/statsd v0.0.0-00010101000000-000000000000 // indirect
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
|
||||||
|
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
|
||||||
|
github.com/coreos/go-semver v0.3.0 // indirect
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
|
||||||
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-errors/errors v1.0.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.2.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
|
github.com/go-redis/redis/extra/rediscmd v0.2.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.9.7 // indirect
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-hclog v1.2.0 // indirect
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||||
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/serf v0.9.7 // indirect
|
||||||
|
github.com/imdario/mergo v0.3.11 // indirect
|
||||||
|
github.com/jhump/protoreflect v1.9.0 // indirect
|
||||||
|
github.com/jinzhu/configor v1.1.1 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.6 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
|
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.1 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||||
|
github.com/shopspring/decimal v1.2.0 // indirect
|
||||||
|
github.com/smartystreets/goconvey v1.7.2 // indirect
|
||||||
|
github.com/spf13/afero v1.8.2 // indirect
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/subosito/gotenv v1.3.0 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||||
|
github.com/uptrace/opentelemetry-go-extra/otelsql v0.1.15 // indirect
|
||||||
|
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
|
||||||
|
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
|
||||||
|
go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v0.31.0 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
go.uber.org/multierr v1.6.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
|
||||||
|
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
golang.org/x/tools v0.1.11-0.20220316014157-77aa08bb151a // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
|
||||||
|
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.9.0
|
||||||
|
|
||||||
|
replace github.com/cactus/go-statsd-client/statsd => github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c
|
|
@ -0,0 +1,138 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/internal/model"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/cache"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/encoding"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PrefixUserExampleCacheKey cache prefix
|
||||||
|
PrefixUserExampleCacheKey = "userExample:%d"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ UserExampleCache = (*userExampleCache)(nil)
|
||||||
|
|
||||||
|
// UserExampleCache cache interface
|
||||||
|
type UserExampleCache interface {
|
||||||
|
Set(ctx context.Context, id uint64, data *model.UserExample, duration time.Duration) error
|
||||||
|
Get(ctx context.Context, id uint64) (ret *model.UserExample, err error)
|
||||||
|
MultiGet(ctx context.Context, ids []uint64) (map[string]*model.UserExample, error)
|
||||||
|
MultiSet(ctx context.Context, data []*model.UserExample, duration time.Duration) error
|
||||||
|
Del(ctx context.Context, id uint64) error
|
||||||
|
SetCacheWithNotFound(ctx context.Context, id uint64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// userExampleCache define a cache struct
|
||||||
|
type userExampleCache struct {
|
||||||
|
cache cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserExampleCache new a cache
|
||||||
|
func NewUserExampleCache(rdb *redis.Client) UserExampleCache {
|
||||||
|
jsonEncoding := encoding.JSONEncoding{}
|
||||||
|
cachePrefix := ""
|
||||||
|
return &userExampleCache{
|
||||||
|
cache: cache.NewRedisCache(rdb, cachePrefix, jsonEncoding, func() interface{} {
|
||||||
|
return &model.UserExample{}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserExampleCacheKey 设置缓存
|
||||||
|
func (c *userExampleCache) GetUserExampleCacheKey(id uint64) string {
|
||||||
|
return fmt.Sprintf(PrefixUserExampleCacheKey, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set write to cache
|
||||||
|
func (c *userExampleCache) Set(ctx context.Context, id uint64, data *model.UserExample, duration time.Duration) error {
|
||||||
|
if data == nil || id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cacheKey := c.GetUserExampleCacheKey(id)
|
||||||
|
err := c.cache.Set(ctx, cacheKey, data, duration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取cache
|
||||||
|
func (c *userExampleCache) Get(ctx context.Context, id uint64) (*model.UserExample, error) {
|
||||||
|
var data *model.UserExample
|
||||||
|
cacheKey := c.GetUserExampleCacheKey(id)
|
||||||
|
err := c.cache.Get(ctx, cacheKey, &data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiSet 批量设置cache
|
||||||
|
func (c *userExampleCache) MultiSet(ctx context.Context, data []*model.UserExample, duration time.Duration) error {
|
||||||
|
valMap := make(map[string]interface{})
|
||||||
|
for _, v := range data {
|
||||||
|
cacheKey := c.GetUserExampleCacheKey(v.ID)
|
||||||
|
valMap[cacheKey] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.cache.MultiSet(ctx, valMap, duration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiGet 批量获取cache,返回map中的key是id值
|
||||||
|
func (c *userExampleCache) MultiGet(ctx context.Context, ids []uint64) (map[string]*model.UserExample, error) {
|
||||||
|
var keys []string
|
||||||
|
for _, v := range ids {
|
||||||
|
cacheKey := c.GetUserExampleCacheKey(v)
|
||||||
|
keys = append(keys, cacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: 需要在这里make实例化,如果在返回参数里直接定义会报 nil map
|
||||||
|
itemMap := make(map[string]*model.UserExample)
|
||||||
|
err := c.cache.MultiGet(ctx, keys, itemMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
retMap := make(map[string]*model.UserExample)
|
||||||
|
for _, v := range ids {
|
||||||
|
val, ok := itemMap[c.GetUserExampleCacheKey(v)]
|
||||||
|
if ok {
|
||||||
|
retMap[cast.ToString(v)] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del 删除cache
|
||||||
|
func (c *userExampleCache) Del(ctx context.Context, id uint64) error {
|
||||||
|
cacheKey := c.GetUserExampleCacheKey(id)
|
||||||
|
err := c.cache.Del(ctx, cacheKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheWithNotFound 设置空缓存
|
||||||
|
func (c *userExampleCache) SetCacheWithNotFound(ctx context.Context, id uint64) error {
|
||||||
|
cacheKey := c.GetUserExampleCacheKey(id)
|
||||||
|
err := c.cache.SetCacheWithNotFound(ctx, cacheKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/internal/model"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/mysql"
|
||||||
|
|
||||||
|
"github.com/alicebob/miniredis/v2"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
redisServer *miniredis.Miniredis
|
||||||
|
redisClient *redis.Client
|
||||||
|
testData = &model.UserExample{
|
||||||
|
Model: mysql.Model{ID: 1},
|
||||||
|
Name: "foo",
|
||||||
|
}
|
||||||
|
uc UserExampleCache
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
redisServer = mockRedis()
|
||||||
|
redisClient = redis.NewClient(&redis.Options{Addr: redisServer.Addr()})
|
||||||
|
uc = NewUserExampleCache(redisClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardown() {
|
||||||
|
redisServer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockRedis() *miniredis.Miniredis {
|
||||||
|
s, err := miniredis.Run()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_userExampleCache_Set(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
var id uint64
|
||||||
|
ctx := context.Background()
|
||||||
|
id = 1
|
||||||
|
err := uc.Set(ctx, id, testData, time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_userExampleCache_Get(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
var id uint64
|
||||||
|
ctx := context.Background()
|
||||||
|
id = 1
|
||||||
|
err := uc.Set(ctx, id, testData, time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
act, err := uc.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, testData, act)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_userExampleCache_MultiGet(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
testData := []*model.UserExample{
|
||||||
|
{Model: mysql.Model{ID: 1}},
|
||||||
|
{Model: mysql.Model{ID: 2}},
|
||||||
|
}
|
||||||
|
err := uc.MultiSet(ctx, testData, time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := make(map[string]*model.UserExample)
|
||||||
|
expected["1"] = &model.UserExample{Model: mysql.Model{ID: 1}}
|
||||||
|
expected["2"] = &model.UserExample{Model: mysql.Model{ID: 2}}
|
||||||
|
|
||||||
|
act, err := uc.MultiGet(ctx, []uint64{1, 2})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, act)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_userExampleCache_MultiSet(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
testData := []*model.UserExample{
|
||||||
|
{Model: mysql.Model{ID: 1}},
|
||||||
|
{Model: mysql.Model{ID: 2}},
|
||||||
|
}
|
||||||
|
err := uc.MultiSet(ctx, testData, time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_userExampleCache_Del(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
var id uint64
|
||||||
|
ctx := context.Background()
|
||||||
|
id = 1
|
||||||
|
err := uc.Del(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,256 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/internal/cache"
|
||||||
|
"github.com/zhufuyi/sponge/internal/model"
|
||||||
|
cacheBase "github.com/zhufuyi/sponge/pkg/cache"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/goredis"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/mysql/query"
|
||||||
|
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ UserExampleDao = (*userExampleDao)(nil)
|
||||||
|
|
||||||
|
// UserExampleDao 定义dao接口
|
||||||
|
type UserExampleDao interface {
|
||||||
|
Create(ctx context.Context, table *model.UserExample) error
|
||||||
|
DeleteByID(ctx context.Context, id uint64) error
|
||||||
|
UpdateByID(ctx context.Context, table *model.UserExample) error
|
||||||
|
GetByID(ctx context.Context, id uint64) (*model.UserExample, error)
|
||||||
|
GetByColumns(ctx context.Context, params *query.Params) ([]*model.UserExample, int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type userExampleDao struct {
|
||||||
|
db *gorm.DB
|
||||||
|
cache cache.UserExampleCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserExampleDao 创建dao接口
|
||||||
|
func NewUserExampleDao(db *gorm.DB, cache cache.UserExampleCache) UserExampleDao {
|
||||||
|
return &userExampleDao{db: db, cache: cache}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建一条记录,插入记录后,id值被回写到table中
|
||||||
|
func (d *userExampleDao) Create(ctx context.Context, table *model.UserExample) error {
|
||||||
|
return d.db.WithContext(ctx).Create(table).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByID 根据id删除一条记录
|
||||||
|
func (d *userExampleDao) DeleteByID(ctx context.Context, id uint64) error {
|
||||||
|
err := d.db.WithContext(ctx).Where("id = ?", id).Delete(&model.UserExample{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete cache
|
||||||
|
_ = d.cache.Del(ctx, id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes 根据id删除多条记录
|
||||||
|
func (d *userExampleDao) Deletes(ctx context.Context, ids []uint64) error {
|
||||||
|
err := d.db.WithContext(ctx).Where("id IN (?)", ids).Delete(&model.UserExample{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete cache
|
||||||
|
for _, id := range ids {
|
||||||
|
_ = d.cache.Del(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateByID 根据id更新记录
|
||||||
|
func (d *userExampleDao) UpdateByID(ctx context.Context, table *model.UserExample) error {
|
||||||
|
if table.ID < 1 {
|
||||||
|
return errors.New("id cannot be 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
update := map[string]interface{}{}
|
||||||
|
// todo generate the update fields code to here
|
||||||
|
// delete the templates code start
|
||||||
|
if table.Name != "" {
|
||||||
|
update["name"] = table.Name
|
||||||
|
}
|
||||||
|
if table.Password != "" {
|
||||||
|
update["password"] = table.Password
|
||||||
|
}
|
||||||
|
if table.Email != "" {
|
||||||
|
update["email"] = table.Email
|
||||||
|
}
|
||||||
|
if table.Phone != "" {
|
||||||
|
update["phone"] = table.Phone
|
||||||
|
}
|
||||||
|
if table.Avatar != "" {
|
||||||
|
update["avatar"] = table.Avatar
|
||||||
|
}
|
||||||
|
if table.Age > 0 {
|
||||||
|
update["age"] = table.Age
|
||||||
|
}
|
||||||
|
if table.Gender > 0 {
|
||||||
|
update["gender"] = table.Gender
|
||||||
|
}
|
||||||
|
if table.LoginAt > 0 {
|
||||||
|
update["login_at"] = table.LoginAt
|
||||||
|
}
|
||||||
|
// delete the templates code end
|
||||||
|
err := d.db.WithContext(ctx).Model(table).Where("id = ?", table.ID).Updates(update).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete cache
|
||||||
|
_ = d.cache.Del(ctx, table.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 根据id获取一条记录
|
||||||
|
func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExample, error) {
|
||||||
|
record, err := d.cache.Get(ctx, id)
|
||||||
|
|
||||||
|
if errors.Is(err, cacheBase.ErrPlaceholder) {
|
||||||
|
return nil, model.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从mysql获取
|
||||||
|
if errors.Is(err, goredis.ErrRedisNotFound) {
|
||||||
|
table := &model.UserExample{}
|
||||||
|
err = d.db.WithContext(ctx).Where("id = ?", id).First(table).Error
|
||||||
|
if err != nil {
|
||||||
|
// if data is empty, set not found cache to prevent cache penetration(防止缓存穿透)
|
||||||
|
if err.Error() == model.ErrRecordNotFound.Error() {
|
||||||
|
err = d.cache.SetCacheWithNotFound(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, model.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if table.ID == 0 {
|
||||||
|
return nil, model.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// set cache
|
||||||
|
err = d.cache.Set(ctx, id, table, cacheBase.DefaultExpireTime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cache.Set error: %v, id=%d", err, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// fail fast, if cache error return, don't request to db
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByColumns 根据分页和列信息筛选多条记录
|
||||||
|
// params 包括分页参数和查询参数
|
||||||
|
// 分页参数(必须):
|
||||||
|
|
||||||
|
// page: 页码,从0开始
|
||||||
|
// size: 每页行数
|
||||||
|
// sort: 排序字段,默认是id倒叙,可以在字段前添加-号表示倒序,没有-号表示升序,多个字段用逗号分隔
|
||||||
|
//
|
||||||
|
// 查询参数(非必须):
|
||||||
|
//
|
||||||
|
// name: 列名
|
||||||
|
// exp: 表达式,有=、!=、>、>=、<、<=、like七种类型,值为空时默认是=
|
||||||
|
// value: 列值
|
||||||
|
// logic: 表示逻辑类型,有&(and)、||(or)两种类型,值为空时默认是and
|
||||||
|
//
|
||||||
|
// 示例: 查询年龄大于20的男性
|
||||||
|
//
|
||||||
|
// params = &query.Params{
|
||||||
|
// Page: 0,
|
||||||
|
// Size: 20,
|
||||||
|
// Columns: []query.Column{
|
||||||
|
// {
|
||||||
|
// serviceName: "age",
|
||||||
|
// Exp: ">",
|
||||||
|
// Value: 20,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// serviceName: "gender",
|
||||||
|
// Value: "男",
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
func (d *userExampleDao) GetByColumns(ctx context.Context, params *query.Params) ([]*model.UserExample, int64, error) {
|
||||||
|
query, args, err := params.ConvertToGormConditions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
err = d.db.WithContext(ctx).Model(&model.UserExample{}).Where(query, args...).Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return nil, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
records := []*model.UserExample{}
|
||||||
|
order, limit, offset := params.ConvertToPage()
|
||||||
|
err = d.db.WithContext(ctx).Order(order).Limit(limit).Offset(offset).Where(query, args...).Find(&records).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIDs 根据id批量获取
|
||||||
|
func (d *userExampleDao) GetByIDs(ctx context.Context, ids []uint64) ([]*model.UserExample, error) {
|
||||||
|
records := []*model.UserExample{}
|
||||||
|
|
||||||
|
itemMap, err := d.cache.MultiGet(ctx, ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var missedID []uint64
|
||||||
|
for _, id := range ids {
|
||||||
|
item, ok := itemMap[cast.ToString(id)]
|
||||||
|
if !ok {
|
||||||
|
missedID = append(missedID, id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
records = append(records, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get missed data
|
||||||
|
if len(missedID) > 0 {
|
||||||
|
var missedData []*model.UserExample
|
||||||
|
err = d.db.WithContext(ctx).Where("id IN (?)", missedID).Find(&missedData).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missedData) > 0 {
|
||||||
|
records = append(records, missedData...)
|
||||||
|
err = d.cache.MultiSet(ctx, missedData, 10*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package ecode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zhufuyi/sponge/pkg/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// rpc系统级别错误码,有status前缀
|
||||||
|
var (
|
||||||
|
StatusSuccess = errcode.StatusSuccess
|
||||||
|
|
||||||
|
StatusInvalidParams = errcode.StatusInvalidParams
|
||||||
|
StatusUnauthorized = errcode.StatusUnauthorized
|
||||||
|
StatusInternalServerError = errcode.StatusInternalServerError
|
||||||
|
StatusNotFound = errcode.StatusNotFound
|
||||||
|
StatusAlreadyExists = errcode.StatusAlreadyExists
|
||||||
|
StatusTimeout = errcode.StatusTimeout
|
||||||
|
StatusTooManyRequests = errcode.StatusTooManyRequests
|
||||||
|
StatusForbidden = errcode.StatusForbidden
|
||||||
|
StatusLimitExceed = errcode.StatusLimitExceed
|
||||||
|
|
||||||
|
StatusDeadlineExceeded = errcode.StatusDeadlineExceeded
|
||||||
|
StatusAccessDenied = errcode.StatusAccessDenied
|
||||||
|
StatusMethodNotAllowed = errcode.StatusMethodNotAllowed
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
package ecode
|
||||||
|
|
||||||
|
import "github.com/zhufuyi/sponge/pkg/errcode"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// todo must be modified manually
|
||||||
|
// 每个资源名称对应唯一编号,编号范围1~1000,如果存在编号相同,启动服务会报错
|
||||||
|
_userExampleNO = 1
|
||||||
|
// userExample对应的中文名称
|
||||||
|
_userExampleName = "userExample_cn_name"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// 服务级别错误码,有Err前缀
|
||||||
|
var (
|
||||||
|
StatusCreateUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+1, "创建"+_userExampleName+"失败") // todo 补充错误码注释,例如 20101
|
||||||
|
StatusDeleteUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+2, "删除"+_userExampleName+"失败")
|
||||||
|
StatusUpdateUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+3, "更新"+_userExampleName+"失败")
|
||||||
|
StatusGetUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+4, "获取"+_userExampleName+"失败")
|
||||||
|
StatusListUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+5, "获取"+_userExampleName+"列表失败")
|
||||||
|
// 每添加一个错误码,在上一个错误码基础上+1
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
package ecode
|
||||||
|
|
||||||
|
import "github.com/zhufuyi/sponge/pkg/errcode"
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// http系统级别错误码,无Err前缀,错误码小于20000
|
||||||
|
var (
|
||||||
|
Success = errcode.Success
|
||||||
|
InvalidParams = errcode.InvalidParams
|
||||||
|
Unauthorized = errcode.Unauthorized
|
||||||
|
InternalServerError = errcode.InternalServerError
|
||||||
|
NotFound = errcode.NotFound
|
||||||
|
AlreadyExists = errcode.AlreadyExists
|
||||||
|
Timeout = errcode.Timeout
|
||||||
|
TooManyRequests = errcode.TooManyRequests
|
||||||
|
Forbidden = errcode.Forbidden
|
||||||
|
LimitExceed = errcode.LimitExceed
|
||||||
|
|
||||||
|
DeadlineExceeded = errcode.DeadlineExceeded
|
||||||
|
AccessDenied = errcode.AccessDenied
|
||||||
|
MethodNotAllowed = errcode.MethodNotAllowed
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
package ecode
|
||||||
|
|
||||||
|
import "github.com/zhufuyi/sponge/pkg/errcode"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// todo must be modified manually
|
||||||
|
// 每个资源名称对应唯一编号,编号范围1~1000,如果存在编号相同,启动服务会报错
|
||||||
|
userExampleNO = 1
|
||||||
|
// userExample对应的中文名称
|
||||||
|
userExampleName = "userExample_cn_name"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// 服务级别错误码,有Err前缀
|
||||||
|
var (
|
||||||
|
ErrCreateUserExample = errcode.NewError(errcode.HCode(userExampleNO)+1, "创建"+userExampleName+"失败") // todo 补充错误码注释,例如 20101
|
||||||
|
ErrDeleteUserExample = errcode.NewError(errcode.HCode(userExampleNO)+2, "删除"+userExampleName+"失败")
|
||||||
|
ErrUpdateUserExample = errcode.NewError(errcode.HCode(userExampleNO)+3, "更新"+userExampleName+"失败")
|
||||||
|
ErrGetUserExample = errcode.NewError(errcode.HCode(userExampleNO)+4, "获取"+userExampleName+"失败")
|
||||||
|
ErrListUserExample = errcode.NewError(errcode.HCode(userExampleNO)+5, "获取"+userExampleName+"列表失败")
|
||||||
|
// 每添加一个错误码,在上一个错误码基础上+1
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
// swagger公共结构体,每个字段建议都写注释,生成的swagger.json也带有注释,
|
||||||
|
// 把swagger.json导入yapi后自动填写备注,避免重复填写
|
||||||
|
|
||||||
|
// Result 输出数据格式
|
||||||
|
type Result struct {
|
||||||
|
Code int `json:"code"` // 返回码
|
||||||
|
Msg string `json:"msg"` // 返回信息说明
|
||||||
|
Data interface{} `json:"data"` // 返回数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params 查询原始参数
|
||||||
|
type Params struct {
|
||||||
|
Page int `form:"page" binding:"gte=0" json:"page"` // 页码
|
||||||
|
Size int `form:"size" binding:"gt=0" json:"size"` // 每页行数
|
||||||
|
Sort string `form:"sort" binding:"" json:"sort,omitempty"` // 排序字段,默认值为-id,字段前面有-号表示倒序,否则升序,多个字段用逗号分隔
|
||||||
|
|
||||||
|
Columns []Column `json:"columns,omitempty"` // 列查询条件
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column 表的列查询信息
|
||||||
|
type Column struct {
|
||||||
|
Name string `json:"name"` // 列名
|
||||||
|
Exp string `json:"exp"` // 表达式,值为空时默认为=,有=、!=、>、>=、<、<=、like七种类型
|
||||||
|
Value interface{} `json:"value"` // 列值
|
||||||
|
Logic string `json:"logic"` // 逻辑类型,值为空时默认为and,有&(and)、||(or)两种类型
|
||||||
|
}
|
|
@ -0,0 +1,307 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/internal/cache"
|
||||||
|
"github.com/zhufuyi/sponge/internal/dao"
|
||||||
|
"github.com/zhufuyi/sponge/internal/ecode"
|
||||||
|
"github.com/zhufuyi/sponge/internal/model"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/mysql/query"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ UserExampleHandler = (*userExampleHandler)(nil)
|
||||||
|
|
||||||
|
// UserExampleHandler 定义handler接口
|
||||||
|
type UserExampleHandler interface {
|
||||||
|
Create(c *gin.Context)
|
||||||
|
DeleteByID(c *gin.Context)
|
||||||
|
UpdateByID(c *gin.Context)
|
||||||
|
GetByID(c *gin.Context)
|
||||||
|
List(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type userExampleHandler struct {
|
||||||
|
iDao dao.UserExampleDao
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserExampleHandler 创建handler接口
|
||||||
|
func NewUserExampleHandler() UserExampleHandler {
|
||||||
|
return &userExampleHandler{
|
||||||
|
iDao: dao.NewUserExampleDao(
|
||||||
|
model.GetDB(),
|
||||||
|
cache.NewUserExampleCache(model.GetRedisCli()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建
|
||||||
|
// @Summary 创建userExample
|
||||||
|
// @Description 提交信息创建userExample
|
||||||
|
// @Tags userExample
|
||||||
|
// @accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param data body CreateUserExampleRequest true "userExample信息"
|
||||||
|
// @Success 200 {object} Result{}
|
||||||
|
// @Router /api/v1/userExample [post]
|
||||||
|
func (h *userExampleHandler) Create(c *gin.Context) {
|
||||||
|
form := &CreateUserExampleRequest{}
|
||||||
|
err := c.ShouldBindJSON(form)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("ShouldBindJSON error: ", logger.Err(err), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.InvalidParams)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userExample := &model.UserExample{}
|
||||||
|
err = copier.Copy(userExample, form)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.InternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.iDao.Create(c.Request.Context(), userExample)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Create error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.ErrCreateUserExample)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{"id": userExample.ID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByID 根据id删除一条记录
|
||||||
|
// @Summary 删除userExample
|
||||||
|
// @Description 根据id删除userExample
|
||||||
|
// @Tags userExample
|
||||||
|
// @accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "id"
|
||||||
|
// @Success 200 {object} Result{}
|
||||||
|
// @Router /api/v1/userExample/{id} [delete]
|
||||||
|
func (h *userExampleHandler) DeleteByID(c *gin.Context) {
|
||||||
|
_, id, isAbout := getUserExampleIDFromPath(c)
|
||||||
|
if isAbout {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.iDao.DeleteByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("DeleteByID error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.ErrDeleteUserExample)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateByID 根据id更新信息
|
||||||
|
// @Summary 更新userExample信息
|
||||||
|
// @Description 根据id更新userExample信息
|
||||||
|
// @Tags userExample
|
||||||
|
// @accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "id"
|
||||||
|
// @Param data body UpdateUserExampleByIDRequest true "userExample信息"
|
||||||
|
// @Success 200 {object} Result{}
|
||||||
|
// @Router /api/v1/userExample/{id} [put]
|
||||||
|
func (h *userExampleHandler) UpdateByID(c *gin.Context) {
|
||||||
|
_, id, isAbout := getUserExampleIDFromPath(c)
|
||||||
|
if isAbout {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := &UpdateUserExampleByIDRequest{}
|
||||||
|
err := c.ShouldBindJSON(form)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("ShouldBindJSON error: ", logger.Err(err), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.InvalidParams)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.ID = id
|
||||||
|
|
||||||
|
userExample := &model.UserExample{}
|
||||||
|
err = copier.Copy(userExample, form)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.InternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.iDao.UpdateByID(c.Request.Context(), userExample)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("UpdateByID error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.ErrUpdateUserExample)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 根据id获取一条记录
|
||||||
|
// @Summary 获取userExample详情
|
||||||
|
// @Description 根据id获取userExample详情
|
||||||
|
// @Tags userExample
|
||||||
|
// @Param id path string true "id"
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} Result{}
|
||||||
|
// @Router /api/v1/userExample/{id} [get]
|
||||||
|
func (h *userExampleHandler) GetByID(c *gin.Context) {
|
||||||
|
idstr, id, isAbout := getUserExampleIDFromPath(c)
|
||||||
|
if isAbout {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userExample, err := h.iDao.GetByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == query.ErrNotFound.Error() {
|
||||||
|
logger.Warn("GetByID not found", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.NotFound)
|
||||||
|
} else {
|
||||||
|
logger.Error("GetByID error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.ErrGetUserExample)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &GetUserExampleByIDRespond{}
|
||||||
|
err = copier.Copy(data, userExample)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Copy error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.InternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.ID = idstr
|
||||||
|
|
||||||
|
response.Success(c, gin.H{"userExample": data})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 通过post获取多条记录
|
||||||
|
// @Summary 获取userExample列表
|
||||||
|
// @Description 使用post请求获取userExample列表
|
||||||
|
// @Tags userExample
|
||||||
|
// @accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param data body Params true "查询条件"
|
||||||
|
// @Success 200 {object} Result{}
|
||||||
|
// @Router /api/v1/userExamples [post]
|
||||||
|
func (h *userExampleHandler) List(c *gin.Context) {
|
||||||
|
form := &GetUserExamplesRequest{}
|
||||||
|
err := c.ShouldBindJSON(form)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("ShouldBindJSON error: ", logger.Err(err), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.InvalidParams)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userExamples, total, err := h.iDao.GetByColumns(c.Request.Context(), &form.Params)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("GetByColumns error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.ErrListUserExample)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := convertUserExamples(userExamples)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.InternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"userExamples": data,
|
||||||
|
"total": total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------- 定义请求参数和返回结果 -----------------------------
|
||||||
|
// todo generate the request and response struct to here
|
||||||
|
// delete the templates code start
|
||||||
|
|
||||||
|
// CreateUserExampleRequest 创建请求参数,所有字段是必须的,并且满足binding规则
|
||||||
|
// binding使用说明 https://github.com/go-playground/validator
|
||||||
|
type CreateUserExampleRequest struct {
|
||||||
|
Name string `json:"name" binding:"min=2"` // 名称
|
||||||
|
Email string `json:"email" binding:"email"` // 邮件
|
||||||
|
Password string `json:"password" binding:"md5"` // 密码
|
||||||
|
Phone string `form:"phone" binding:"e164"` // 手机号码,必须在前加'+86'
|
||||||
|
Avatar string `form:"avatar" binding:"min=5"` // 头像
|
||||||
|
Age int `form:"age" binding:"gt=0,lt=120"` // 年龄
|
||||||
|
Gender int `form:"gender" binding:"gte=0,lte=2"` // 性别,1:男,2:女
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserExampleByIDRequest 更新请求参数,所有字段不是必须的,字段为非零值更新
|
||||||
|
type UpdateUserExampleByIDRequest struct {
|
||||||
|
ID uint64 `json:"id" binding:"-"` // id
|
||||||
|
Name string `json:"name" binding:""` // 名称
|
||||||
|
Email string `json:"email" binding:""` // 邮件
|
||||||
|
Password string `json:"password" binding:""` // 密码
|
||||||
|
Phone string `form:"phone" binding:""` // 手机号码,必须在前加'+86'
|
||||||
|
Avatar string `form:"avatar" binding:""` // 头像
|
||||||
|
Age int `form:"age" binding:""` // 年龄
|
||||||
|
Gender int `form:"gender" binding:""` // 性别,1:男,2:女
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserExampleByIDRespond 返回数据
|
||||||
|
type GetUserExampleByIDRespond struct {
|
||||||
|
ID string `json:"id"` // id
|
||||||
|
Name string `json:"name"` // 名称
|
||||||
|
Email string `json:"email"` // 邮件
|
||||||
|
Phone string `json:"phone"` // 手机号码
|
||||||
|
Avatar string `json:"avatar"` // 头像
|
||||||
|
Age int `json:"age"` // 年龄
|
||||||
|
Gender int `json:"gender"` // 性别,1:男,2:女
|
||||||
|
Status int `json:"status"` // 账号状态
|
||||||
|
LoginAt int64 `json:"login_at"` // 登录时间戳
|
||||||
|
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||||
|
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the templates code end
|
||||||
|
|
||||||
|
// GetUserExamplesRequest query params
|
||||||
|
type GetUserExamplesRequest struct {
|
||||||
|
query.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUserExamplesRespond list data
|
||||||
|
type ListUserExamplesRespond []struct {
|
||||||
|
GetUserExampleByIDRespond
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------- 除了handler的辅助函数 -----------------------------
|
||||||
|
|
||||||
|
func getUserExampleIDFromPath(c *gin.Context) (string, uint64, bool) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := utils.StrToUint64E(idStr)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
logger.Warn("StrToUint64E error: ", logger.String("idStr", idStr), utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Error(c, ecode.InvalidParams)
|
||||||
|
return "", 0, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return idStr, id, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUserExamples(fromValues []*model.UserExample) ([]*GetUserExampleByIDRespond, error) {
|
||||||
|
toValues := []*GetUserExampleByIDRespond{}
|
||||||
|
for _, v := range fromValues {
|
||||||
|
data := &GetUserExampleByIDRespond{}
|
||||||
|
err := copier.Copy(data, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data.ID = utils.Uint64ToStr(v.ID)
|
||||||
|
toValues = append(toValues, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return toValues, nil
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/config"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/goredis"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/mysql"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrRecordNotFound 没有找到记录
|
||||||
|
ErrRecordNotFound = gorm.ErrRecordNotFound
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *gorm.DB
|
||||||
|
once1 sync.Once
|
||||||
|
|
||||||
|
redisCli *redis.Client
|
||||||
|
once2 sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitMysql 连接mysql
|
||||||
|
func InitMysql() {
|
||||||
|
opts := []mysql.Option{
|
||||||
|
mysql.WithSlowThreshold(time.Duration(config.Get().Mysql.SlowThreshold) * time.Millisecond),
|
||||||
|
mysql.WithMaxIdleConns(config.Get().Mysql.MaxIdleConns),
|
||||||
|
mysql.WithMaxOpenConns(config.Get().Mysql.MaxOpenConns),
|
||||||
|
mysql.WithConnMaxLifetime(time.Duration(config.Get().Mysql.ConnMaxLifetime) * time.Minute),
|
||||||
|
}
|
||||||
|
if config.Get().Mysql.EnableLog {
|
||||||
|
opts = append(opts, mysql.WithLog())
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Get().App.EnableTracing {
|
||||||
|
opts = append(opts, mysql.WithEnableTrace())
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
db, err = mysql.Init(config.Get().Mysql.Dsn, opts...)
|
||||||
|
if err != nil {
|
||||||
|
panic("mysql.Init error: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB 返回db对象
|
||||||
|
func GetDB() *gorm.DB {
|
||||||
|
if db == nil {
|
||||||
|
once1.Do(func() {
|
||||||
|
InitMysql()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseMysql 关闭mysql
|
||||||
|
func CloseMysql() error {
|
||||||
|
if db != nil {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sqlDB != nil {
|
||||||
|
return sqlDB.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitRedis 连接redis
|
||||||
|
func InitRedis() {
|
||||||
|
opts := []goredis.Option{}
|
||||||
|
if config.Get().App.EnableTracing {
|
||||||
|
opts = append(opts, goredis.WithEnableTrace())
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
redisCli, err = goredis.Init(config.Get().Redis.Dsn, opts...)
|
||||||
|
if err != nil {
|
||||||
|
panic("goredis.Init error: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRedisCli 返回redis client
|
||||||
|
func GetRedisCli() *redis.Client {
|
||||||
|
if redisCli == nil {
|
||||||
|
once2.Do(func() {
|
||||||
|
InitRedis()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return redisCli
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseRedis 关闭redis
|
||||||
|
func CloseRedis() error {
|
||||||
|
err := redisCli.Close()
|
||||||
|
if err != nil && err.Error() != redis.ErrClosed.Error() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// todo generate model codes to here
|
||||||
|
// delete the templates code start
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zhufuyi/sponge/pkg/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserExample object fields mapping table
|
||||||
|
type UserExample struct {
|
||||||
|
mysql.Model `gorm:"embedded"`
|
||||||
|
|
||||||
|
Name string `gorm:"column:name;NOT NULL" json:"name"` // 用户名
|
||||||
|
Password string `gorm:"column:password;NOT NULL" json:"password"` // 密码
|
||||||
|
Email string `gorm:"column:email;NOT NULL" json:"email"` // 邮件
|
||||||
|
Phone string `gorm:"column:phone;NOT NULL" json:"phone"` // 手机号码
|
||||||
|
Avatar string `gorm:"column:avatar;NOT NULL" json:"avatar"` // 头像
|
||||||
|
Age int `gorm:"column:age;NOT NULL" json:"age"` // 年龄
|
||||||
|
Gender int `gorm:"column:gender;NOT NULL" json:"gender"` // 性别,1:男,2:女,其他值:未知
|
||||||
|
Status int `gorm:"column:status;NOT NULL" json:"status"` // 账号状态,1:未激活,2:已激活,3:封禁
|
||||||
|
LoginAt int64 `gorm:"column:login_at;NOT NULL" json:"login_at"` // 登录时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName get table name
|
||||||
|
func (table *UserExample) TableName() string {
|
||||||
|
return "user_example"
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the templates code end
|
|
@ -0,0 +1,93 @@
|
||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/config"
|
||||||
|
"github.com/zhufuyi/sponge/docs"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/handlerfunc"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/middleware"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/middleware/metrics"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/middleware/ratelimiter"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/validator"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/pprof"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
routerFns []func() // 路由集合
|
||||||
|
apiV1 *gin.RouterGroup // 基础路由组
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRouter 实例化路由
|
||||||
|
func NewRouter() *gin.Engine {
|
||||||
|
r := gin.New()
|
||||||
|
|
||||||
|
r.Use(gin.Recovery())
|
||||||
|
r.Use(middleware.Cors())
|
||||||
|
|
||||||
|
// request id 中间件
|
||||||
|
r.Use(middleware.RequestID())
|
||||||
|
|
||||||
|
// logger 中间件
|
||||||
|
r.Use(middleware.Logging(
|
||||||
|
middleware.WithLog(logger.Get()),
|
||||||
|
middleware.WithRequestIDFromContext(),
|
||||||
|
middleware.WithIgnoreRoutes("/metrics"), // 忽略路由
|
||||||
|
))
|
||||||
|
|
||||||
|
// metrics 中间件
|
||||||
|
if config.Get().App.EnableMetrics {
|
||||||
|
r.Use(metrics.Metrics(r,
|
||||||
|
//metrics.WithMetricsPath("/metrics"), // 默认是 /metrics
|
||||||
|
metrics.WithIgnoreStatusCodes(http.StatusNotFound), // 忽略404状态码
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// limit 中间件
|
||||||
|
if config.Get().App.EnableLimit {
|
||||||
|
opts := []ratelimiter.Option{
|
||||||
|
ratelimiter.WithQPS(config.Get().RateLimiter.QPSLimit),
|
||||||
|
ratelimiter.WithBurst(config.Get().RateLimiter.MaxLimit),
|
||||||
|
}
|
||||||
|
if strings.ToUpper(config.Get().RateLimiter.Dimension) == "IP" {
|
||||||
|
opts = append(opts, ratelimiter.WithIP())
|
||||||
|
}
|
||||||
|
r.Use(ratelimiter.QPS(opts...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// trace 中间件
|
||||||
|
if config.Get().App.EnableTracing {
|
||||||
|
r.Use(middleware.Tracing(config.Get().App.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// profile 性能分析
|
||||||
|
if config.Get().App.EnableProfile {
|
||||||
|
pprof.Register(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验器
|
||||||
|
binding.Validator = validator.Init()
|
||||||
|
|
||||||
|
// 注册swagger路由,通过swag init生成代码
|
||||||
|
docs.SwaggerInfo.BasePath = ""
|
||||||
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
|
||||||
|
r.GET("/health", handlerfunc.CheckHealth)
|
||||||
|
r.GET("/ping", handlerfunc.Ping)
|
||||||
|
|
||||||
|
apiV1 = r.Group("/api/v1")
|
||||||
|
|
||||||
|
// 注册所有路由
|
||||||
|
for _, fn := range routerFns {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zhufuyi/sponge/internal/handler"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
func init() {
|
||||||
|
routerFns = append(routerFns, func() {
|
||||||
|
userExampleRouter(apiV1, handler.NewUserExampleHandler()) // 加入到路由组
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func userExampleRouter(group *gin.RouterGroup, h handler.UserExampleHandler) {
|
||||||
|
group.POST("/userExample", h.Create)
|
||||||
|
group.DELETE("/userExample/:id", h.DeleteByID)
|
||||||
|
group.PUT("/userExample/:id", h.UpdateByID)
|
||||||
|
group.GET("/userExample/:id", h.GetByID)
|
||||||
|
group.POST("/userExamples", h.List) // 通过post任意列组合查询
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/config"
|
||||||
|
"github.com/zhufuyi/sponge/internal/service"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/app"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/grpc/interceptor"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/grpc/metrics"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/registry"
|
||||||
|
|
||||||
|
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ app.IServer = (*grpcServer)(nil)
|
||||||
|
|
||||||
|
type grpcServer struct {
|
||||||
|
addr string
|
||||||
|
server *grpc.Server
|
||||||
|
listen net.Listener
|
||||||
|
|
||||||
|
metricsHTTPServer *http.Server
|
||||||
|
goRunPromHTTPServer func() error
|
||||||
|
|
||||||
|
iRegistry registry.Registry
|
||||||
|
serviceInstance *registry.ServiceInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start grpc service
|
||||||
|
func (s *grpcServer) Start() error {
|
||||||
|
if s.iRegistry != nil {
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
|
||||||
|
if err := s.iRegistry.Register(ctx, s.serviceInstance); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.goRunPromHTTPServer != nil {
|
||||||
|
if err := s.goRunPromHTTPServer(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.server.Serve(s.listen); err != nil { // block
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop grpc service
|
||||||
|
func (s *grpcServer) Stop() error {
|
||||||
|
if s.iRegistry != nil {
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
|
||||||
|
if err := s.iRegistry.Deregister(ctx, s.serviceInstance); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.server.GracefulStop()
|
||||||
|
|
||||||
|
if s.metricsHTTPServer != nil {
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
|
||||||
|
if err := s.metricsHTTPServer.Shutdown(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String comment
|
||||||
|
func (s *grpcServer) String() string {
|
||||||
|
return "grpc service, addr = " + s.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitServerOptions 初始化rpc设置
|
||||||
|
func (s *grpcServer) serverOptions() []grpc.ServerOption {
|
||||||
|
var options []grpc.ServerOption
|
||||||
|
|
||||||
|
unaryServerInterceptors := []grpc.UnaryServerInterceptor{
|
||||||
|
interceptor.UnaryServerRecovery(),
|
||||||
|
interceptor.UnaryServerCtxTags(),
|
||||||
|
}
|
||||||
|
|
||||||
|
streamServerInterceptors := []grpc.StreamServerInterceptor{}
|
||||||
|
|
||||||
|
// logger 拦截器
|
||||||
|
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerLog(
|
||||||
|
logger.Get(),
|
||||||
|
))
|
||||||
|
|
||||||
|
// metrics 拦截器
|
||||||
|
if config.Get().App.EnableMetrics {
|
||||||
|
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerMetrics())
|
||||||
|
s.goRunPromHTTPServer = func() error {
|
||||||
|
if s == nil || s.server == nil {
|
||||||
|
return errors.New("grpc server is nil")
|
||||||
|
}
|
||||||
|
promAddr := fmt.Sprintf(":%d", config.Get().Metrics.Port)
|
||||||
|
s.metricsHTTPServer = metrics.GoHTTPService(promAddr, s.server)
|
||||||
|
logger.Infof("start up prometheus http service, addr = %s", promAddr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// limit 拦截器
|
||||||
|
if config.Get().App.EnableLimit {
|
||||||
|
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerRateLimit(
|
||||||
|
interceptor.WithRateLimitQPS(config.Get().RateLimiter.QPSLimit),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// trace 拦截器
|
||||||
|
if config.Get().App.EnableTracing {
|
||||||
|
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerTracing())
|
||||||
|
}
|
||||||
|
|
||||||
|
unaryServer := grpc_middleware.WithUnaryServerChain(unaryServerInterceptors...)
|
||||||
|
streamServer := grpc_middleware.WithStreamServerChain(streamServerInterceptors...)
|
||||||
|
|
||||||
|
options = append(options, unaryServer, streamServer)
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGRPCServer 创建一个grpc服务
|
||||||
|
func NewGRPCServer(addr string, opts ...GRPCOption) app.IServer {
|
||||||
|
var err error
|
||||||
|
o := defaultGRPCOptions()
|
||||||
|
o.apply(opts...)
|
||||||
|
s := &grpcServer{
|
||||||
|
addr: addr,
|
||||||
|
iRegistry: o.iRegistry,
|
||||||
|
serviceInstance: o.instance,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听TCP端口
|
||||||
|
s.listen, err = net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建grpc server对象,拦截器可以在这里注入
|
||||||
|
s.server = grpc.NewServer(s.serverOptions()...)
|
||||||
|
|
||||||
|
// 注册所有服务
|
||||||
|
service.RegisterAllService(s.server)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GRPCOption 设置grpc
|
||||||
|
type GRPCOption func(*grpcOptions)
|
||||||
|
|
||||||
|
type grpcOptions struct {
|
||||||
|
readTimeout time.Duration
|
||||||
|
writeTimeout time.Duration
|
||||||
|
instance *registry.ServiceInstance
|
||||||
|
iRegistry registry.Registry
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认设置
|
||||||
|
func defaultGRPCOptions() *grpcOptions {
|
||||||
|
return &grpcOptions{
|
||||||
|
readTimeout: time.Second * 3,
|
||||||
|
writeTimeout: time.Second * 3,
|
||||||
|
instance: nil,
|
||||||
|
iRegistry: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *grpcOptions) apply(opts ...GRPCOption) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGRPCReadTimeout 设置read timeout
|
||||||
|
func WithGRPCReadTimeout(timeout time.Duration) GRPCOption {
|
||||||
|
return func(o *grpcOptions) {
|
||||||
|
o.readTimeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGRPCWriteTimeout 设置writer timeout
|
||||||
|
func WithGRPCWriteTimeout(timeout time.Duration) GRPCOption {
|
||||||
|
return func(o *grpcOptions) {
|
||||||
|
o.writeTimeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRegistry 设置registry
|
||||||
|
func WithRegistry(iRegistry registry.Registry, instance *registry.ServiceInstance) GRPCOption {
|
||||||
|
return func(o *grpcOptions) {
|
||||||
|
o.iRegistry = iRegistry
|
||||||
|
o.instance = instance
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/internal/routers"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/app"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ app.IServer = (*httpServer)(nil)
|
||||||
|
|
||||||
|
type httpServer struct {
|
||||||
|
addr string
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start http service
|
||||||
|
func (s *httpServer) Start() error {
|
||||||
|
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
return fmt.Errorf("listen server error: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop http service
|
||||||
|
func (s *httpServer) Stop() error {
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
|
||||||
|
return s.server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String comment
|
||||||
|
func (s *httpServer) String() string {
|
||||||
|
return "http service, addr = " + s.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPServer creates a new web server
|
||||||
|
func NewHTTPServer(addr string, opts ...HTTPOption) app.IServer {
|
||||||
|
o := defaultHTTPOptions()
|
||||||
|
o.apply(opts...)
|
||||||
|
|
||||||
|
if o.isProd {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
} else {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := routers.NewRouter()
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: router,
|
||||||
|
ReadTimeout: o.readTimeout,
|
||||||
|
WriteTimeout: o.writeTimeout,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &httpServer{
|
||||||
|
addr: addr,
|
||||||
|
server: server,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPOption 设置http
|
||||||
|
type HTTPOption func(*httpOptions)
|
||||||
|
|
||||||
|
type httpOptions struct {
|
||||||
|
readTimeout time.Duration
|
||||||
|
writeTimeout time.Duration
|
||||||
|
isProd bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认设置
|
||||||
|
func defaultHTTPOptions() *httpOptions {
|
||||||
|
return &httpOptions{
|
||||||
|
readTimeout: time.Second * 60,
|
||||||
|
writeTimeout: time.Second * 60,
|
||||||
|
isProd: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *httpOptions) apply(opts ...HTTPOption) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHTTPReadTimeout 设置read timeout
|
||||||
|
func WithHTTPReadTimeout(timeout time.Duration) HTTPOption {
|
||||||
|
return func(o *httpOptions) {
|
||||||
|
o.readTimeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHTTPWriteTimeout 设置writer timeout
|
||||||
|
func WithHTTPWriteTimeout(timeout time.Duration) HTTPOption {
|
||||||
|
return func(o *httpOptions) {
|
||||||
|
o.writeTimeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHTTPIsProd 设置是否为生产环境
|
||||||
|
func WithHTTPIsProd(IsProd bool) HTTPOption {
|
||||||
|
return func(o *httpOptions) {
|
||||||
|
o.isProd = IsProd
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/health"
|
||||||
|
healthPB "google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// registerFns 注册方法集合
|
||||||
|
registerFns []func(server *grpc.Server)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterAllService 注册所有service到服务中
|
||||||
|
func RegisterAllService(server *grpc.Server) {
|
||||||
|
healthPB.RegisterHealthServer(server, health.NewServer()) // 注册健康检测
|
||||||
|
|
||||||
|
for _, fn := range registerFns {
|
||||||
|
fn(server)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
pb "github.com/zhufuyi/sponge/api/userExample/v1"
|
||||||
|
"github.com/zhufuyi/sponge/internal/cache"
|
||||||
|
"github.com/zhufuyi/sponge/internal/dao"
|
||||||
|
"github.com/zhufuyi/sponge/internal/ecode"
|
||||||
|
"github.com/zhufuyi/sponge/internal/model"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/mysql/query"
|
||||||
|
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
func init() {
|
||||||
|
registerFns = append(registerFns, func(server *grpc.Server) {
|
||||||
|
pb.RegisterUserExampleServiceServer(server, NewUserExampleServiceServer()) // 把service注册到rpc服务中
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ pb.UserExampleServiceServer = (*userExampleService)(nil)
|
||||||
|
|
||||||
|
type userExampleService struct {
|
||||||
|
pb.UnimplementedUserExampleServiceServer
|
||||||
|
|
||||||
|
iDao dao.UserExampleDao
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserExampleServiceServer 创建一个实例
|
||||||
|
func NewUserExampleServiceServer() pb.UserExampleServiceServer {
|
||||||
|
return &userExampleService{
|
||||||
|
iDao: dao.NewUserExampleDao(
|
||||||
|
model.GetDB(),
|
||||||
|
cache.NewUserExampleCache(model.GetRedisCli()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *userExampleService) Create(ctx context.Context, req *pb.CreateUserExampleRequest) (*pb.CreateUserExampleReply, error) {
|
||||||
|
err := req.Validate()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
|
||||||
|
return nil, ecode.StatusInvalidParams.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
userExample := &model.UserExample{}
|
||||||
|
err = copier.Copy(userExample, req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("req", req))
|
||||||
|
return nil, ecode.StatusInternalServerError.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.iDao.Create(ctx, userExample)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("s.iDao.Create error", logger.Err(err), logger.Any("userExample", userExample))
|
||||||
|
return nil, ecode.StatusCreateUserExample.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.CreateUserExampleReply{Id: userExample.ID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *userExampleService) DeleteByID(ctx context.Context, req *pb.DeleteUserExampleByIDRequest) (*pb.DeleteUserExampleByIDReply, error) {
|
||||||
|
err := req.Validate()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
|
||||||
|
return nil, ecode.StatusInvalidParams.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.iDao.DeleteByID(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("s.iDao.DeleteByID error", logger.Err(err), logger.Any("id", req.Id))
|
||||||
|
return nil, ecode.StatusDeleteUserExample.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.DeleteUserExampleByIDReply{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *userExampleService) UpdateByID(ctx context.Context, req *pb.UpdateUserExampleByIDRequest) (*pb.UpdateUserExampleByIDReply, error) {
|
||||||
|
err := req.Validate()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
|
||||||
|
return nil, ecode.StatusInvalidParams.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
userExample := &model.UserExample{}
|
||||||
|
err = copier.Copy(userExample, req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("req", req))
|
||||||
|
return nil, ecode.StatusInternalServerError.Err()
|
||||||
|
}
|
||||||
|
userExample.ID = req.Id
|
||||||
|
|
||||||
|
err = s.iDao.UpdateByID(ctx, userExample)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("s.iDao.UpdateByID error", logger.Err(err), logger.Any("userExample", userExample))
|
||||||
|
return nil, ecode.StatusUpdateUserExample.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.UpdateUserExampleByIDReply{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *userExampleService) GetByID(ctx context.Context, req *pb.GetUserExampleByIDRequest) (*pb.GetUserExampleByIDReply, error) {
|
||||||
|
err := req.Validate()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
|
||||||
|
return nil, ecode.StatusInvalidParams.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := s.iDao.GetByID(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == query.ErrNotFound.Error() {
|
||||||
|
logger.Warn("s.iDao.GetByID error", logger.Err(err), logger.Any("id", req.Id))
|
||||||
|
return nil, ecode.StatusNotFound.Err()
|
||||||
|
}
|
||||||
|
logger.Error("s.iDao.GetByID error", logger.Err(err), logger.Any("id", req.Id))
|
||||||
|
return nil, ecode.StatusGetUserExample.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := covertUserExample(record)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("covertUserExample error", logger.Err(err), logger.Any("record", record))
|
||||||
|
return nil, ecode.StatusInternalServerError.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.GetUserExampleByIDReply{UserExample: value}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *userExampleService) List(ctx context.Context, req *pb.ListUserExampleRequest) (*pb.ListUserExampleReply, error) {
|
||||||
|
err := req.Validate()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
|
||||||
|
return nil, ecode.StatusInvalidParams.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &query.Params{}
|
||||||
|
err = copier.Copy(params, req.Params)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("params", req.Params))
|
||||||
|
return nil, ecode.StatusInternalServerError.Err()
|
||||||
|
}
|
||||||
|
params.Size = int(req.Params.Limit)
|
||||||
|
|
||||||
|
records, total, err := s.iDao.GetByColumns(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("s.iDao.GetByColumns error", logger.Err(err), logger.Any("params", params))
|
||||||
|
return nil, ecode.StatusListUserExample.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
values := []*pb.UserExample{}
|
||||||
|
for _, record := range records {
|
||||||
|
value, err := covertUserExample(record)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("covertUserExample error", logger.Err(err), logger.Any("id", record.ID))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListUserExampleReply{
|
||||||
|
Total: total,
|
||||||
|
UserExamples: values,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func covertUserExample(record *model.UserExample) (*pb.UserExample, error) {
|
||||||
|
value := &pb.UserExample{}
|
||||||
|
err := copier.Copy(value, record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
value.Id = record.ID
|
||||||
|
value.CreatedAt = record.CreatedAt.UnixNano()
|
||||||
|
value.UpdatedAt = record.UpdatedAt.UnixNano()
|
||||||
|
return value, nil
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/api/types"
|
||||||
|
pb "github.com/zhufuyi/sponge/api/userExample/v1"
|
||||||
|
"github.com/zhufuyi/sponge/config"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/grpc/benchmark"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initUserExampleServiceClient() pb.UserExampleServiceClient {
|
||||||
|
err := config.Init(config.Path("conf.yml"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%d", config.Get().Grpc.Port)
|
||||||
|
|
||||||
|
conn, err := grpc.Dial(addr,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
//defer conn.Close()
|
||||||
|
|
||||||
|
return pb.NewUserExampleServiceClient(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过客户端测试userExample的各个方法
|
||||||
|
func Test_userExampleService_methods(t *testing.T) {
|
||||||
|
cli := initUserExampleServiceClient()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fn func() (interface{}, error)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// todo generate the service struct code here
|
||||||
|
// delete the templates code start
|
||||||
|
{
|
||||||
|
name: "Create",
|
||||||
|
fn: func() (interface{}, error) {
|
||||||
|
// todo test after filling in parameters
|
||||||
|
return cli.Create(ctx, &pb.CreateUserExampleRequest{
|
||||||
|
Name: "宋九",
|
||||||
|
Email: "foo7@bar.com",
|
||||||
|
Password: "f447b20a7fcbf53a5d5be013ea0b15af",
|
||||||
|
Phone: "+8618576552066",
|
||||||
|
Avatar: "http://internal.com/7.jpg",
|
||||||
|
Age: 21,
|
||||||
|
Gender: 2,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "UpdateByID",
|
||||||
|
fn: func() (interface{}, error) {
|
||||||
|
// todo test after filling in parameters
|
||||||
|
return cli.UpdateByID(ctx, &pb.UpdateUserExampleByIDRequest{
|
||||||
|
Id: 7,
|
||||||
|
Phone: "18666666666",
|
||||||
|
Age: 21,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
// delete the templates code end
|
||||||
|
{
|
||||||
|
name: "DeleteByID",
|
||||||
|
fn: func() (interface{}, error) {
|
||||||
|
// todo test after filling in parameters
|
||||||
|
return cli.DeleteByID(ctx, &pb.DeleteUserExampleByIDRequest{
|
||||||
|
Id: 3,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "GetByID",
|
||||||
|
fn: func() (interface{}, error) {
|
||||||
|
// todo test after filling in parameters
|
||||||
|
return cli.GetByID(ctx, &pb.GetUserExampleByIDRequest{
|
||||||
|
Id: 3,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "List",
|
||||||
|
fn: func() (interface{}, error) {
|
||||||
|
// todo test after filling in parameters
|
||||||
|
return cli.List(ctx, &pb.ListUserExampleRequest{
|
||||||
|
Params: &types.Params{
|
||||||
|
Page: 0,
|
||||||
|
Limit: 10,
|
||||||
|
Sort: "",
|
||||||
|
Columns: []*types.Column{
|
||||||
|
{
|
||||||
|
Name: "id",
|
||||||
|
Exp: "<",
|
||||||
|
Value: "100",
|
||||||
|
Logic: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.fn()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("test '%s' error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Log("reply data: ", got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 压测userExample的各个方法,完成后复制报告路径到浏览器查看
|
||||||
|
func Test_userExampleService_benchmark(t *testing.T) {
|
||||||
|
err := config.Init(config.Path("conf.yml"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
host := fmt.Sprintf("127.0.0.1:%d", config.Get().Grpc.Port)
|
||||||
|
protoFile := config.Path("../api/userExample/v1/userExample.proto")
|
||||||
|
// 如果压测过程中缺少第三方依赖,复制到项目的third_party目录下(不包括import路径)
|
||||||
|
importPaths := []string{
|
||||||
|
config.Path("../third_party"), // third_party目录
|
||||||
|
config.Path(".."), // third_party的上一级目录
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fn func() error
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GetByID",
|
||||||
|
fn: func() error {
|
||||||
|
// todo test after filling in parameters
|
||||||
|
message := &pb.GetUserExampleByIDRequest{
|
||||||
|
Id: 3,
|
||||||
|
}
|
||||||
|
b, err := benchmark.New(host, protoFile, "GetByID", message, 100, importPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.Run()
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "List",
|
||||||
|
fn: func() error {
|
||||||
|
// todo test after filling in parameters
|
||||||
|
message := &pb.ListUserExampleRequest{
|
||||||
|
Params: &types.Params{
|
||||||
|
Page: 0,
|
||||||
|
Limit: 10,
|
||||||
|
Sort: "",
|
||||||
|
Columns: []*types.Column{
|
||||||
|
{
|
||||||
|
Name: "age",
|
||||||
|
Exp: ">=",
|
||||||
|
Value: "15",
|
||||||
|
Logic: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := benchmark.New(host, protoFile, "List", message, 100, importPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.Run()
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.fn()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("test '%s' error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
## app
|
||||||
|
|
||||||
|
优雅的启动和停止服务,使用[errgroup](golang.org/x/sync/errgroup)保证多个服务同时正常启动。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
> go get -u github.com/zhufuyi/pkg/app
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
inits := registerInits()
|
||||||
|
servers := registerServers()
|
||||||
|
closes := registerCloses(servers)
|
||||||
|
|
||||||
|
s := app.New(inits, servers, closes)
|
||||||
|
s.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerInits() []app.Init {
|
||||||
|
// 读取配置文件
|
||||||
|
|
||||||
|
var inits []app.Init
|
||||||
|
|
||||||
|
// 初始化日志
|
||||||
|
inits = append(inits, func() {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
inits = append(inits, func() {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// ......
|
||||||
|
|
||||||
|
return inits
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerServers() []app.IServer {
|
||||||
|
var servers []app.IServer
|
||||||
|
|
||||||
|
// 创建http服务
|
||||||
|
servers = append(servers, server.NewHTTPServer(
|
||||||
|
|
||||||
|
))
|
||||||
|
|
||||||
|
// 创建grpc服务
|
||||||
|
servers = append(servers, server.NewGRPCServer(
|
||||||
|
|
||||||
|
))
|
||||||
|
|
||||||
|
// ......
|
||||||
|
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerCloses(servers []app.IServer) []app.Close {
|
||||||
|
var closes []app.Close
|
||||||
|
|
||||||
|
// 关闭服务
|
||||||
|
for _, server := range servers {
|
||||||
|
closes = append(closes, server.Stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭数据库连接
|
||||||
|
closes = append(closes, func() error {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// ......
|
||||||
|
|
||||||
|
return closes
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,103 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init app initialization
|
||||||
|
type Init func()
|
||||||
|
|
||||||
|
// Close app close
|
||||||
|
type Close func() error
|
||||||
|
|
||||||
|
// IServer http or grpc server interface
|
||||||
|
type IServer interface {
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// App servers
|
||||||
|
type App struct {
|
||||||
|
inits []Init
|
||||||
|
servers []IServer
|
||||||
|
closes []Close
|
||||||
|
}
|
||||||
|
|
||||||
|
// New create an app
|
||||||
|
func New(inits []Init, servers []IServer, closes []Close) *App {
|
||||||
|
for _, init := range inits {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
inits: inits,
|
||||||
|
servers: servers,
|
||||||
|
closes: closes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run servers
|
||||||
|
func (a *App) Run() {
|
||||||
|
// ctx will be notified whenever an error occurs in one of the goroutines
|
||||||
|
eg, ctx := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
|
// start all servers
|
||||||
|
for _, server := range a.servers {
|
||||||
|
s := server
|
||||||
|
eg.Go(func() error {
|
||||||
|
logger.Infof("start up %s", s.String())
|
||||||
|
if err := s.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch and stop app
|
||||||
|
eg.Go(func() error {
|
||||||
|
return a.watch(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch the os signal and the ctx signal from the errgroup, and stop the service if either signal is triggered
|
||||||
|
func (a *App) watch(ctx context.Context) error {
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done(): // service error signals
|
||||||
|
_ = a.stop()
|
||||||
|
return ctx.Err()
|
||||||
|
case s := <-quit: // system notification signal
|
||||||
|
logger.Infof("receive a quit signal: %s", s.String())
|
||||||
|
if err := a.stop(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Infof("stop app successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopping services and releasing resources
|
||||||
|
func (a *App) stop() error {
|
||||||
|
for _, closeFn := range a.closes {
|
||||||
|
if err := closeFn(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
## cache
|
||||||
|
|
||||||
|
内存类型的有:memory和NoSQL
|
||||||
|
|
||||||
|
NoSQL的主要有: redis
|
||||||
|
|
||||||
|
各类库只要实现了cache定义的接口(driver)即可。
|
||||||
|
> 这里的接口driver命名参考了Go官方mysql接口的命名规范
|
||||||
|
|
||||||
|
## 多级缓存
|
||||||
|
|
||||||
|
### 二级缓存
|
||||||
|
|
||||||
|
这里的多级主要是指二级缓存:本地缓存(一级缓存L1)+redis缓存(二级缓存L2)
|
||||||
|
使用本地缓存可以减少应用服务器到redis之间的网络I/O开销
|
||||||
|
|
||||||
|
> 需要注意的是:在并发量不大的系统内,本地缓存的意义不大,反而增加维护的困难。但在高并发系统中,
|
||||||
|
> 本地缓存可以大大节约带宽。但是要注意本地缓存不是银弹,它会引起多个副本间数据的
|
||||||
|
> 不一致,还会占据大量的内存,所以不适合保存特别大的数据,而且需要严格考虑刷新机制。
|
||||||
|
|
||||||
|
### 过期时间
|
||||||
|
|
||||||
|
本地缓存过期时间比分布式缓存小至少一半,以防止本地缓存太久造成多实例数据不一致。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
// GetByID 根据id获取一条记录
|
||||||
|
func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExample, error) {
|
||||||
|
record, err := d.cache.Get(ctx, id)
|
||||||
|
|
||||||
|
if errors.Is(err, cacheBase.ErrPlaceholder) {
|
||||||
|
return nil, model.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从mysql获取
|
||||||
|
if errors.Is(err, goredis.ErrRedisNotFound) {
|
||||||
|
table := &model.UserExample{}
|
||||||
|
err := d.db.WithContext(ctx).Where("id = ?", id).First(table).Error
|
||||||
|
if err != nil {
|
||||||
|
// if data is empty, set not found cache to prevent cache penetration(防止缓存穿透)
|
||||||
|
if err.Error() == model.ErrRecordNotFound.Error() {
|
||||||
|
err = d.cache.SetCacheWithNotFound(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, model.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if table.ID == 0 {
|
||||||
|
return nil, model.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// set cache
|
||||||
|
err = d.cache.Set(ctx, id, table, cacheBase.DefaultExpireTime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cache.Set error: %v, id=%d", err, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// fail fast, if cache error return, don't request to db
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 缓存问题
|
||||||
|
|
||||||
|
需要注意以下几个问题:
|
||||||
|
|
||||||
|
- 缓存穿透
|
||||||
|
- 缓存击穿
|
||||||
|
- 缓存雪崩
|
||||||
|
|
||||||
|
可以参考文章:[Redis缓存三大问题](https://mp.weixin.qq.com/s/HjzwefprYSGraU1aJcJ25g)
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
- ristretto:https://github.com/dgraph-io/ristretto (号称最快的本地缓存)
|
||||||
|
- [Ristretto简介:高性能Go缓存](https://www.yuque.com/kshare/2020/ade1d9b5-5925-426a-9566-3a5587af2181)
|
||||||
|
- bigcache: https://github.com/allegro/bigcache
|
||||||
|
- freecache: https://github.com/coocood/freecache
|
||||||
|
- concurrent_map: https://github.com/easierway/concurrent_map
|
||||||
|
- gocache: https://github.com/eko/gocache (Built-in stores, eg: bigcache,memcache,redis)
|
|
@ -0,0 +1,64 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultExpireTime 默认过期时间
|
||||||
|
DefaultExpireTime = time.Hour * 24
|
||||||
|
// DefaultNotFoundExpireTime 结果为空时的过期时间 1分钟, 常用于数据为空时的缓存时间(缓存穿透)
|
||||||
|
DefaultNotFoundExpireTime = time.Minute
|
||||||
|
// NotFoundPlaceholder 占位符
|
||||||
|
NotFoundPlaceholder = "*"
|
||||||
|
|
||||||
|
// DefaultClient 生成一个缓存客户端,其中keyPrefix 一般为业务前缀
|
||||||
|
DefaultClient Cache
|
||||||
|
|
||||||
|
// ErrPlaceholder .
|
||||||
|
ErrPlaceholder = errors.New("cache: placeholder")
|
||||||
|
// ErrSetMemoryWithNotFound .
|
||||||
|
ErrSetMemoryWithNotFound = errors.New("cache: set memory cache err for not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache 定义cache驱动接口
|
||||||
|
type Cache interface {
|
||||||
|
Set(ctx context.Context, key string, val interface{}, expiration time.Duration) error
|
||||||
|
Get(ctx context.Context, key string, val interface{}) error
|
||||||
|
MultiSet(ctx context.Context, valMap map[string]interface{}, expiration time.Duration) error
|
||||||
|
MultiGet(ctx context.Context, keys []string, valueMap interface{}) error
|
||||||
|
Del(ctx context.Context, keys ...string) error
|
||||||
|
SetCacheWithNotFound(ctx context.Context, key string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 数据
|
||||||
|
func Set(ctx context.Context, key string, val interface{}, expiration time.Duration) error {
|
||||||
|
return DefaultClient.Set(ctx, key, val, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 数据
|
||||||
|
func Get(ctx context.Context, key string, val interface{}) error {
|
||||||
|
return DefaultClient.Get(ctx, key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiSet 批量set
|
||||||
|
func MultiSet(ctx context.Context, valMap map[string]interface{}, expiration time.Duration) error {
|
||||||
|
return DefaultClient.MultiSet(ctx, valMap, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiGet 批量获取
|
||||||
|
func MultiGet(ctx context.Context, keys []string, valueMap interface{}) error {
|
||||||
|
return DefaultClient.MultiGet(ctx, keys, valueMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del 批量删除
|
||||||
|
func Del(ctx context.Context, keys ...string) error {
|
||||||
|
return DefaultClient.Del(ctx, keys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheWithNotFound .
|
||||||
|
func SetCacheWithNotFound(ctx context.Context, key string) error {
|
||||||
|
return DefaultClient.SetCacheWithNotFound(ctx, key)
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/encoding"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
|
||||||
|
"github.com/dgraph-io/ristretto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryCache struct {
|
||||||
|
client *ristretto.Cache
|
||||||
|
KeyPrefix string
|
||||||
|
encoding encoding.Encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemoryCache create a memory cache
|
||||||
|
func NewMemoryCache(keyPrefix string, encoding encoding.Encoding) Cache {
|
||||||
|
// see: https://dgraph.io/blog/post/introducing-ristretto-high-perf-go-cache/
|
||||||
|
// https://www.start.io/blog/we-chose-ristretto-cache-for-go-heres-why/
|
||||||
|
config := &ristretto.Config{
|
||||||
|
NumCounters: 1e7, // number of keys to track frequency of (10M).
|
||||||
|
MaxCost: 1 << 30, // maximum cost of cache (1GB).
|
||||||
|
BufferItems: 64, // number of keys per Get buffer.
|
||||||
|
}
|
||||||
|
store, _ := ristretto.NewCache(config)
|
||||||
|
return &memoryCache{
|
||||||
|
client: store,
|
||||||
|
KeyPrefix: keyPrefix,
|
||||||
|
encoding: encoding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set add cache
|
||||||
|
func (m *memoryCache) Set(ctx context.Context, key string, val interface{}, expiration time.Duration) error {
|
||||||
|
buf, err := encoding.Marshal(m.encoding, val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encoding.Marshal error: %v, key=%s, val=%+v ", err, key, val)
|
||||||
|
}
|
||||||
|
cacheKey, err := BuildCacheKey(m.KeyPrefix, key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
|
||||||
|
}
|
||||||
|
m.client.SetWithTTL(cacheKey, buf, 0, expiration)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get data
|
||||||
|
func (m *memoryCache) Get(ctx context.Context, key string, val interface{}) error {
|
||||||
|
cacheKey, err := BuildCacheKey(m.KeyPrefix, key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
|
||||||
|
}
|
||||||
|
data, ok := m.client.Get(cacheKey)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if data == NotFoundPlaceholder {
|
||||||
|
return ErrPlaceholder
|
||||||
|
}
|
||||||
|
err = encoding.Unmarshal(m.encoding, data.([]byte), val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encoding.Unmarshal error: %v, key=%s, cacheKey=%s, type=%v, json=%+v ",
|
||||||
|
err, key, cacheKey, reflect.TypeOf(val), string(data.([]byte)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del 删除
|
||||||
|
func (m *memoryCache) Del(ctx context.Context, keys ...string) error {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key := keys[0]
|
||||||
|
cacheKey, err := BuildCacheKey(m.KeyPrefix, key)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("build cache key error", logger.Err(err), logger.String("key", key))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.client.Del(cacheKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiSet 批量set
|
||||||
|
func (m *memoryCache) MultiSet(ctx context.Context, valMap map[string]interface{}, expiration time.Duration) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiGet 批量获取
|
||||||
|
func (m *memoryCache) MultiGet(ctx context.Context, keys []string, val interface{}) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryCache) SetCacheWithNotFound(ctx context.Context, key string) error {
|
||||||
|
if m.client.Set(key, NotFoundPlaceholder, int64(DefaultNotFoundExpireTime)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ErrSetMemoryWithNotFound
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/encoding"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_NewMemoryCache(t *testing.T) {
|
||||||
|
asserts := assert.New(t)
|
||||||
|
|
||||||
|
client := NewMemoryCache("memory-unit-test", encoding.JSONEncoding{})
|
||||||
|
asserts.NotNil(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoStore_Set(t *testing.T) {
|
||||||
|
asserts := assert.New(t)
|
||||||
|
|
||||||
|
store := NewMemoryCache("memory-unit-test", encoding.JSONEncoding{})
|
||||||
|
err := store.Set(context.Background(), "test-key", "test-val", -1)
|
||||||
|
asserts.NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoStore_Get(t *testing.T) {
|
||||||
|
asserts := assert.New(t)
|
||||||
|
store := NewMemoryCache("memory-unit-test", encoding.JSONEncoding{})
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 正常情况
|
||||||
|
{
|
||||||
|
var gotVal string
|
||||||
|
setVal := "test-val"
|
||||||
|
err := store.Set(ctx, "test-get-key", setVal, 3600)
|
||||||
|
asserts.NoError(err)
|
||||||
|
err = store.Get(ctx, "test-get-key", &gotVal)
|
||||||
|
asserts.NoError(err)
|
||||||
|
t.Log(setVal, gotVal)
|
||||||
|
asserts.Equal(setVal, gotVal)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,200 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/encoding"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// redisCache redis cache结构体
|
||||||
|
type redisCache struct {
|
||||||
|
client *redis.Client
|
||||||
|
KeyPrefix string
|
||||||
|
encoding encoding.Encoding
|
||||||
|
DefaultExpireTime time.Duration
|
||||||
|
newObject func() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRedisCache new一个cache, client 参数是可传入的,方便进行单元测试
|
||||||
|
func NewRedisCache(client *redis.Client, keyPrefix string, encoding encoding.Encoding, newObject func() interface{}) Cache {
|
||||||
|
return &redisCache{
|
||||||
|
client: client,
|
||||||
|
KeyPrefix: keyPrefix,
|
||||||
|
encoding: encoding,
|
||||||
|
newObject: newObject,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) Set(ctx context.Context, key string, val interface{}, expiration time.Duration) error {
|
||||||
|
buf, err := encoding.Marshal(c.encoding, val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encoding.Marshal error: %v, key=%s, val=%+v ", err, key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
|
||||||
|
}
|
||||||
|
if expiration == 0 {
|
||||||
|
expiration = DefaultExpireTime
|
||||||
|
}
|
||||||
|
err = c.client.Set(ctx, cacheKey, buf, expiration).Err()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("c.client.Set error: %v, cacheKey=%s", err, cacheKey)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) Get(ctx context.Context, key string, val interface{}) error {
|
||||||
|
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := c.client.Get(ctx, cacheKey).Bytes()
|
||||||
|
// NOTE: don't handle the case where redis value is nil
|
||||||
|
// but leave it to the upstream for processing if need
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止data为空时,Unmarshal报错
|
||||||
|
if string(bytes) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if string(bytes) == NotFoundPlaceholder {
|
||||||
|
return ErrPlaceholder
|
||||||
|
}
|
||||||
|
err = encoding.Unmarshal(c.encoding, bytes, val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encoding.Unmarshal error: %v, key=%s, cacheKey=%s, type=%v, json=%+v ",
|
||||||
|
err, key, cacheKey, reflect.TypeOf(val), string(bytes))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) MultiSet(ctx context.Context, valueMap map[string]interface{}, expiration time.Duration) error {
|
||||||
|
if len(valueMap) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if expiration == 0 {
|
||||||
|
expiration = DefaultExpireTime
|
||||||
|
}
|
||||||
|
// key-value是成对的,所以这里的容量是map的2倍
|
||||||
|
paris := make([]interface{}, 0, 2*len(valueMap))
|
||||||
|
for key, value := range valueMap {
|
||||||
|
buf, err := encoding.Marshal(c.encoding, value)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("marshal data error", logger.Err(err), logger.Any("value", value))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("build cache key error", logger.Err(err), logger.String("key", key))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
paris = append(paris, []byte(cacheKey))
|
||||||
|
paris = append(paris, buf)
|
||||||
|
}
|
||||||
|
pipeline := c.client.Pipeline()
|
||||||
|
err := pipeline.MSet(ctx, paris...).Err()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pipeline.MSet error: %v", err)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(paris); i = i + 2 {
|
||||||
|
switch paris[i].(type) {
|
||||||
|
case []byte:
|
||||||
|
pipeline.Expire(ctx, string(paris[i].([]byte)), expiration)
|
||||||
|
default:
|
||||||
|
logger.Warnf("redis expire is unsupported key type: %+v", reflect.TypeOf(paris[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = pipeline.Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pipeline.Exec error: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) MultiGet(ctx context.Context, keys []string, value interface{}) error {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cacheKeys := make([]string, len(keys))
|
||||||
|
for index, key := range keys {
|
||||||
|
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
|
||||||
|
}
|
||||||
|
cacheKeys[index] = cacheKey
|
||||||
|
}
|
||||||
|
values, err := c.client.MGet(ctx, cacheKeys...).Result()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("c.client.MGet error: %v, keys=%+v", err, cacheKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过反射注入到map
|
||||||
|
valueMap := reflect.ValueOf(value)
|
||||||
|
for i, value := range values {
|
||||||
|
if value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
object := c.newObject()
|
||||||
|
err = encoding.Unmarshal(c.encoding, []byte(value.(string)), object)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("unmarshal data error: %+v, key=%s, cacheKey=%s type=%v", err,
|
||||||
|
keys[i], cacheKeys[i], reflect.TypeOf(value))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
valueMap.SetMapIndex(reflect.ValueOf(cacheKeys[i]), reflect.ValueOf(object))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) Del(ctx context.Context, keys ...string) error {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量构建cacheKey
|
||||||
|
cacheKeys := make([]string, len(keys))
|
||||||
|
for index, key := range keys {
|
||||||
|
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("build cache key err: %+v, key is %+v", err, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cacheKeys[index] = cacheKey
|
||||||
|
}
|
||||||
|
err := c.client.Del(ctx, cacheKeys...).Err()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("c.client.Del error: %v, keys=%+v", err, cacheKeys)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *redisCache) SetCacheWithNotFound(ctx context.Context, key string) error {
|
||||||
|
return c.client.Set(ctx, key, NotFoundPlaceholder, DefaultNotFoundExpireTime).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildCacheKey 构建一个带有前缀的缓存key
|
||||||
|
func BuildCacheKey(keyPrefix string, key string) (cacheKey string, err error) {
|
||||||
|
if key == "" {
|
||||||
|
return "", errors.New("[cache] key should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey = key
|
||||||
|
if keyPrefix != "" {
|
||||||
|
cacheKey, err = strings.Join([]string{keyPrefix, key}, ":"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/encoding"
|
||||||
|
|
||||||
|
"github.com/alicebob/miniredis/v2"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitTestRedis 实例化一个可以用于单元测试的redis
|
||||||
|
func InitTestRedis() *redis.Client {
|
||||||
|
var mr, err = miniredis.Run()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// 打开下面命令可以测试链接关闭的情况
|
||||||
|
// defer mr.Close()
|
||||||
|
|
||||||
|
fmt.Println("mini redis addr:", mr.Addr())
|
||||||
|
return redis.NewClient(&redis.Options{
|
||||||
|
Addr: mr.Addr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_redisCache_SetGet(t *testing.T) {
|
||||||
|
// 实例化redis客户端
|
||||||
|
redisClient := InitTestRedis()
|
||||||
|
|
||||||
|
// 实例化redis cache
|
||||||
|
cache := NewRedisCache(redisClient, "unit-test", encoding.JSONEncoding{}, func() interface{} {
|
||||||
|
return new(int64)
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// test set
|
||||||
|
type setArgs struct {
|
||||||
|
key string
|
||||||
|
value interface{}
|
||||||
|
expiration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
value := "val-001"
|
||||||
|
setTests := []struct {
|
||||||
|
name string
|
||||||
|
cache Cache
|
||||||
|
args setArgs
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"test redis set",
|
||||||
|
cache,
|
||||||
|
setArgs{"key-001", &value, 60 * time.Second},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range setTests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := tt.cache
|
||||||
|
if err := c.Set(ctx, tt.args.key, tt.args.value, tt.args.expiration); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// test get
|
||||||
|
type args struct {
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cache Cache
|
||||||
|
args args
|
||||||
|
wantVal interface{}
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"test redis get",
|
||||||
|
cache,
|
||||||
|
args{"key-001"},
|
||||||
|
"val-001",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := tt.cache
|
||||||
|
var gotVal interface{}
|
||||||
|
err := c.Get(ctx, tt.args.key, &gotVal)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Log("gotval", gotVal)
|
||||||
|
if !reflect.DeepEqual(gotVal, tt.wantVal) {
|
||||||
|
t.Errorf("Get() gotVal = %v, want %v", gotVal, tt.wantVal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
## conf
|
||||||
|
|
||||||
|
解析yaml、json、toml配置文件到go struct,结合[goctl](https://github.com/zhufuyi/goctl)工具自动生成config.go到指定目录,例如:
|
||||||
|
|
||||||
|
> goctl covert yaml --file=test.yaml --tags=json --out=/yourProjectName/config。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
> go get -u github.com/zhufuyi/pkg/conf
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
具体示例看[config](internal/config)。
|
|
@ -0,0 +1,123 @@
|
||||||
|
// nolint
|
||||||
|
// code generated from config file
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "github.com/zhufuyi/sponge/pkg/conf"
|
||||||
|
|
||||||
|
type Config = GenerateName
|
||||||
|
|
||||||
|
var config *Config
|
||||||
|
|
||||||
|
// Init parsing configuration files to struct, including yaml, toml, json, etc.
|
||||||
|
func Init(configFile string, fs ...func()) error {
|
||||||
|
config = &Config{}
|
||||||
|
return conf.Parse(configFile, config, fs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Show() {
|
||||||
|
conf.Show(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get() *Config {
|
||||||
|
if config == nil {
|
||||||
|
panic("config is nil")
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenerateName struct {
|
||||||
|
App App `yaml:"app" json:"app"`
|
||||||
|
Etcd Etcd `yaml:"etcd" json:"etcd"`
|
||||||
|
Grpc Grpc `yaml:"grpc" json:"grpc"`
|
||||||
|
HTTP HTTP `yaml:"http" json:"http"`
|
||||||
|
Jaeger Jaeger `yaml:"jaeger" json:"jaeger"`
|
||||||
|
Logger Logger `yaml:"logger" json:"logger"`
|
||||||
|
Metrics Metrics `yaml:"metrics" json:"metrics"`
|
||||||
|
Mysql Mysql `yaml:"mysql" json:"mysql"`
|
||||||
|
RateLimiter RateLimiter `yaml:"rateLimiter" json:"rateLimiter"`
|
||||||
|
Redis Redis `yaml:"redis" json:"redis"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Redis struct {
|
||||||
|
Addr string `yaml:"addr" json:"addr"`
|
||||||
|
DB int `yaml:"dB" json:"dB"`
|
||||||
|
DialTimeout int `yaml:"dialTimeout" json:"dialTimeout"`
|
||||||
|
Dsn string `yaml:"dsn" json:"dsn"`
|
||||||
|
MinIdleConn int `yaml:"minIdleConn" json:"minIdleConn"`
|
||||||
|
Password string `yaml:"password" json:"password"`
|
||||||
|
PoolSize int `yaml:"poolSize" json:"poolSize"`
|
||||||
|
PoolTimeout int `yaml:"poolTimeout" json:"poolTimeout"`
|
||||||
|
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
|
||||||
|
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Etcd struct {
|
||||||
|
Addrs []string `yaml:"addrs" json:"addrs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Jaeger struct {
|
||||||
|
AgentHost string `yaml:"agentHost" json:"agentHost"`
|
||||||
|
AgentPort string `yaml:"agentPort" json:"agentPort"`
|
||||||
|
SamplingRate float64 `yaml:"samplingRate" json:"samplingRate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mysql struct {
|
||||||
|
ConnMaxLifetime int `yaml:"connMaxLifetime" json:"connMaxLifetime"`
|
||||||
|
Dsn string `yaml:"dsn" json:"dsn"`
|
||||||
|
EnableLog bool `yaml:"enableLog" json:"enableLog"`
|
||||||
|
MaxIdleConns int `yaml:"maxIdleConns" json:"maxIdleConns"`
|
||||||
|
MaxOpenConns int `yaml:"maxOpenConns" json:"maxOpenConns"`
|
||||||
|
SlowThreshold int `yaml:"slowThreshold" json:"slowThreshold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateLimiter struct {
|
||||||
|
Dimension string `yaml:"dimension" json:"dimension"`
|
||||||
|
MaxLimit int `yaml:"maxLimit" json:"maxLimit"`
|
||||||
|
QPSLimit int `yaml:"qpsLimit" json:"qpsLimit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
EnableRegistryDiscovery bool `yaml:"enableRegistryDiscovery" json:"enableRegistryDiscovery"`
|
||||||
|
EnableLimit bool `yaml:"enableLimit" json:"enableLimit"`
|
||||||
|
EnableMetrics bool `yaml:"enableMetrics" json:"enableMetrics"`
|
||||||
|
EnableProfile bool `yaml:"enableProfile" json:"enableProfile"`
|
||||||
|
EnableTracing bool `yaml:"enableTracing" json:"enableTracing"`
|
||||||
|
Env string `yaml:"env" json:"env"`
|
||||||
|
HostIP string `yaml:"hostIP" json:"hostIP"`
|
||||||
|
Name string `yaml:"name" json:"name"`
|
||||||
|
Version string `yaml:"version" json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogFileConfig struct {
|
||||||
|
Filename string `yaml:"filename" json:"filename"`
|
||||||
|
IsCompression bool `yaml:"isCompression" json:"isCompression"`
|
||||||
|
MaxAge int `yaml:"maxAge" json:"maxAge"`
|
||||||
|
MaxBackups int `yaml:"maxBackups" json:"maxBackups"`
|
||||||
|
MaxSize int `yaml:"maxSize" json:"maxSize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
Format string `yaml:"format" json:"format"`
|
||||||
|
IsSave bool `yaml:"isSave" json:"isSave"`
|
||||||
|
Level string `yaml:"level" json:"level"`
|
||||||
|
LogFileConfig LogFileConfig `yaml:"logFileConfig" json:"logFileConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTP struct {
|
||||||
|
Port int `yaml:"port" json:"port"`
|
||||||
|
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
|
||||||
|
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
|
||||||
|
ServiceName string `yaml:"serviceName" json:"serviceName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Grpc struct {
|
||||||
|
Port int `yaml:"port" json:"port"`
|
||||||
|
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
|
||||||
|
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
|
||||||
|
ServiceName string `yaml:"serviceName" json:"serviceName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metrics struct {
|
||||||
|
Port int `yaml:"port" json:"port"`
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
# 使用工具自动生成go struct
|
||||||
|
# goctl covert yaml --file=conf.yml --tags=json
|
||||||
|
|
||||||
|
# app 设置
|
||||||
|
app:
|
||||||
|
name: "userExample" # 服务名称
|
||||||
|
env: "dev" # 运行环境,dev:开发环境,prod:生产环境,pre:预生产环境
|
||||||
|
version: "v0.0.0" # 版本
|
||||||
|
hostIP: "127.0.0.1" # 主机ip地址
|
||||||
|
enableProfile: false # 是否开启性能分析功能,true:开启,false:关闭
|
||||||
|
enableMetrics: true # 是否开启指标采集,true:开启,false:关闭
|
||||||
|
enableLimit: false # 是否开启限流,true:开启,false:关闭
|
||||||
|
enableTracing: false # 是否开启链路跟踪,true:开启,false:关闭
|
||||||
|
enableRegistryDiscovery: true # 是否开启注册与发现,true:开启,false:关闭
|
||||||
|
|
||||||
|
|
||||||
|
# http 设置
|
||||||
|
http:
|
||||||
|
port: 8080 # 监听端口
|
||||||
|
readTimeout: 3 # 读超时,单位(秒)
|
||||||
|
writeTimeout: 90 # 写超时,单位(秒),如果enableProfile为true,需要大于60s,pprof做profiling的默认值是60s
|
||||||
|
|
||||||
|
|
||||||
|
# grpc 服务设置
|
||||||
|
grpc:
|
||||||
|
port: 9090 # 监听端口
|
||||||
|
readTimeout: 3 # 读超时,单位(秒)
|
||||||
|
writeTimeout: 3 # 写超时,单位(秒)
|
||||||
|
|
||||||
|
|
||||||
|
# logger 设置
|
||||||
|
logger:
|
||||||
|
level: "info" # 输出日志级别 debug, info, warn, error,默认是debug
|
||||||
|
format: "console" # 输出格式,console或json,默认是console
|
||||||
|
isSave: false # false:输出到终端,true:输出到文件,默认是false
|
||||||
|
logFileConfig: # isSave=true时有效
|
||||||
|
filename: "out.log" # 文件名称,默认值out.log
|
||||||
|
maxSize: 20 # 最大文件大小(MB),默认值10MB
|
||||||
|
maxBackups: 50 # 保留旧文件的最大个数,默认值100个
|
||||||
|
maxAge: 15 # 保留旧文件的最大天数,默认值30天
|
||||||
|
isCompression: true # 是否压缩/归档旧文件,默认值false
|
||||||
|
|
||||||
|
|
||||||
|
# mysql 设置
|
||||||
|
mysql:
|
||||||
|
# dsn格式:<user>:<pass>@(127.0.0.1:3306)/<db>?[k=v& ......]
|
||||||
|
dsn: "root:123456@(192.168.3.37:3306)/account?parseTime=true&loc=Local&charset=utf8,utf8mb4"
|
||||||
|
enableLog: true # 是否开启打印所有日志
|
||||||
|
slowThreshold: 0 # 如果大于0,只打印时间大于阈值的日志,优先级比enableLog高,单位(毫秒)
|
||||||
|
maxIdleConns: 3 #设置空闲连接池中连接的最大数量
|
||||||
|
maxOpenConns: 50 # 设置打开数据库连接的最大数量
|
||||||
|
connMaxLifetime: 30 # 设置了连接可复用的最大时间,单位(分钟)
|
||||||
|
|
||||||
|
|
||||||
|
# redis 设置
|
||||||
|
redis:
|
||||||
|
# dsn只适合redis6版本以上,默认用户为default,url格式 [user]:<pass>@]127.0.0.1:6379/[db]
|
||||||
|
dsn: "default:123456@192.168.3.37:6379"
|
||||||
|
# 适合各个版本redis
|
||||||
|
addr: 127.0.0.1:6379
|
||||||
|
password: "123456"
|
||||||
|
dB: 0
|
||||||
|
minIdleConn: 20
|
||||||
|
dialTimeout: 30 # 链接超时,单位(秒)
|
||||||
|
readTimeout: 500 # 读超时,单位(毫秒)
|
||||||
|
writeTimeout: 500 # 写超时,单位(毫秒)
|
||||||
|
poolSize: 100
|
||||||
|
poolTimeout: 200 # 连接池超时,单位(秒)
|
||||||
|
|
||||||
|
|
||||||
|
# jaeger配置
|
||||||
|
jaeger:
|
||||||
|
agentHost: "192.168.3.37"
|
||||||
|
agentPort: "6831"
|
||||||
|
samplingRate: 1.0 # 采样率,0~1之间,0表示禁止采样,大于等于1表示采样所有链路
|
||||||
|
|
||||||
|
|
||||||
|
# limit配置
|
||||||
|
rateLimiter:
|
||||||
|
dimension: "path" # 限流维度,支持path和ip两种,默认是path
|
||||||
|
qpsLimit: 1000 # 持续每秒允许成功请求数,默认是500
|
||||||
|
maxLimit: 2000 # 瞬时最大允许峰值,默认是1000,通常大于qpsLimit
|
||||||
|
|
||||||
|
|
||||||
|
# metrics配置
|
||||||
|
metrics:
|
||||||
|
port: 9082
|
||||||
|
|
||||||
|
|
||||||
|
# etcd配置
|
||||||
|
etcd:
|
||||||
|
addrs: ["192.168.3.37:2379"]
|
|
@ -0,0 +1,41 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseYAML(t *testing.T) {
|
||||||
|
err := Init("conf.yml") // 解析yaml文件
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Show(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试更新配置文件
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
fs := []func(){
|
||||||
|
func() {
|
||||||
|
fmt.Println("更新字段1")
|
||||||
|
},
|
||||||
|
func() {
|
||||||
|
fmt.Println("更新字段2")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Init("conf.yml", fs...)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
fmt.Println("port:", Get().App.Env)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse 解析配置文件到struct,包括yaml、toml、json等文件,如果fs不为空,开启监听配置文件变化
|
||||||
|
func Parse(configFile string, obj interface{}, fs ...func()) error {
|
||||||
|
confFileAbs, err := filepath.Abs(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filePathStr, filename := filepath.Split(confFileAbs)
|
||||||
|
if filePathStr == "" {
|
||||||
|
filePathStr = "."
|
||||||
|
}
|
||||||
|
ext := strings.TrimLeft(path.Ext(filename), ".")
|
||||||
|
filename = strings.ReplaceAll(filename, "."+ext, "") // 不包括后缀名
|
||||||
|
|
||||||
|
viper.AddConfigPath(filePathStr) // 路径
|
||||||
|
viper.SetConfigName(filename) // 名称
|
||||||
|
viper.SetConfigType(ext) // 从文件名中获取配置类型
|
||||||
|
err = viper.ReadInConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = viper.Unmarshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fs) > 0 {
|
||||||
|
watchConfig(obj, fs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听配置文件更新
|
||||||
|
func watchConfig(obj interface{}, fs ...func()) {
|
||||||
|
viper.WatchConfig()
|
||||||
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
|
err := viper.Unmarshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("viper.Unmarshal error: ", err)
|
||||||
|
} else {
|
||||||
|
// 更新初始化
|
||||||
|
for _, f := range fs {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show 打印配置信息(去掉敏感信息)
|
||||||
|
func Show(obj interface{}, keywords ...string) {
|
||||||
|
data, err := json.MarshalIndent(obj, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("json.MarshalIndent error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bufio.NewReader(bytes.NewReader(data))
|
||||||
|
for {
|
||||||
|
line, err := buf.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
keywords = append(keywords, `"dsn"`, `"password"`)
|
||||||
|
|
||||||
|
fmt.Printf(replacePWD(line, keywords...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换密码
|
||||||
|
func replacePWD(line string, keywords ...string) string {
|
||||||
|
for _, keyword := range keywords {
|
||||||
|
if strings.Contains(line, keyword) {
|
||||||
|
index := strings.Index(line, keyword)
|
||||||
|
if strings.Contains(line, "@") && strings.Contains(line, ":") {
|
||||||
|
return replaceDSN(line)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%s: \"******\",\n", line[:index+len(keyword)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换dsn的密码
|
||||||
|
func replaceDSN(str string) string {
|
||||||
|
mysqlPWD := []byte(str)
|
||||||
|
start, end := 0, 0
|
||||||
|
for k, v := range mysqlPWD {
|
||||||
|
if v == ':' {
|
||||||
|
start = k
|
||||||
|
}
|
||||||
|
if v == '@' {
|
||||||
|
end = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start >= end {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s******%s", mysqlPWD[:start+1], mysqlPWD[end:])
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
## discovery
|
||||||
|
|
||||||
|
discovery 服务服务发现,与服务注册[registry](../registry)对应,支持etcd、consul、nacos三种方式。
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
#### etcd
|
||||||
|
|
||||||
|
```go
|
||||||
|
func getETCDDiscovery(etcdEndpoints []string) registry.Discovery {
|
||||||
|
cli, err := clientv3.New(clientv3.Config{
|
||||||
|
Endpoints: etcdEndpoints,
|
||||||
|
DialTimeout: 10 * time.Second,
|
||||||
|
DialOptions: []grpc.DialOption{
|
||||||
|
grpc.WithBlock(),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return etcd.New(cli)
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoveryExample() {
|
||||||
|
iDiscovery := getETCDDiscovery([]string{"192.168.1.6:2379"})
|
||||||
|
|
||||||
|
// 服务发现的endpoint固定格式discovery:///serviceName
|
||||||
|
endpoint := "discovery:///" + "user"
|
||||||
|
// grpc客户端
|
||||||
|
conn, err := grpccli.DialInsecure(ctx, endpoint,
|
||||||
|
grpccli.WithUnaryInterceptors(interceptor.UnaryClientLog(logger.Get())),
|
||||||
|
grpccli.WithDiscovery(discovery),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ......
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
#### consul
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
#### nacos
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/registry"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const name = "discovery"
|
||||||
|
|
||||||
|
// Option is builder option.
|
||||||
|
type Option func(o *builder)
|
||||||
|
|
||||||
|
// WithTimeout with timeout option.
|
||||||
|
func WithTimeout(timeout time.Duration) Option {
|
||||||
|
return func(b *builder) {
|
||||||
|
b.timeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInsecure with isSecure option.
|
||||||
|
func WithInsecure(insecure bool) Option {
|
||||||
|
return func(b *builder) {
|
||||||
|
b.insecure = insecure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableDebugLog disables update instances log.
|
||||||
|
func DisableDebugLog() Option {
|
||||||
|
return func(b *builder) {
|
||||||
|
b.debugLogDisabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type builder struct {
|
||||||
|
discoverer registry.Discovery
|
||||||
|
timeout time.Duration
|
||||||
|
insecure bool
|
||||||
|
debugLogDisabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder creates a builder which is used to factory registry resolvers.
|
||||||
|
func NewBuilder(d registry.Discovery, opts ...Option) resolver.Builder {
|
||||||
|
b := &builder{
|
||||||
|
discoverer: d,
|
||||||
|
timeout: time.Second * 10,
|
||||||
|
insecure: false,
|
||||||
|
debugLogDisabled: false,
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(b)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
w registry.Watcher
|
||||||
|
)
|
||||||
|
done := make(chan struct{}, 1)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
w, err = b.discoverer.Watch(ctx, strings.TrimPrefix(target.URL.Path, "/"))
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(b.timeout):
|
||||||
|
err = errors.New("discovery create watcher overtime")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r := &discoveryResolver{
|
||||||
|
w: w,
|
||||||
|
cc: cc,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
insecure: b.insecure,
|
||||||
|
debugLogDisabled: b.debugLogDisabled,
|
||||||
|
}
|
||||||
|
go r.watch()
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scheme return scheme of discovery
|
||||||
|
func (*builder) Scheme() string {
|
||||||
|
return name
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/registry"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/attributes"
|
||||||
|
"google.golang.org/grpc/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type discoveryResolver struct {
|
||||||
|
w registry.Watcher
|
||||||
|
cc resolver.ClientConn
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
insecure bool
|
||||||
|
debugLogDisabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *discoveryResolver) watch() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ins, err := r.w.Next()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Errorf("[resolver] Failed to watch discovery endpoint: %v", err)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.update(ins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *discoveryResolver) update(ins []*registry.ServiceInstance) {
|
||||||
|
addrs := make([]resolver.Address, 0)
|
||||||
|
endpoints := make(map[string]struct{})
|
||||||
|
for _, in := range ins {
|
||||||
|
endpoint, err := parseEndpoint(in.Endpoints, "grpc", !r.insecure)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("[resolver] Failed to parse discovery endpoint: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if endpoint == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// filter redundant endpoints
|
||||||
|
if _, ok := endpoints[endpoint]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endpoints[endpoint] = struct{}{}
|
||||||
|
addr := resolver.Address{
|
||||||
|
ServerName: in.Name,
|
||||||
|
Attributes: parseAttributes(in.Metadata),
|
||||||
|
Addr: endpoint,
|
||||||
|
}
|
||||||
|
addr.Attributes = addr.Attributes.WithValue("rawServiceInstance", in)
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
logger.Warnf("[resolver] Zero endpoint found,refused to write, instances: %v", ins)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := r.cc.UpdateState(resolver.State{Addresses: addrs})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("[resolver] failed to update state: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.debugLogDisabled {
|
||||||
|
b, _ := json.Marshal(ins)
|
||||||
|
logger.Infof("[resolver] update instances: %s", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *discoveryResolver) Close() {
|
||||||
|
r.cancel()
|
||||||
|
err := r.w.Stop()
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("[resolver] failed to watch top: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *discoveryResolver) ResolveNow(options resolver.ResolveNowOptions) {}
|
||||||
|
|
||||||
|
func parseAttributes(md map[string]string) *attributes.Attributes {
|
||||||
|
var a *attributes.Attributes
|
||||||
|
for k, v := range md {
|
||||||
|
if a == nil {
|
||||||
|
a = attributes.New(k, v)
|
||||||
|
} else {
|
||||||
|
a = a.WithValue(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEndpoint parses an Endpoint URL.
|
||||||
|
func parseEndpoint(endpoints []string, scheme string, isSecure bool) (string, error) {
|
||||||
|
for _, e := range endpoints {
|
||||||
|
u, err := url.Parse(e)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if u.Scheme == scheme && IsSecure(u) == isSecure {
|
||||||
|
return u.Host, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSecure parses isSecure for Endpoint URL.
|
||||||
|
func IsSecure(u *url.URL) bool {
|
||||||
|
ok, err := strconv.ParseBool(u.Query().Get("isSecure"))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotAPointer .
|
||||||
|
ErrNotAPointer = errors.New("v argument must be a pointer")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Codec defines the interface gRPC uses to encode and decode messages. Note
|
||||||
|
// that implementations of this interface must be thread safe; a Codec's
|
||||||
|
// methods can be called from concurrent goroutines.
|
||||||
|
type Codec interface {
|
||||||
|
// Marshal returns the wire format of v.
|
||||||
|
Marshal(v interface{}) ([]byte, error)
|
||||||
|
// Unmarshal parses the wire format into v.
|
||||||
|
Unmarshal(data []byte, v interface{}) error
|
||||||
|
// Name returns the name of the Codec implementation. The returned string
|
||||||
|
// will be used as part of content type in transmission. The result must be
|
||||||
|
// static; the result cannot change between calls.
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
var registeredCodecs = make(map[string]Codec)
|
||||||
|
|
||||||
|
// RegisterCodec registers the provided Codec for use with all transport clients and
|
||||||
|
// servers.
|
||||||
|
//
|
||||||
|
// The Codec will be stored and looked up by result of its Name() method, which
|
||||||
|
// should match the content-subtype of the encoding handled by the Codec. This
|
||||||
|
// is case-insensitive, and is stored and looked up as lowercase. If the
|
||||||
|
// result of calling Name() is an empty string, RegisterCodec will panic. See
|
||||||
|
// Content-Type on
|
||||||
|
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for
|
||||||
|
// more details.
|
||||||
|
//
|
||||||
|
// NOTE: this function must only be called during initialization time (i.e. in
|
||||||
|
// an init() function), and is not thread-safe. If multiple Compressors are
|
||||||
|
// registered with the same name, the one registered last will take effect.
|
||||||
|
func RegisterCodec(codec Codec) {
|
||||||
|
if codec == nil {
|
||||||
|
panic("cannot register a nil Codec")
|
||||||
|
}
|
||||||
|
if codec.Name() == "" {
|
||||||
|
panic("cannot register Codec with empty string result for Name()")
|
||||||
|
}
|
||||||
|
contentSubtype := strings.ToLower(codec.Name())
|
||||||
|
registeredCodecs[contentSubtype] = codec
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCodec gets a registered Codec by content-subtype, or nil if no Codec is
|
||||||
|
// registered for the content-subtype.
|
||||||
|
//
|
||||||
|
// The content-subtype is expected to be lowercase.
|
||||||
|
func GetCodec(contentSubtype string) Codec {
|
||||||
|
return registeredCodecs[contentSubtype]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoding 编码接口定义
|
||||||
|
type Encoding interface {
|
||||||
|
Marshal(v interface{}) ([]byte, error)
|
||||||
|
Unmarshal(data []byte, v interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal encode data
|
||||||
|
func Marshal(e Encoding, v interface{}) (data []byte, err error) {
|
||||||
|
if !isPointer(v) {
|
||||||
|
return data, ErrNotAPointer
|
||||||
|
}
|
||||||
|
bm, ok := v.(encoding.BinaryMarshaler)
|
||||||
|
if ok && e == nil {
|
||||||
|
data, err = bm.MarshalBinary()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = e.Marshal(v)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
data, err = bm.MarshalBinary()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal decode data
|
||||||
|
func Unmarshal(e Encoding, data []byte, v interface{}) (err error) {
|
||||||
|
if !isPointer(v) {
|
||||||
|
return ErrNotAPointer
|
||||||
|
}
|
||||||
|
bm, ok := v.(encoding.BinaryUnmarshaler)
|
||||||
|
if ok && e == nil {
|
||||||
|
err = bm.UnmarshalBinary(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.Unmarshal(data, v)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return bm.UnmarshalBinary(data)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPointer(data interface{}) bool {
|
||||||
|
switch reflect.ValueOf(data).Kind() {
|
||||||
|
case reflect.Ptr, reflect.Interface:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package encoding
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func BenchmarkJsonMarshal(b *testing.B) {
|
||||||
|
a := make([]int, 0, 400)
|
||||||
|
for i := 0; i < 400; i++ {
|
||||||
|
a = append(a, i)
|
||||||
|
}
|
||||||
|
jsonEncoding := JSONEncoding{}
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, err := jsonEncoding.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkJsonUnmarshal(b *testing.B) {
|
||||||
|
a := make([]int, 0, 400)
|
||||||
|
for i := 0; i < 400; i++ {
|
||||||
|
a = append(a, i)
|
||||||
|
}
|
||||||
|
jsonEncoding := JSONEncoding{}
|
||||||
|
data, err := jsonEncoding.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
}
|
||||||
|
var result []int
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
err = jsonEncoding.Unmarshal(data, &result)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMsgpack(b *testing.B) {
|
||||||
|
// run the Fib function b.N times
|
||||||
|
a := make([]int, 400)
|
||||||
|
for i := 0; i < 400; i++ {
|
||||||
|
a = append(a, i)
|
||||||
|
}
|
||||||
|
msgPackEncoding := MsgPackEncoding{}
|
||||||
|
data, err := msgPackEncoding.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
}
|
||||||
|
var result []int
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
err = msgPackEncoding.Unmarshal(data, &result)
|
||||||
|
if err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package encoding
|
|
@ -0,0 +1,28 @@
|
||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GobEncoding gob encode
|
||||||
|
type GobEncoding struct{}
|
||||||
|
|
||||||
|
// Marshal gob encode
|
||||||
|
func (g GobEncoding) Marshal(v interface{}) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
buffer bytes.Buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
err := gob.NewEncoder(&buffer).Encode(v)
|
||||||
|
return buffer.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal gob encode
|
||||||
|
func (g GobEncoding) Unmarshal(data []byte, value interface{}) error {
|
||||||
|
err := gob.NewDecoder(bytes.NewReader(data)).Decode(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/encoding"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Name is the name registered for the json codec.
|
||||||
|
const Name = "json"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// MarshalOptions is a configurable JSON format marshaller.
|
||||||
|
MarshalOptions = protojson.MarshalOptions{
|
||||||
|
EmitUnpopulated: true,
|
||||||
|
}
|
||||||
|
// UnmarshalOptions is a configurable JSON format parser.
|
||||||
|
UnmarshalOptions = protojson.UnmarshalOptions{
|
||||||
|
DiscardUnknown: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
encoding.RegisterCodec(codec{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// codec is a Codec implementation with json.
|
||||||
|
type codec struct{}
|
||||||
|
|
||||||
|
func (codec) Marshal(v interface{}) ([]byte, error) {
|
||||||
|
switch m := v.(type) {
|
||||||
|
case json.Marshaler:
|
||||||
|
return m.MarshalJSON()
|
||||||
|
case proto.Message:
|
||||||
|
return MarshalOptions.Marshal(m)
|
||||||
|
default:
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (codec) Unmarshal(data []byte, v interface{}) error {
|
||||||
|
switch m := v.(type) {
|
||||||
|
case json.Unmarshaler:
|
||||||
|
return m.UnmarshalJSON(data)
|
||||||
|
case proto.Message:
|
||||||
|
return UnmarshalOptions.Unmarshal(data, m)
|
||||||
|
default:
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
for rv := rv; rv.Kind() == reflect.Ptr; {
|
||||||
|
if rv.IsNil() {
|
||||||
|
rv.Set(reflect.New(rv.Type().Elem()))
|
||||||
|
}
|
||||||
|
rv = rv.Elem()
|
||||||
|
}
|
||||||
|
if m, ok := reflect.Indirect(rv).Interface().(proto.Message); ok {
|
||||||
|
return UnmarshalOptions.Unmarshal(data, m)
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (codec) Name() string {
|
||||||
|
return Name
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/golang/snappy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONEncoding json格式
|
||||||
|
type JSONEncoding struct{}
|
||||||
|
|
||||||
|
// Marshal json encode
|
||||||
|
func (j JSONEncoding) Marshal(v interface{}) ([]byte, error) {
|
||||||
|
buf, err := json.Marshal(v)
|
||||||
|
return buf, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal json decode
|
||||||
|
func (j JSONEncoding) Unmarshal(data []byte, value interface{}) error {
|
||||||
|
err := json.Unmarshal(data, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONGzipEncoding json and gzip
|
||||||
|
type JSONGzipEncoding struct{}
|
||||||
|
|
||||||
|
// Marshal json encode and gzip
|
||||||
|
func (jz JSONGzipEncoding) Marshal(v interface{}) ([]byte, error) {
|
||||||
|
buf, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// var bufSizeBefore = len(buf)
|
||||||
|
|
||||||
|
buf, err = GzipEncode(buf)
|
||||||
|
// log.Infof("gzip_json_compress_ratio=%d/%d=%.2f", bufSizeBefore, len(buf), float64(bufSizeBefore)/float64(len(buf)))
|
||||||
|
return buf, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal json encode and gzip
|
||||||
|
func (jz JSONGzipEncoding) Unmarshal(data []byte, value interface{}) error {
|
||||||
|
jsonData, err := GzipDecode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(jsonData, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GzipEncode 编码
|
||||||
|
func GzipEncode(in []byte) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
buffer bytes.Buffer
|
||||||
|
out []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
writer, err := gzip.NewWriterLevel(&buffer, gzip.BestCompression)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = writer.Write(in)
|
||||||
|
if err != nil {
|
||||||
|
err = writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
err = writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GzipDecode 解码
|
||||||
|
func GzipDecode(in []byte) ([]byte, error) {
|
||||||
|
reader, err := gzip.NewReader(bytes.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
var out []byte
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = reader.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("reader close err: %+v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return io.ReadAll(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONSnappyEncoding json格式和snappy压缩
|
||||||
|
type JSONSnappyEncoding struct{}
|
||||||
|
|
||||||
|
// Marshal 序列化
|
||||||
|
func (s JSONSnappyEncoding) Marshal(v interface{}) (data []byte, err error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d := snappy.Encode(nil, b)
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal 反序列化
|
||||||
|
func (s JSONSnappyEncoding) Unmarshal(data []byte, value interface{}) error {
|
||||||
|
b, err := snappy.Decode(nil, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(b, value)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package encoding
|
||||||
|
|
||||||
|
import "github.com/vmihailenco/msgpack"
|
||||||
|
|
||||||
|
// MsgPackEncoding msgpack 格式
|
||||||
|
type MsgPackEncoding struct{}
|
||||||
|
|
||||||
|
// Marshal msgpack encode
|
||||||
|
func (mp MsgPackEncoding) Marshal(v interface{}) ([]byte, error) {
|
||||||
|
buf, err := msgpack.Marshal(v)
|
||||||
|
return buf, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal msgpack decode
|
||||||
|
func (mp MsgPackEncoding) Unmarshal(data []byte, value interface{}) error {
|
||||||
|
err := msgpack.Unmarshal(data, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/encoding"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Name is the name registered for the proto compressor.
|
||||||
|
const Name = "proto"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
encoding.RegisterCodec(codec{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// codec is a Codec implementation with protobuf. It is the default codec for gRPC.
|
||||||
|
type codec struct{}
|
||||||
|
|
||||||
|
func (codec) Marshal(v interface{}) ([]byte, error) {
|
||||||
|
vv, ok := v.(proto.Message)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to marshal, message is %T, want proto.Message", v)
|
||||||
|
}
|
||||||
|
return proto.Marshal(vv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (codec) Unmarshal(data []byte, v interface{}) error {
|
||||||
|
vv, ok := v.(proto.Message)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("failed to unmarshal, message is %T, want proto.Message", v)
|
||||||
|
}
|
||||||
|
return proto.Unmarshal(data, vv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (codec) Name() string {
|
||||||
|
return Name
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
## errcode
|
||||||
|
|
||||||
|
错误码通常包括系统级错误码和服务级错误码,一共6位十进制数字组成,例如200101
|
||||||
|
|
||||||
|
| 10 | 01 | 01 |
|
||||||
|
| :------ | :------ | :------ |
|
||||||
|
| 对于http错误码,20表示服务级错误(10为系统级错误) | 服务模块代码 | 具体错误代码 |
|
||||||
|
| 对于grpc错误码,40表示服务级错误(30为系统级错误) | 服务模块代码 | 具体错误代码 |
|
||||||
|
|
||||||
|
- 错误级别占2位数:10(http)和30(grpc)表示系统级错误,20(http)和40(grpc)表示服务级错误,通常是由用户非法操作引起的。
|
||||||
|
- 服务模块占两位数:一个大型系统的服务模块通常不超过两位数,如果超过,说明这个系统该拆分了。
|
||||||
|
- 错误码占两位数:防止一个模块定制过多的错误码,后期不好维护。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
> go get -u github.com/zhufuyi/pkg/errcode
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
### http错误码使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 定义错误码
|
||||||
|
var ErrLogin = errcode.NewError(200101, "用户名或密码错误")
|
||||||
|
|
||||||
|
// 请求返回
|
||||||
|
response.Error(c, errcode.LoginErr)
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### grpc错误码使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 定义错误码
|
||||||
|
var ErrLogin = NewRPCStatus(400101, "用户名或密码错误")
|
||||||
|
|
||||||
|
// 返回错误
|
||||||
|
errcode.ErrLogin.Err()
|
||||||
|
// 返回附带错误详情信息
|
||||||
|
errcode.ErrLogin.Err(errcode.Any("err", err))
|
||||||
|
```
|
|
@ -0,0 +1,97 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GRPCStatus grpc 状态
|
||||||
|
type GRPCStatus struct {
|
||||||
|
status *status.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusCodes = map[codes.Code]string{}
|
||||||
|
|
||||||
|
// NewGRPCStatus 新建一个status
|
||||||
|
func NewGRPCStatus(code codes.Code, msg string) *GRPCStatus {
|
||||||
|
if v, ok := statusCodes[code]; ok {
|
||||||
|
panic(fmt.Sprintf("grpc status code = %d already exists, please replace with a new error code, old msg = %s", code, v))
|
||||||
|
} else {
|
||||||
|
statusCodes[code] = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GRPCStatus{
|
||||||
|
status: status.New(code, msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail error details
|
||||||
|
type Detail struct {
|
||||||
|
key string
|
||||||
|
val interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String detail key-value
|
||||||
|
func (d Detail) String() string {
|
||||||
|
return fmt.Sprintf("%s: {%v}", d.key, d.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any type
|
||||||
|
func Any(key string, val interface{}) Detail {
|
||||||
|
return Detail{
|
||||||
|
key: key,
|
||||||
|
val: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCErr rpc error
|
||||||
|
func RPCErr(g *GRPCStatus, details ...Detail) error {
|
||||||
|
var dts []string
|
||||||
|
for _, detail := range details {
|
||||||
|
dts = append(dts, detail.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.Errorf(g.status.Code(), "%s details = %v", g.status.Message(), dts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err return error
|
||||||
|
func (g *GRPCStatus) Err(details ...Detail) error {
|
||||||
|
var dts []string
|
||||||
|
for _, detail := range details {
|
||||||
|
dts = append(dts, detail.String())
|
||||||
|
}
|
||||||
|
if len(dts) == 0 {
|
||||||
|
return status.Errorf(g.status.Code(), "%s", g.status.Message())
|
||||||
|
}
|
||||||
|
return status.Errorf(g.status.Code(), "%s details = %v", g.status.Message(), dts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRPCCode 转换为RPC识别的错误码,避免返回Unknown状态码
|
||||||
|
func ToRPCCode(code int) codes.Code {
|
||||||
|
var statusCode codes.Code
|
||||||
|
|
||||||
|
switch code {
|
||||||
|
case InternalServerError.code:
|
||||||
|
statusCode = codes.Internal
|
||||||
|
case InvalidParams.code:
|
||||||
|
statusCode = codes.InvalidArgument
|
||||||
|
case Unauthorized.code:
|
||||||
|
statusCode = codes.Unauthenticated
|
||||||
|
case NotFound.code:
|
||||||
|
statusCode = codes.NotFound
|
||||||
|
case DeadlineExceeded.code:
|
||||||
|
statusCode = codes.DeadlineExceeded
|
||||||
|
case AccessDenied.code:
|
||||||
|
statusCode = codes.PermissionDenied
|
||||||
|
case LimitExceed.code:
|
||||||
|
statusCode = codes.ResourceExhausted
|
||||||
|
case MethodNotAllowed.code:
|
||||||
|
statusCode = codes.Unimplemented
|
||||||
|
default:
|
||||||
|
statusCode = codes.Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusCode
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
import "google.golang.org/grpc/codes"
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// rpc服务级别错误码,有Status前缀
|
||||||
|
var (
|
||||||
|
// StatusUserCreate = NewGRPCStatus(400101, "创建用户失败")
|
||||||
|
// StatusUserDelete = NewGRPCStatus(400102, "删除用户失败")
|
||||||
|
// StatusUserUpdate = NewGRPCStatus(400103, "更新用户失败")
|
||||||
|
// StatusUserGet = NewGRPCStatus(400104, "获取用户失败")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GCode 根据编号生成400000~500000之间的错误码
|
||||||
|
func GCode(NO int) codes.Code {
|
||||||
|
if NO > 1000 {
|
||||||
|
panic("NO must be < 1000")
|
||||||
|
}
|
||||||
|
return codes.Code(400000 + NO*100)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// rpc系统级别错误码,有status前缀
|
||||||
|
var (
|
||||||
|
StatusSuccess = NewGRPCStatus(0, "ok")
|
||||||
|
|
||||||
|
StatusInvalidParams = NewGRPCStatus(300001, "参数错误")
|
||||||
|
StatusUnauthorized = NewGRPCStatus(300002, "认证错误")
|
||||||
|
StatusInternalServerError = NewGRPCStatus(300003, "服务内部错误")
|
||||||
|
StatusNotFound = NewGRPCStatus(300004, "资源不存在")
|
||||||
|
StatusAlreadyExists = NewGRPCStatus(300005, "资源已存在")
|
||||||
|
StatusTimeout = NewGRPCStatus(300006, "超时")
|
||||||
|
StatusTooManyRequests = NewGRPCStatus(300007, "请求过多")
|
||||||
|
StatusForbidden = NewGRPCStatus(300008, "拒绝访问")
|
||||||
|
StatusLimitExceed = NewGRPCStatus(300009, "访问限制")
|
||||||
|
|
||||||
|
StatusDeadlineExceeded = NewGRPCStatus(300010, "已超过最后期限")
|
||||||
|
StatusAccessDenied = NewGRPCStatus(300011, "拒绝访问")
|
||||||
|
StatusMethodNotAllowed = NewGRPCStatus(300012, "不允许使用的方法")
|
||||||
|
)
|
|
@ -0,0 +1,85 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error 错误
|
||||||
|
type Error struct {
|
||||||
|
// 错误码
|
||||||
|
code int
|
||||||
|
// 错误消息
|
||||||
|
msg string
|
||||||
|
// 详细信息
|
||||||
|
details []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var errCodes = map[int]string{}
|
||||||
|
|
||||||
|
// NewError 创建新错误信息
|
||||||
|
func NewError(code int, msg string) *Error {
|
||||||
|
if v, ok := errCodes[code]; ok {
|
||||||
|
panic(fmt.Sprintf("http error code = %d already exists, please replace with a new error code, old msg = %s", code, v))
|
||||||
|
}
|
||||||
|
errCodes[code] = msg
|
||||||
|
return &Error{code: code, msg: msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String 打印错误
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return fmt.Sprintf("错误码:%d, 错误信息::%s", e.Code(), e.Msg())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code 错误码
|
||||||
|
func (e *Error) Code() int {
|
||||||
|
return e.code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msg 错误信息
|
||||||
|
func (e *Error) Msg() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgf 附加信息
|
||||||
|
func (e *Error) Msgf(args []interface{}) string {
|
||||||
|
return fmt.Sprintf(e.msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details 错误详情
|
||||||
|
func (e *Error) Details() []string {
|
||||||
|
return e.details
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDetails 携带附加错误详情
|
||||||
|
func (e *Error) WithDetails(details ...string) *Error {
|
||||||
|
newError := *e
|
||||||
|
newError.details = []string{}
|
||||||
|
newError.details = append(newError.details, details...)
|
||||||
|
|
||||||
|
return &newError
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCode 对应http错误码
|
||||||
|
func (e *Error) StatusCode() int {
|
||||||
|
switch e.Code() {
|
||||||
|
case Success.Code():
|
||||||
|
return http.StatusOK
|
||||||
|
case InternalServerError.Code():
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
case InvalidParams.Code():
|
||||||
|
return http.StatusBadRequest
|
||||||
|
case Unauthorized.Code():
|
||||||
|
return http.StatusUnauthorized
|
||||||
|
case TooManyRequests.Code(), LimitExceed.Code():
|
||||||
|
return http.StatusTooManyRequests
|
||||||
|
case Forbidden.Code():
|
||||||
|
return http.StatusForbidden
|
||||||
|
case NotFound.Code():
|
||||||
|
return http.StatusNotFound
|
||||||
|
case Timeout.Code():
|
||||||
|
return http.StatusRequestTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// 服务级别错误码,有Err前缀
|
||||||
|
var (
|
||||||
|
// ErrUserCreate = NewError(200101, "创建用户失败")
|
||||||
|
// ErrUserDelete = NewError(200102, "删除用户失败")
|
||||||
|
// ErrUserUpdate = NewError(200103, "更新用户失败")
|
||||||
|
// ErrUserGet = NewError(200104, "获取用户失败")
|
||||||
|
)
|
||||||
|
|
||||||
|
// HCode 根据编号生成200000~300000之间的错误码
|
||||||
|
func HCode(NO int) int {
|
||||||
|
if NO > 1000 {
|
||||||
|
panic("NO must be < 1000")
|
||||||
|
}
|
||||||
|
return 200000 + NO*100
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
// http系统级别错误码,无Err前缀
|
||||||
|
var (
|
||||||
|
Success = NewError(0, "ok")
|
||||||
|
InvalidParams = NewError(100001, "参数错误")
|
||||||
|
Unauthorized = NewError(100002, "认证错误")
|
||||||
|
InternalServerError = NewError(100003, "服务内部错误")
|
||||||
|
NotFound = NewError(100004, "资源不存在")
|
||||||
|
AlreadyExists = NewError(100005, "资源已存在")
|
||||||
|
Timeout = NewError(100006, "超时")
|
||||||
|
TooManyRequests = NewError(100007, "请求过多")
|
||||||
|
Forbidden = NewError(100008, "拒绝访问")
|
||||||
|
LimitExceed = NewError(100009, "访问限制")
|
||||||
|
|
||||||
|
DeadlineExceeded = NewError(100010, "已超过最后期限")
|
||||||
|
AccessDenied = NewError(100011, "拒绝访问")
|
||||||
|
MethodNotAllowed = NewError(100012, "不允许使用的方法")
|
||||||
|
)
|
|
@ -0,0 +1,23 @@
|
||||||
|
## errcode
|
||||||
|
|
||||||
|
错误码通常包括系统级错误码和服务级错误码,一共5位十进制数字组成,例如20101
|
||||||
|
|
||||||
|
| 2 | 01 | 01 |
|
||||||
|
| :------ | :------ | :------ |
|
||||||
|
| 2表示服务级错误(1为系统级错误) | 服务模块代码 | 具体错误代码 |
|
||||||
|
|
||||||
|
- 错误级别占一位数:1表示系统级错误,2表示服务级错误,通常是由用户非法操作引起的。
|
||||||
|
- 服务模块占两位数:一个大型系统的服务模块通常不超过两位数,如果超过,说明这个系统该拆分了。
|
||||||
|
- 错误码占两位数:防止一个模块定制过多的错误码,后期不好维护。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 定义错误码
|
||||||
|
ErrLogin = errcode.NewError(20101, "用户名或密码错误")
|
||||||
|
|
||||||
|
// 请求返回
|
||||||
|
response.Error(c, errcode.LoginErr)
|
||||||
|
```
|
|
@ -0,0 +1,85 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error 错误
|
||||||
|
type Error struct {
|
||||||
|
// 错误码
|
||||||
|
code int
|
||||||
|
// 错误消息
|
||||||
|
msg string
|
||||||
|
// 详细信息
|
||||||
|
details []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var codes = map[int]string{}
|
||||||
|
|
||||||
|
// NewError 创建新错误信息
|
||||||
|
func NewError(code int, msg string) *Error {
|
||||||
|
if _, ok := codes[code]; ok {
|
||||||
|
panic(fmt.Sprintf("错误码 %d 已经存在,请更换一个", code))
|
||||||
|
}
|
||||||
|
codes[code] = msg
|
||||||
|
return &Error{code: code, msg: msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String 打印错误
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return fmt.Sprintf("错误码:%d, 错误信息::%s", e.Code(), e.Msg())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code 错误码
|
||||||
|
func (e *Error) Code() int {
|
||||||
|
return e.code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msg 错误信息
|
||||||
|
func (e *Error) Msg() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgf 附加信息
|
||||||
|
func (e *Error) Msgf(args []interface{}) string {
|
||||||
|
return fmt.Sprintf(e.msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details 错误详情
|
||||||
|
func (e *Error) Details() []string {
|
||||||
|
return e.details
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDetails 携带附加错误详情
|
||||||
|
func (e *Error) WithDetails(details ...string) *Error {
|
||||||
|
newError := *e
|
||||||
|
newError.details = []string{}
|
||||||
|
newError.details = append(newError.details, details...)
|
||||||
|
|
||||||
|
return &newError
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCode 对应http错误码
|
||||||
|
func (e *Error) StatusCode() int {
|
||||||
|
switch e.Code() {
|
||||||
|
case Success.Code():
|
||||||
|
return http.StatusOK
|
||||||
|
case InternalServerError.Code():
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
case InvalidParams.Code():
|
||||||
|
return http.StatusBadRequest
|
||||||
|
case Unauthorized.Code():
|
||||||
|
return http.StatusUnauthorized
|
||||||
|
case TooManyRequests.Code(), LimitExceed.Code():
|
||||||
|
return http.StatusTooManyRequests
|
||||||
|
case Forbidden.Code():
|
||||||
|
return http.StatusForbidden
|
||||||
|
case NotFound.Code():
|
||||||
|
return http.StatusNotFound
|
||||||
|
case Timeout.Code():
|
||||||
|
return http.StatusRequestTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
// 服务级别错误码,有Err前缀
|
||||||
|
var (
|
||||||
|
// ErrUserCreate = NewError(20101, "创建用户失败")
|
||||||
|
// ErrUserDelete = NewError(20102, "删除用户失败")
|
||||||
|
// ErrUserUpdate = NewError(20103, "更新用户失败")
|
||||||
|
// ErrUserGet = NewError(20104, "获取用户失败")
|
||||||
|
)
|
|
@ -0,0 +1,15 @@
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
// 系统级别错误码,无Err前缀
|
||||||
|
var (
|
||||||
|
Success = NewError(0, "ok")
|
||||||
|
InvalidParams = NewError(10001, "参数错误")
|
||||||
|
Unauthorized = NewError(10002, "认证错误")
|
||||||
|
InternalServerError = NewError(10003, "服务内部错误")
|
||||||
|
NotFound = NewError(10004, "资源不存在")
|
||||||
|
AlreadyExists = NewError(10005, "资源已存在")
|
||||||
|
Timeout = NewError(10006, "超时")
|
||||||
|
TooManyRequests = NewError(10007, "请求过多")
|
||||||
|
Forbidden = NewError(10008, "拒绝访问")
|
||||||
|
LimitExceed = NewError(10009, "访问限制")
|
||||||
|
)
|
|
@ -0,0 +1,38 @@
|
||||||
|
package handlerfunc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkHealthResponse check health result
|
||||||
|
type checkHealthResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckHealth check healthy.
|
||||||
|
// @Summary check health
|
||||||
|
// @Description check health
|
||||||
|
// @Tags system
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} checkHealthResponse{}
|
||||||
|
// @Router /health [get]
|
||||||
|
func CheckHealth(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, checkHealthResponse{Status: "UP", Hostname: utils.GetHostname()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping ping
|
||||||
|
// @Summary ping
|
||||||
|
// @Description ping
|
||||||
|
// @Tags system
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Router /ping [get]
|
||||||
|
func Ping(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
## render
|
||||||
|
|
||||||
|
gin中间件插件。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
> go get -u github.com/zhufuyi/pkg/gin/middleware
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 日志中间件
|
||||||
|
|
||||||
|
可以设置打印最大长度、添加请求id字段、忽略打印path、自定义[zap](go.uber.org/zap) log
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// 默认打印日志
|
||||||
|
r.Use(middleware.Logging())
|
||||||
|
|
||||||
|
// 自定义打印日志
|
||||||
|
r.Use(middleware.Logging(
|
||||||
|
middleware.WithMaxLen(400), // 打印body最大长度,超过则忽略
|
||||||
|
//WithRequestIDFromHeader(), // 支持自定义requestID名称
|
||||||
|
WithRequestIDFromContext(), // 支持自定义requestID名称
|
||||||
|
//middleware.WithIgnoreRoutes("/hello"), // 忽略/hello
|
||||||
|
))
|
||||||
|
|
||||||
|
// 自定义zap log
|
||||||
|
log, _ := logger.Init(logger.WithFormat("json"))
|
||||||
|
r.Use(middlewareLogging(
|
||||||
|
middleware.WithLog(log),
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 允许跨域请求
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := gin.Default()
|
||||||
|
r.Use(middleware.Cors())
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### qps限流
|
||||||
|
|
||||||
|
#### path维度的qps限流
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// path, 默认qps=500, burst=1000
|
||||||
|
r.Use(ratelimiter.QPS())
|
||||||
|
|
||||||
|
// path, 自定义qps=50, burst=100
|
||||||
|
r.Use(ratelimiter.QPS(
|
||||||
|
ratelimiter.WithQPS(50),
|
||||||
|
ratelimiter.WithBurst(100),
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ip维度的qps限流
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ip, 自定义qps=40, burst=80
|
||||||
|
r.Use(ratelimiter.QPS(
|
||||||
|
ratelimiter.WithIP(),
|
||||||
|
ratelimiter.WithQPS(40),
|
||||||
|
ratelimiter.WithBurst(80),
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### jwt鉴权
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := gin.Default()
|
||||||
|
r.GET("/user/:id", middleware.JWT(), userFun) // 需要鉴权
|
||||||
|
```
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 链路跟踪
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 初始化trace
|
||||||
|
func InitTrace(serviceName string) {
|
||||||
|
exporter, err := tracer.NewJaegerAgentExporter("192.168.3.37", "6831")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource := tracer.NewResource(
|
||||||
|
tracer.WithServiceName(serviceName),
|
||||||
|
tracer.WithEnvironment("dev"),
|
||||||
|
tracer.WithServiceVersion("demo"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tracer.Init(exporter, resource) // 默认采集全部
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRouter(
|
||||||
|
r := gin.Default()
|
||||||
|
r.Use(middleware.Tracing("your-service-name"))
|
||||||
|
|
||||||
|
// ......
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果有需要,可以在程序创建一个span
|
||||||
|
func SpanDemo(serviceName string, spanName string, ctx context.Context) {
|
||||||
|
_, span := otel.Tracer(serviceName).Start(
|
||||||
|
ctx, spanName,
|
||||||
|
trace.WithAttributes(attribute.String(spanName, time.Now().String())), // 自定义属性
|
||||||
|
)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// ......
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### 监控指标
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
r.Use(metrics.Metrics(r,
|
||||||
|
//metrics.WithMetricsPath("/demo/metrics"), // default is /metrics
|
||||||
|
metrics.WithIgnoreStatusCodes(http.StatusNotFound), // ignore status codes
|
||||||
|
//metrics.WithIgnoreRequestMethods(http.MethodHead), // ignore request methods
|
||||||
|
//metrics.WithIgnoreRequestPaths("/ping", "/health"), // ignore request paths
|
||||||
|
))
|
||||||
|
```
|
|
@ -0,0 +1,66 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zhufuyi/sponge/pkg/errcode"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/jwt"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auth 鉴权
|
||||||
|
func Auth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authorization := c.GetHeader("Authorization")
|
||||||
|
if len(authorization) < 20 {
|
||||||
|
logger.Error("authorization is illegal", logger.String("authorization", authorization))
|
||||||
|
response.Error(c, errcode.Unauthorized)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := authorization[7:] // 去掉Bearer 前缀
|
||||||
|
claims, err := jwt.VerifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("VerifyToken error", logger.Err(err))
|
||||||
|
response.Error(c, errcode.Unauthorized)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("uid", claims.UID)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthAdmin 管理员鉴权
|
||||||
|
func AuthAdmin() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authorization := c.GetHeader("Authorization")
|
||||||
|
if len(authorization) < 20 {
|
||||||
|
logger.Error("authorization is illegal", logger.String("authorization", authorization))
|
||||||
|
response.Error(c, errcode.Unauthorized)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := authorization[7:] // 去掉Bearer 前缀
|
||||||
|
claims, err := jwt.VerifyToken(token)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("VerifyToken error", logger.Err(err))
|
||||||
|
response.Error(c, errcode.Unauthorized)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为管理员
|
||||||
|
if claims.Role != "admin" {
|
||||||
|
logger.Error("prohibition of access", logger.String("uid", claims.UID), logger.String("role", claims.Role))
|
||||||
|
response.Error(c, errcode.Forbidden)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("uid", claims.UID)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gohttp"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var uid = "123"
|
||||||
|
|
||||||
|
func initServer2() {
|
||||||
|
jwt.Init()
|
||||||
|
|
||||||
|
addr := getAddr()
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
tokenFun := func(c *gin.Context) {
|
||||||
|
token, _ := jwt.GenerateToken(uid)
|
||||||
|
fmt.Println("token =", token)
|
||||||
|
response.Success(c, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
userFun := func(c *gin.Context) {
|
||||||
|
response.Success(c, "hello "+uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.GET("/token", tokenFun)
|
||||||
|
r.GET("/user/:id", Auth(), userFun) // 需要鉴权
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := r.Run(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuth(t *testing.T) {
|
||||||
|
initServer2()
|
||||||
|
|
||||||
|
// 获取token
|
||||||
|
result := &gohttp.StdResult{}
|
||||||
|
err := gohttp.Get(result, requestAddr+"/token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
token := result.Data.(string)
|
||||||
|
|
||||||
|
// 使用访问
|
||||||
|
authorization := fmt.Sprintf("Bearer %s", token)
|
||||||
|
val, err := getUser(authorization)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUser(authorization string) (string, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
url := requestAddr + "/user/" + uid
|
||||||
|
reqest, err := http.NewRequest("GET", url, nil)
|
||||||
|
reqest.Header.Add("Authorization", authorization)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
response, _ := client.Do(reqest)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cors 跨域
|
||||||
|
func Cors() gin.HandlerFunc {
|
||||||
|
return cors.New(
|
||||||
|
cors.Config{
|
||||||
|
AllowOrigins: []string{"*"},
|
||||||
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
|
||||||
|
AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "Accept"},
|
||||||
|
ExposeHeaders: []string{"Content-Length", "text/plain", "Authorization", "Content-Type"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 12 * time.Hour,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Print body max length
|
||||||
|
defaultMaxLength = 300
|
||||||
|
|
||||||
|
// default zap log
|
||||||
|
defaultLogger, _ = zap.NewDevelopment()
|
||||||
|
|
||||||
|
// Default request id name
|
||||||
|
defaultRequestIDNameInHeader = "X-Request-Id"
|
||||||
|
defaultRequestIDNameInContext = "request_id"
|
||||||
|
|
||||||
|
// Ignore route list
|
||||||
|
defaultIgnoreRoutes = map[string]struct{}{
|
||||||
|
"/ping": {},
|
||||||
|
"/pong": {},
|
||||||
|
"/health": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option set the gin logger options.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
func defaultOptions() *options {
|
||||||
|
return &options{
|
||||||
|
maxLength: defaultMaxLength,
|
||||||
|
log: defaultLogger,
|
||||||
|
ignoreRoutes: defaultIgnoreRoutes,
|
||||||
|
requestIDName: "",
|
||||||
|
requestIDFrom: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
maxLength int
|
||||||
|
log *zap.Logger
|
||||||
|
ignoreRoutes map[string]struct{}
|
||||||
|
requestIDName string
|
||||||
|
requestIDFrom int // 0: ignore, 1: from header, 2: from context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *options) apply(opts ...Option) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxLen logger content max length
|
||||||
|
func WithMaxLen(maxLen int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.maxLength = maxLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLog set log
|
||||||
|
func WithLog(log *zap.Logger) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
if log != nil {
|
||||||
|
o.log = log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIgnoreRoutes no logger content routes
|
||||||
|
func WithIgnoreRoutes(routes ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
for _, route := range routes {
|
||||||
|
o.ignoreRoutes[route] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestIDFromHeader name is field in header, default value is X-Request-Id
|
||||||
|
func WithRequestIDFromHeader(name ...string) Option {
|
||||||
|
var requestIDName string
|
||||||
|
if len(name) > 0 && name[0] != "" {
|
||||||
|
requestIDName = name[0]
|
||||||
|
} else {
|
||||||
|
requestIDName = defaultRequestIDNameInHeader
|
||||||
|
}
|
||||||
|
return func(o *options) {
|
||||||
|
o.requestIDFrom = 1
|
||||||
|
o.requestIDName = requestIDName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestIDFromContext name is field in context, default value is request_id
|
||||||
|
func WithRequestIDFromContext(name ...string) Option {
|
||||||
|
var requestIDName string
|
||||||
|
if len(name) > 0 && name[0] != "" {
|
||||||
|
requestIDName = name[0]
|
||||||
|
} else {
|
||||||
|
requestIDName = defaultRequestIDNameInContext
|
||||||
|
}
|
||||||
|
return func(o *options) {
|
||||||
|
o.requestIDFrom = 2
|
||||||
|
o.requestIDName = requestIDName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type bodyLogWriter struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
body *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w bodyLogWriter) Write(b []byte) (int, error) {
|
||||||
|
w.body.Write(b)
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBodyData(buf *bytes.Buffer, maxLen int) string {
|
||||||
|
var body string
|
||||||
|
|
||||||
|
if buf.Len() > maxLen {
|
||||||
|
body = string(buf.Bytes()[:maxLen]) + " ...... "
|
||||||
|
// 如果有敏感数据需要过滤掉,比如明文密码
|
||||||
|
} else {
|
||||||
|
body = buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging print request and response info
|
||||||
|
func Logging(opts ...Option) gin.HandlerFunc {
|
||||||
|
o := defaultOptions()
|
||||||
|
o.apply(opts...)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// 忽略打印指定的路由
|
||||||
|
if _, ok := o.ignoreRoutes[c.Request.URL.Path]; ok {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理前打印输入信息
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
_, _ = buf.ReadFrom(c.Request.Body)
|
||||||
|
|
||||||
|
fields := []zap.Field{
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
zap.String("url", c.Request.URL.String()),
|
||||||
|
}
|
||||||
|
if c.Request.Method == http.MethodPost || c.Request.Method == http.MethodPut || c.Request.Method == http.MethodPatch || c.Request.Method == http.MethodDelete {
|
||||||
|
fields = append(fields,
|
||||||
|
zap.Int("size", buf.Len()),
|
||||||
|
zap.String("body", getBodyData(&buf, o.maxLength)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
reqID := ""
|
||||||
|
if o.requestIDFrom == 1 {
|
||||||
|
reqID = c.Request.Header.Get(o.requestIDName)
|
||||||
|
fields = append(fields, zap.String(o.requestIDName, reqID))
|
||||||
|
} else if o.requestIDFrom == 2 {
|
||||||
|
if v, isExist := c.Get(o.requestIDName); isExist {
|
||||||
|
if requestID, ok := v.(string); ok {
|
||||||
|
reqID = requestID
|
||||||
|
fields = append(fields, zap.String(o.requestIDName, reqID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.log.Info("<<<<", fields...)
|
||||||
|
|
||||||
|
c.Request.Body = io.NopCloser(&buf)
|
||||||
|
|
||||||
|
// 替换writer
|
||||||
|
newWriter := &bodyLogWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
|
||||||
|
c.Writer = newWriter
|
||||||
|
|
||||||
|
// 处理请求
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 处理后打印返回信息
|
||||||
|
fields = []zap.Field{
|
||||||
|
zap.Int("code", c.Writer.Status()),
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
zap.String("url", c.Request.URL.Path),
|
||||||
|
zap.Int64("time_us", time.Since(start).Nanoseconds()/1000),
|
||||||
|
zap.Int("size", newWriter.body.Len()),
|
||||||
|
zap.String("response", strings.TrimRight(getBodyData(newWriter.body, o.maxLength), "\n")),
|
||||||
|
}
|
||||||
|
if o.requestIDName != "" {
|
||||||
|
fields = append(fields, zap.String(o.requestIDName, reqID))
|
||||||
|
}
|
||||||
|
o.log.Info(">>>>", fields...)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/response"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gohttp"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/logger"
|
||||||
|
"github.com/zhufuyi/sponge/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var requestAddr string
|
||||||
|
|
||||||
|
func initServer1() {
|
||||||
|
addr := getAddr()
|
||||||
|
r := gin.Default()
|
||||||
|
r.Use(RequestID())
|
||||||
|
|
||||||
|
// 默认打印日志
|
||||||
|
// r.Use(Logging())
|
||||||
|
|
||||||
|
// 自定义打印日志
|
||||||
|
r.Use(Logging(
|
||||||
|
WithMaxLen(400),
|
||||||
|
//WithRequestIDFromHeader(),
|
||||||
|
WithRequestIDFromContext(),
|
||||||
|
//WithIgnoreRoutes("/hello"), // 忽略/hello
|
||||||
|
))
|
||||||
|
|
||||||
|
// 自定义zap log
|
||||||
|
//log, _ := logger.Init(logger.WithFormat("json"))
|
||||||
|
//r.Use(Logging(
|
||||||
|
// WithLog(log),
|
||||||
|
//))
|
||||||
|
|
||||||
|
helloFun := func(c *gin.Context) {
|
||||||
|
logger.Info("test request id", utils.FieldRequestIDFromContext(c))
|
||||||
|
response.Success(c, "hello world")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.GET("/hello", helloFun)
|
||||||
|
r.DELETE("/hello", helloFun)
|
||||||
|
r.POST("/hello", helloFun)
|
||||||
|
r.PUT("/hello", helloFun)
|
||||||
|
r.PATCH("/hello", helloFun)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := r.Run(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestRequest(t *testing.T) {
|
||||||
|
initServer1()
|
||||||
|
|
||||||
|
wantHello := "hello world"
|
||||||
|
result := &gohttp.StdResult{}
|
||||||
|
type User struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("get hello", func(t *testing.T) {
|
||||||
|
err := gohttp.Get(result, requestAddr+"/hello", gohttp.KV{"id": "100"})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got := result.Data.(string)
|
||||||
|
if got != wantHello {
|
||||||
|
t.Errorf("got: %s, want: %s", got, wantHello)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("delete hello", func(t *testing.T) {
|
||||||
|
err := gohttp.Delete(result, requestAddr+"/hello", gohttp.KV{"id": "100"})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got := result.Data.(string)
|
||||||
|
if got != wantHello {
|
||||||
|
t.Errorf("got: %s, want: %s", got, wantHello)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("post hello", func(t *testing.T) {
|
||||||
|
err := gohttp.Post(result, requestAddr+"/hello", &User{"foo"})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got := result.Data.(string)
|
||||||
|
if got != wantHello {
|
||||||
|
t.Errorf("got: %s, want: %s", got, wantHello)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("put hello", func(t *testing.T) {
|
||||||
|
err := gohttp.Put(result, requestAddr+"/hello", &User{"foo"})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got := result.Data.(string)
|
||||||
|
if got != wantHello {
|
||||||
|
t.Errorf("got: %s, want: %s", got, wantHello)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("patch hello", func(t *testing.T) {
|
||||||
|
err := gohttp.Patch(result, requestAddr+"/hello", &User{"foo"})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
got := result.Data.(string)
|
||||||
|
if got != wantHello {
|
||||||
|
t.Errorf("got: %s, want: %s", got, wantHello)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAddr() string {
|
||||||
|
port, _ := getAvailablePort()
|
||||||
|
requestAddr = fmt.Sprintf("http://localhost:%d", port)
|
||||||
|
return fmt.Sprintf(":%d", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAvailablePort() (int, error) {
|
||||||
|
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.ListenTCP("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
err = listener.Close()
|
||||||
|
|
||||||
|
return port, err
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
## metrics
|
||||||
|
|
||||||
|
gin metrics library, collect five metrics, `uptime`, `http_request_count_total`, `http_request_duration_seconds`, `http_request_size_bytes`, `http_response_size_bytes`.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
r.Use(metrics.Metrics(r,
|
||||||
|
//metrics.WithMetricsPath("/demo/metrics"), // default is /metrics
|
||||||
|
metrics.WithIgnoreStatusCodes(http.StatusNotFound), // ignore status codes
|
||||||
|
//metrics.WithIgnoreRequestMethods(http.MethodHead), // ignore request methods
|
||||||
|
//metrics.WithIgnoreRequestPaths("/ping", "/health"), // ignore request paths
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
Details about exposed Prometheus metrics.
|
||||||
|
|
||||||
|
| Name | Type | Exposed Information |
|
||||||
|
| ---- | ---- | ---------------------|
|
||||||
|
| gin_uptime | Counter | HTTP service uptime. |
|
||||||
|
| gin_http_request_count_total | Counter | Total number of HTTP requests made. |
|
||||||
|
| gin_http_request_duration_seconds | Histogram | HTTP request latencies in seconds. |
|
||||||
|
| gin_http_request_size_bytes | Summary | HTTP request sizes in bytes. |
|
||||||
|
| gin_http_response_size_bytes | Summary | HTTP response sizes in bytes. |
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Grafana charts
|
||||||
|
|
||||||
|
import [gin_grafana.json](gin_grafana.json) to your grafana, datasource name is `Prometheus`, change the name of the datasource according to your actual datasource.
|
||||||
|
|
||||||
|

|
Binary file not shown.
After Width: | Height: | Size: 247 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,140 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
namespace = "gin"
|
||||||
|
|
||||||
|
labels = []string{"status", "path", "method"}
|
||||||
|
|
||||||
|
uptime = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: "uptime",
|
||||||
|
Help: "HTTP service uptime.",
|
||||||
|
}, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
reqCount = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: "http_request_count_total",
|
||||||
|
Help: "Total number of HTTP requests made.",
|
||||||
|
}, labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
reqDuration = prometheus.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: "http_request_duration_seconds",
|
||||||
|
Help: "HTTP request latencies in seconds.",
|
||||||
|
}, labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
reqSizeBytes = prometheus.NewSummaryVec(
|
||||||
|
prometheus.SummaryOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: "http_request_size_bytes",
|
||||||
|
Help: "HTTP request sizes in bytes.",
|
||||||
|
}, labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
respSizeBytes = prometheus.NewSummaryVec(
|
||||||
|
prometheus.SummaryOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: "http_response_size_bytes",
|
||||||
|
Help: "HTTP response sizes in bytes.",
|
||||||
|
}, labels,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// init registers the prometheus metrics
|
||||||
|
func initPrometheus() {
|
||||||
|
prometheus.MustRegister(uptime, reqCount, reqDuration, reqSizeBytes, respSizeBytes)
|
||||||
|
go recordUptime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordUptime increases service uptime per 30 second.
|
||||||
|
func recordUptime() {
|
||||||
|
for range time.Tick(time.Second * 30) {
|
||||||
|
uptime.WithLabelValues().Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcRequestSize returns the size of request object.
|
||||||
|
func calcRequestSize(r *http.Request) float64 {
|
||||||
|
size := 0
|
||||||
|
if r.URL != nil {
|
||||||
|
size = len(r.URL.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
size += len(r.Method)
|
||||||
|
size += len(r.Proto)
|
||||||
|
|
||||||
|
for name, values := range r.Header {
|
||||||
|
size += len(name)
|
||||||
|
for _, value := range values {
|
||||||
|
size += len(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size += len(r.Host)
|
||||||
|
|
||||||
|
// r.Form and r.MultipartForm are assumed to be included in r.URL.
|
||||||
|
if r.ContentLength != -1 {
|
||||||
|
size += int(r.ContentLength)
|
||||||
|
}
|
||||||
|
return float64(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// metricsHandler wrappers the standard http.Handler to gin.HandlerFunc
|
||||||
|
func metricsHandler() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
handler := promhttp.Handler()
|
||||||
|
handler.ServeHTTP(c.Writer, c.Request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics returns a gin.HandlerFunc for exporting some Web metrics
|
||||||
|
func Metrics(r *gin.Engine, opts ...Option) gin.HandlerFunc {
|
||||||
|
o := defaultOptions()
|
||||||
|
o.apply(opts...)
|
||||||
|
|
||||||
|
// init prometheus
|
||||||
|
initPrometheus()
|
||||||
|
|
||||||
|
r.GET(o.metricsPath, metricsHandler())
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
ok := o.isIgnoreCodeStatus(c.Writer.Status()) ||
|
||||||
|
o.isIgnorePath(c.Request.URL.Path) ||
|
||||||
|
o.checkIgnoreMethod(c.Request.Method)
|
||||||
|
if ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// no response content will return -1
|
||||||
|
respSize := c.Writer.Size()
|
||||||
|
if respSize < 0 {
|
||||||
|
respSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
lvs := []string{strconv.Itoa(c.Writer.Status()), c.Request.URL.Path, c.Request.Method}
|
||||||
|
reqCount.WithLabelValues(lvs...).Inc()
|
||||||
|
reqDuration.WithLabelValues(lvs...).Observe(time.Since(start).Seconds())
|
||||||
|
reqSizeBytes.WithLabelValues(lvs...).Observe(calcRequestSize(c.Request))
|
||||||
|
respSizeBytes.WithLabelValues(lvs...).Observe(float64(respSize))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var requestAddr string
|
||||||
|
|
||||||
|
func initGin(r *gin.Engine, metricsFun gin.HandlerFunc) {
|
||||||
|
addr := getAddr()
|
||||||
|
|
||||||
|
r.Use(metricsFun)
|
||||||
|
|
||||||
|
r.GET("/hello", func(c *gin.Context) {
|
||||||
|
c.String(200, "[get] hello")
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := r.Run(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetricsPath(t *testing.T) {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
r := gin.New()
|
||||||
|
metricsFun := Metrics(r,
|
||||||
|
WithMetricsPath("/test/metrics"),
|
||||||
|
)
|
||||||
|
initGin(r, metricsFun)
|
||||||
|
|
||||||
|
resp, err := http.Get(requestAddr + "/test/metrics")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("code is %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnoreStatusCodes(t *testing.T) {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
r := gin.New()
|
||||||
|
metricsFun := Metrics(r,
|
||||||
|
WithIgnoreStatusCodes(http.StatusNotFound),
|
||||||
|
)
|
||||||
|
initGin(r, metricsFun)
|
||||||
|
|
||||||
|
_, err := http.Get(requestAddr + "/xxxxxx")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(requestAddr + "/metrics")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(string(body), `status="404"`) {
|
||||||
|
t.Fatal("ignore request status code [404] failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnoreRequestPaths(t *testing.T) {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
r := gin.New()
|
||||||
|
metricsFun := Metrics(r,
|
||||||
|
WithIgnoreRequestPaths("/hello"),
|
||||||
|
)
|
||||||
|
initGin(r, metricsFun)
|
||||||
|
|
||||||
|
_, err := http.Get(requestAddr + "/hello")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(requestAddr + "/metrics")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(string(body), `path="/hello"`) {
|
||||||
|
t.Fatal("ignore request paths [/hello] failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnoreRequestMethods(t *testing.T) {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
r := gin.New()
|
||||||
|
metricsFun := Metrics(r,
|
||||||
|
WithIgnoreRequestMethods(http.MethodGet),
|
||||||
|
)
|
||||||
|
initGin(r, metricsFun)
|
||||||
|
|
||||||
|
_, err := http.Get(requestAddr + "/hello")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(requestAddr + "/metrics")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(string(body), `method="GET"`) {
|
||||||
|
t.Fatal("ignore request method [GET] failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAddr() string {
|
||||||
|
port, _ := getAvailablePort()
|
||||||
|
requestAddr = fmt.Sprintf("http://localhost:%d", port)
|
||||||
|
return fmt.Sprintf(":%d", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAvailablePort() (int, error) {
|
||||||
|
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.ListenTCP("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
err = listener.Close()
|
||||||
|
|
||||||
|
return port, err
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option set the metrics options.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
metricsPath string
|
||||||
|
ignoreStatusCodes map[int]struct{}
|
||||||
|
ignoreRequestPaths map[string]struct{}
|
||||||
|
ignoreRequestMethods map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultOptions default value
|
||||||
|
func defaultOptions() *options {
|
||||||
|
return &options{
|
||||||
|
metricsPath: "/metrics",
|
||||||
|
ignoreStatusCodes: nil,
|
||||||
|
ignoreRequestPaths: nil,
|
||||||
|
ignoreRequestMethods: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *options) apply(opts ...Option) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMetricsPath set metrics path
|
||||||
|
func WithMetricsPath(metricsPath string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.metricsPath = metricsPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIgnoreStatusCodes ignore status codes
|
||||||
|
func WithIgnoreStatusCodes(statusCodes ...int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
codeMaps := make(map[int]struct{}, len(statusCodes))
|
||||||
|
for _, code := range statusCodes {
|
||||||
|
codeMaps[code] = struct{}{}
|
||||||
|
}
|
||||||
|
o.ignoreStatusCodes = codeMaps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIgnoreRequestPaths ignore request paths
|
||||||
|
func WithIgnoreRequestPaths(paths ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
pathMaps := make(map[string]struct{}, len(paths))
|
||||||
|
for _, path := range paths {
|
||||||
|
pathMaps[path] = struct{}{}
|
||||||
|
}
|
||||||
|
o.ignoreRequestPaths = pathMaps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIgnoreRequestMethods ignore request methods
|
||||||
|
func WithIgnoreRequestMethods(methods ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
methodMaps := make(map[string]struct{}, len(methods))
|
||||||
|
for _, method := range methods {
|
||||||
|
methodMaps[strings.ToUpper(method)] = struct{}{}
|
||||||
|
}
|
||||||
|
o.ignoreRequestMethods = methodMaps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *options) isIgnoreCodeStatus(statusCode int) bool {
|
||||||
|
if o.ignoreStatusCodes == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := o.ignoreStatusCodes[statusCode]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *options) isIgnorePath(path string) bool {
|
||||||
|
if o.ignoreRequestPaths == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := o.ignoreRequestPaths[path]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *options) checkIgnoreMethod(method string) bool {
|
||||||
|
if o.ignoreRequestMethods == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := o.ignoreRequestMethods[strings.ToUpper(method)]
|
||||||
|
return ok
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
## ratelimiter
|
||||||
|
|
||||||
|
gin `path` or `ip` limit.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
> go get -u github.com/zhufuyi/pkg/gin/middleware/ratelimiter
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// e.g. (1) default path limit, qps=500, burst=1000
|
||||||
|
// r.Use(QPS())
|
||||||
|
|
||||||
|
// e.g. (2) path limit, qps=50, burst=100
|
||||||
|
r.Use(QPS(
|
||||||
|
WithPath(),
|
||||||
|
WithQPS(50),
|
||||||
|
WithBurst(100),
|
||||||
|
))
|
||||||
|
|
||||||
|
// e.g. (3) ip limit, qps=20, burst=40
|
||||||
|
// r.Use(QPS(
|
||||||
|
// WithIP(),
|
||||||
|
// WithQPS(20),
|
||||||
|
// WithBurst(40),
|
||||||
|
// ))
|
||||||
|
```
|
|
@ -0,0 +1,67 @@
|
||||||
|
package ratelimiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// default qps value
|
||||||
|
defaultQPS rate.Limit = 500
|
||||||
|
|
||||||
|
// default the maximum instantaneous request spike allowed, burst >= qps
|
||||||
|
defaultBurst = 1000
|
||||||
|
|
||||||
|
// default is path limit, fault:path limit, true:ip limit
|
||||||
|
defaultIsIP = false //nolint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option set the rate limits options.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
func defaultOptions() *options {
|
||||||
|
return &options{
|
||||||
|
qps: defaultQPS,
|
||||||
|
burst: defaultBurst,
|
||||||
|
isIP: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
qps rate.Limit
|
||||||
|
burst int
|
||||||
|
isIP bool // false: path limit, true: IP limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *options) apply(opts ...Option) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithQPS set the qps value
|
||||||
|
func WithQPS(qps int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.qps = rate.Limit(qps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBurst set the burst value, burst >= qps
|
||||||
|
func WithBurst(burst int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.burst = burst
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPath set the path limit mode
|
||||||
|
func WithPath() Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.isIP = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIP set the path limit mode
|
||||||
|
func WithIP() Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.isIP = true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package ratelimiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
var l *Limiter
|
||||||
|
|
||||||
|
// Limiter is a controller for the request rate.
|
||||||
|
type Limiter struct {
|
||||||
|
qpsLimiter sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLimiter instantiation
|
||||||
|
func NewLimiter() *Limiter {
|
||||||
|
return &Limiter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLimiter get Limiter object, can be updated or query
|
||||||
|
func GetLimiter() *Limiter {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLimiter set limiter parameters
|
||||||
|
// "limit" indicates the number of token buckets to be added at a rate = value/second (e.g. 10 means 1 token every 100 ms)
|
||||||
|
// "burst" the maximum instantaneous request spike allowed
|
||||||
|
func (l *Limiter) SetLimiter(limit rate.Limit, burst int) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
path := c.FullPath()
|
||||||
|
l.qpsLimiter.LoadOrStore(path, rate.NewLimiter(limit, burst))
|
||||||
|
if !l.allow(path) {
|
||||||
|
c.JSON(http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Limiter) allow(path string) bool {
|
||||||
|
if limiter, exist := l.qpsLimiter.Load(path); exist {
|
||||||
|
if ql, ok := limiter.(*rate.Limiter); ok && !ql.Allow() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateQPSLimiter updates the settings for a given path's QPS limiter.
|
||||||
|
func (l *Limiter) UpdateQPSLimiter(path string, limit rate.Limit, burst int) {
|
||||||
|
if limiter, exist := l.qpsLimiter.Load(path); exist {
|
||||||
|
limiter.(*rate.Limiter).SetLimit(limit)
|
||||||
|
limiter.(*rate.Limiter).SetBurst(burst)
|
||||||
|
} else {
|
||||||
|
l.qpsLimiter.Store(path, rate.NewLimiter(limit, burst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQPSLimiterStatus returns the status of a given path's QPS limiter.
|
||||||
|
func (l *Limiter) GetQPSLimiterStatus(path string) (limit rate.Limit, burst int) {
|
||||||
|
if limiter, exist := l.qpsLimiter.Load(path); exist {
|
||||||
|
return limiter.(*rate.Limiter).Limit(), limiter.(*rate.Limiter).Burst()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// QPS set limit qps parameters
|
||||||
|
func QPS(opts ...Option) gin.HandlerFunc {
|
||||||
|
o := defaultOptions()
|
||||||
|
o.apply(opts...)
|
||||||
|
l = NewLimiter()
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var path string
|
||||||
|
if !o.isIP {
|
||||||
|
path = c.FullPath()
|
||||||
|
} else {
|
||||||
|
path = c.ClientIP()
|
||||||
|
}
|
||||||
|
|
||||||
|
l.qpsLimiter.LoadOrStore(path, rate.NewLimiter(o.qps, o.burst))
|
||||||
|
if !l.allow(path) {
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
package ratelimiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var requestAddr string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addr := getAddr()
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
r := gin.New()
|
||||||
|
|
||||||
|
// e.g. (1) path limit, qps=500, burst=1000
|
||||||
|
// r.Use(QPS())
|
||||||
|
|
||||||
|
// e.g. (2) path limit, qps=50, burst=100
|
||||||
|
r.Use(QPS(
|
||||||
|
WithPath(),
|
||||||
|
WithQPS(50),
|
||||||
|
WithBurst(100),
|
||||||
|
))
|
||||||
|
|
||||||
|
// e.g. (3) ip limit, qps=20, burst=40
|
||||||
|
// r.Use(QPS(
|
||||||
|
// WithIP(),
|
||||||
|
// WithQPS(20),
|
||||||
|
// WithBurst(40),
|
||||||
|
// ))
|
||||||
|
|
||||||
|
r.GET("/ping", func(c *gin.Context) {
|
||||||
|
c.JSON(200, "pong "+c.ClientIP())
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/hello", func(c *gin.Context) {
|
||||||
|
c.JSON(200, "hello "+c.ClientIP())
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := r.Run(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimiter_QPS(t *testing.T) {
|
||||||
|
success, failure := 0, 0
|
||||||
|
start := time.Now()
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
err := get(requestAddr + "/hello")
|
||||||
|
if err != nil {
|
||||||
|
failure++
|
||||||
|
if failure%10 == 0 {
|
||||||
|
fmt.Printf("%d %v\n", i, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond) // 间隔1毫秒
|
||||||
|
}
|
||||||
|
time := time.Now().Sub(start).Seconds()
|
||||||
|
t.Logf("time=%.3fs, success=%d, failure=%d, qps=%.1f", time, success, failure, float64(success)/time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRateLimiter(t *testing.T) {
|
||||||
|
var pingSuccess, pingFailures int32
|
||||||
|
var helloSuccess, helloFailures int32
|
||||||
|
|
||||||
|
for j := 0; j < 10; j++ {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := get(requestAddr + "/ping"); err != nil {
|
||||||
|
atomic.AddInt32(&pingFailures, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt32(&pingSuccess, 1)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := get(requestAddr + "/hello"); err != nil {
|
||||||
|
atomic.AddInt32(&helloFailures, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt32(&helloSuccess, 1)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
fmt.Printf("%s helloSuccess: %d, helloFailures: %d pingSuccess: %d, pingFailures: %d\n", time.Now().Format(time.RFC3339Nano), helloSuccess, helloFailures, pingSuccess, pingFailures)
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimiter_GetQPSLimiterStatus(t *testing.T) {
|
||||||
|
var pingSuccess, pingFailures int32
|
||||||
|
|
||||||
|
for j := 0; j < 10; j++ {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := get(requestAddr + "/ping"); err != nil {
|
||||||
|
atomic.AddInt32(&pingFailures, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt32(&pingSuccess, 1)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
qps, _ := GetLimiter().GetQPSLimiterStatus("/ping")
|
||||||
|
fmt.Printf("%s pingSuccess: %d, pingFailures: %d limit:%.f\n", time.Now().Format(time.RFC3339Nano), pingSuccess, pingFailures, qps)
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimiter_UpdateQPSLimiter(t *testing.T) {
|
||||||
|
var pingSuccess, pingFailures int32
|
||||||
|
|
||||||
|
for j := 0; j < 10; j++ {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := get(requestAddr + "/ping"); err != nil {
|
||||||
|
atomic.AddInt32(&pingFailures, 1)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt32(&pingSuccess, 1)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
limit, burst := GetLimiter().GetQPSLimiterStatus("/ping")
|
||||||
|
GetLimiter().UpdateQPSLimiter("/ping", limit+rate.Limit(j), burst)
|
||||||
|
fmt.Printf("%s pingSuccess: %d, pingFailures: %d limit:%.f\n", time.Now().Format(time.RFC3339Nano), pingSuccess, pingFailures, limit)
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAddr() string {
|
||||||
|
port, _ := getAvailablePort()
|
||||||
|
requestAddr = fmt.Sprintf("http://localhost:%d", port)
|
||||||
|
return fmt.Sprintf(":%d", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAvailablePort() (int, error) {
|
||||||
|
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.ListenTCP("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
err = listener.Close()
|
||||||
|
|
||||||
|
return port, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(url string) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errors.New(http.StatusText(resp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zhufuyi/sponge/pkg/krand"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ContextRequestIDKey context request id for context
|
||||||
|
ContextRequestIDKey = "request_id"
|
||||||
|
|
||||||
|
// HeaderXRequestIDKey http header request ID key
|
||||||
|
HeaderXRequestIDKey = "X-Request-ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestID is an interceptor that injects a 'X-Request-ID' into the context and request/response header of each request.
|
||||||
|
func RequestID() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Check for incoming header, use it if exists
|
||||||
|
requestID := c.Request.Header.Get(HeaderXRequestIDKey)
|
||||||
|
|
||||||
|
// Create request id
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = krand.String(krand.R_All, 12) // 生成长度为12的随机字符串
|
||||||
|
c.Request.Header.Set(HeaderXRequestIDKey, requestID)
|
||||||
|
// Expose it for use in the application
|
||||||
|
c.Set(ContextRequestIDKey, requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set X-Request-ID header
|
||||||
|
c.Writer.Header().Set(HeaderXRequestIDKey, requestID)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRequestIDFromContext returns 'RequestID' from the given context if present.
|
||||||
|
func GetRequestIDFromContext(c *gin.Context) string {
|
||||||
|
if v, ok := c.Get(ContextRequestIDKey); ok {
|
||||||
|
if requestID, ok := v.(string); ok {
|
||||||
|
return requestID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRequestIDFromHeaders returns 'RequestID' from the headers if present.
|
||||||
|
func GetRequestIDFromHeaders(c *gin.Context) string {
|
||||||
|
return c.Request.Header.Get(HeaderXRequestIDKey)
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
otelcontrib "go.opentelemetry.io/contrib"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
|
||||||
|
oteltrace "go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tracerKey = "otel-tracer"
|
||||||
|
tracerName = "otelgin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type traceConfig struct {
|
||||||
|
TracerProvider oteltrace.TracerProvider
|
||||||
|
Propagators propagation.TextMapPropagator
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceOption specifies instrumentation configuration options.
|
||||||
|
type TraceOption func(*traceConfig)
|
||||||
|
|
||||||
|
// WithPropagators specifies propagators to use for extracting
|
||||||
|
// information from the HTTP requests. If none are specified, global
|
||||||
|
// ones will be used.
|
||||||
|
func WithPropagators(propagators propagation.TextMapPropagator) TraceOption {
|
||||||
|
return func(cfg *traceConfig) {
|
||||||
|
cfg.Propagators = propagators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
|
||||||
|
// If none is specified, the global provider is used.
|
||||||
|
func WithTracerProvider(provider oteltrace.TracerProvider) TraceOption {
|
||||||
|
return func(cfg *traceConfig) {
|
||||||
|
cfg.TracerProvider = provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracing returns interceptor that will trace incoming requests.
|
||||||
|
// The service parameter should describe the name of the (virtual)
|
||||||
|
// server handling the request.
|
||||||
|
func Tracing(serviceName string, opts ...TraceOption) gin.HandlerFunc {
|
||||||
|
cfg := traceConfig{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&cfg)
|
||||||
|
}
|
||||||
|
if cfg.TracerProvider == nil {
|
||||||
|
cfg.TracerProvider = otel.GetTracerProvider()
|
||||||
|
}
|
||||||
|
tracer := cfg.TracerProvider.Tracer(
|
||||||
|
tracerName,
|
||||||
|
oteltrace.WithInstrumentationVersion(otelcontrib.SemVersion()),
|
||||||
|
)
|
||||||
|
if cfg.Propagators == nil {
|
||||||
|
cfg.Propagators = otel.GetTextMapPropagator()
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Set(tracerKey, tracer)
|
||||||
|
savedCtx := c.Request.Context()
|
||||||
|
defer func() {
|
||||||
|
c.Request = c.Request.WithContext(savedCtx)
|
||||||
|
}()
|
||||||
|
ctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header))
|
||||||
|
route := c.FullPath()
|
||||||
|
opts := []oteltrace.SpanStartOption{
|
||||||
|
oteltrace.WithAttributes(semconv.NetAttributesFromHTTPRequest("tcp", c.Request)...),
|
||||||
|
oteltrace.WithAttributes(semconv.EndUserAttributesFromHTTPRequest(c.Request)...),
|
||||||
|
oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(serviceName, route, c.Request)...),
|
||||||
|
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
|
||||||
|
}
|
||||||
|
spanName := route
|
||||||
|
if spanName == "" {
|
||||||
|
spanName = fmt.Sprintf("HTTP %s route not found", c.Request.Method)
|
||||||
|
}
|
||||||
|
ctx, span := tracer.Start(ctx, spanName, opts...)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// pass the span through the request context
|
||||||
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
|
||||||
|
// serve the request to the next interceptor
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
status := c.Writer.Status()
|
||||||
|
attrs := semconv.HTTPAttributesFromHTTPStatusCode(status)
|
||||||
|
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCode(status)
|
||||||
|
span.SetAttributes(attrs...)
|
||||||
|
span.SetStatus(spanStatus, spanMessage)
|
||||||
|
if len(c.Errors) > 0 {
|
||||||
|
span.SetAttributes(attribute.String("gin.errors", c.Errors.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
## response
|
||||||
|
|
||||||
|
封装gin返回json数据插件。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
> go get -u github.com/zhufuyi/pkg/gin/response
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
`Output`函数返回兼容http状态码
|
||||||
|
|
||||||
|
`Success`和`Error`统一返回状态码200,在data.code自定义状态码
|
||||||
|
|
||||||
|
所有请求统一返回json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// c是*gin.Context
|
||||||
|
|
||||||
|
// 返回成功
|
||||||
|
response.Success(c)
|
||||||
|
// 返回成功,并返回数据
|
||||||
|
response.Success(c, gin.H{"users":users})
|
||||||
|
|
||||||
|
// 返回失败
|
||||||
|
response.Error(c, errcode.SendEmailErr)
|
||||||
|
// 返回失败,并返回数据
|
||||||
|
response.Error(c, errcode.SendEmailErr, gin.H{"user":user})
|
||||||
|
```
|
|
@ -0,0 +1,108 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/errcode"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result 输出数据格式
|
||||||
|
type Result struct {
|
||||||
|
Code int `json:"code"` // 返回码
|
||||||
|
Msg string `json:"msg"` // 返回信息说明
|
||||||
|
Data interface{} `json:"data"` // 返回数据
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResp(code int, msg string, data interface{}) *Result {
|
||||||
|
resp := &Result{
|
||||||
|
Code: code,
|
||||||
|
Msg: msg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保证返回时data字段不为nil,注意resp.Data=[]interface {}时不为nil,经过序列化变成了null
|
||||||
|
if data == nil {
|
||||||
|
resp.Data = &struct{}{}
|
||||||
|
} else {
|
||||||
|
resp.Data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonContentType = []string{"application/json; charset=utf-8"}
|
||||||
|
|
||||||
|
func writeContentType(w http.ResponseWriter, value []string) {
|
||||||
|
header := w.Header()
|
||||||
|
if val := header["Content-Type"]; len(val) == 0 {
|
||||||
|
header["Content-Type"] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(c *gin.Context, code int, res interface{}) {
|
||||||
|
c.Writer.WriteHeader(code)
|
||||||
|
writeContentType(c.Writer, jsonContentType)
|
||||||
|
err := json.NewEncoder(c.Writer).Encode(res)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("json encode error, err = %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func respJSONWithStatusCode(c *gin.Context, code int, msg string, data ...interface{}) {
|
||||||
|
var FirstData interface{}
|
||||||
|
if len(data) > 0 {
|
||||||
|
FirstData = data[0]
|
||||||
|
}
|
||||||
|
resp := newResp(code, msg, FirstData)
|
||||||
|
|
||||||
|
writeJSON(c, code, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output 根据http status code返回json数据
|
||||||
|
func Output(c *gin.Context, code int, msg ...interface{}) {
|
||||||
|
switch code {
|
||||||
|
case http.StatusOK:
|
||||||
|
respJSONWithStatusCode(c, http.StatusOK, "ok", msg...)
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
respJSONWithStatusCode(c, http.StatusBadRequest, errcode.InvalidParams.Msg(), msg...)
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
respJSONWithStatusCode(c, http.StatusUnauthorized, errcode.Unauthorized.Msg(), msg...)
|
||||||
|
case http.StatusForbidden:
|
||||||
|
respJSONWithStatusCode(c, http.StatusForbidden, errcode.Forbidden.Msg(), msg...)
|
||||||
|
case http.StatusNotFound:
|
||||||
|
respJSONWithStatusCode(c, http.StatusNotFound, errcode.NotFound.Msg(), msg...)
|
||||||
|
case http.StatusRequestTimeout:
|
||||||
|
respJSONWithStatusCode(c, http.StatusRequestTimeout, errcode.Timeout.Msg(), msg...)
|
||||||
|
case http.StatusConflict:
|
||||||
|
respJSONWithStatusCode(c, http.StatusConflict, errcode.AlreadyExists.Msg(), msg...)
|
||||||
|
case http.StatusInternalServerError:
|
||||||
|
respJSONWithStatusCode(c, http.StatusInternalServerError, errcode.InternalServerError.Msg(), msg...)
|
||||||
|
|
||||||
|
default:
|
||||||
|
respJSONWithStatusCode(c, code, http.StatusText(code), msg...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态码统一200,自定义错误码在data.code
|
||||||
|
func respJSONWith200(c *gin.Context, code int, msg string, data ...interface{}) {
|
||||||
|
var FirstData interface{}
|
||||||
|
if len(data) > 0 {
|
||||||
|
FirstData = data[0]
|
||||||
|
}
|
||||||
|
resp := newResp(code, msg, FirstData)
|
||||||
|
|
||||||
|
writeJSON(c, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success 正确
|
||||||
|
func Success(c *gin.Context, data ...interface{}) {
|
||||||
|
respJSONWith200(c, 0, "ok", data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 错误
|
||||||
|
func Error(c *gin.Context, err *errcode.Error, data ...interface{}) {
|
||||||
|
respJSONWith200(c, err.Code(), err.Msg(), data...)
|
||||||
|
}
|
|
@ -0,0 +1,270 @@
|
||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/errcode"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
requestAddr string
|
||||||
|
wantCode int
|
||||||
|
wantData interface{}
|
||||||
|
wantErrInfo *errcode.Error
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
port, _ := getAvailablePort()
|
||||||
|
requestAddr = fmt.Sprintf("http://localhost:%d", port)
|
||||||
|
addr := fmt.Sprintf(":%d", port)
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
r.GET("/hello1", func(c *gin.Context) { Output(c, wantCode, wantData) })
|
||||||
|
r.GET("/hello2", func(c *gin.Context) { Success(c, wantData) })
|
||||||
|
r.GET("/hello3", func(c *gin.Context) { Error(c, wantErrInfo) })
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := r.Run(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可用端口
|
||||||
|
func getAvailablePort() (int, error) {
|
||||||
|
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.ListenTCP("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
err = listener.Close()
|
||||||
|
|
||||||
|
return port, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func do(method string, url string, body interface{}) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
resp *http.Response
|
||||||
|
err error
|
||||||
|
contentType = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
v, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
case http.MethodGet:
|
||||||
|
resp, err = http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
case http.MethodPost:
|
||||||
|
resp, err = http.Post(url, contentType, bytes.NewReader(v))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
case http.MethodDelete, http.MethodPut, http.MethodPatch:
|
||||||
|
req, err := http.NewRequest(method, url, bytes.NewReader(v))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
resp, err = http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s method not supported", method)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(url string) ([]byte, error) {
|
||||||
|
return do(http.MethodGet, url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(url string) ([]byte, error) {
|
||||||
|
return do(http.MethodDelete, url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func post(url string, body interface{}) ([]byte, error) {
|
||||||
|
return do(http.MethodPost, url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func put(url string, body interface{}) ([]byte, error) {
|
||||||
|
return do(http.MethodPut, url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func patch(url string, body interface{}) ([]byte, error) {
|
||||||
|
return do(http.MethodPatch, url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestRespond(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
url string
|
||||||
|
code int
|
||||||
|
data interface{}
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "respond 200",
|
||||||
|
args: args{
|
||||||
|
url: requestAddr + "/hello1",
|
||||||
|
code: http.StatusOK,
|
||||||
|
data: gin.H{"name": "zhangsan"},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "respond 400",
|
||||||
|
args: args{
|
||||||
|
url: requestAddr + "/hello1",
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
data: nil,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
wantCode = tt.args.code
|
||||||
|
wantData = tt.args.data
|
||||||
|
data, err := get(tt.args.url)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("http.Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("%s", data)
|
||||||
|
var resp = &Result{}
|
||||||
|
err = json.Unmarshal(data, resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.Code != wantCode {
|
||||||
|
t.Errorf("%s, got = %v, want %v", tt.name, resp.Code, wantCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuccess(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
url string
|
||||||
|
code int
|
||||||
|
data interface{}
|
||||||
|
ei *errcode.Error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
args: args{
|
||||||
|
url: requestAddr + "/hello2",
|
||||||
|
code: http.StatusOK,
|
||||||
|
data: gin.H{"name": "zhangsan"},
|
||||||
|
ei: errcode.Success,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
wantData = tt.args.data
|
||||||
|
wantErrInfo = tt.args.ei
|
||||||
|
data, err := get(tt.args.url)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("http.Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("%s", data)
|
||||||
|
var resp = &Result{}
|
||||||
|
err = json.Unmarshal(data, resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.Code != wantErrInfo.Code() && resp.Msg != wantErrInfo.Msg() {
|
||||||
|
t.Errorf("%s, got = %v, want %v", tt.name, resp, wantErrInfo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
url string
|
||||||
|
code int
|
||||||
|
data interface{}
|
||||||
|
ei *errcode.Error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unauthorized",
|
||||||
|
args: args{
|
||||||
|
url: requestAddr + "/hello3",
|
||||||
|
code: http.StatusOK,
|
||||||
|
data: nil,
|
||||||
|
ei: errcode.Unauthorized,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
wantErrInfo = tt.args.ei
|
||||||
|
data, err := get(tt.args.url)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("http.Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("%s", data)
|
||||||
|
var resp = &Result{}
|
||||||
|
err = json.Unmarshal(data, resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.Code != wantErrInfo.Code() && resp.Msg != wantErrInfo.Msg() {
|
||||||
|
t.Errorf("%s, got = %v, want %v", tt.name, resp, wantErrInfo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
## render
|
||||||
|
|
||||||
|
gin请求参数校验。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
> go get -u github.com/zhufuyi/pkg/gin/validator
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zhufuyi/sponge/pkg/gin/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
binding.Validator = validator.Init()
|
||||||
|
|
||||||
|
r.Run(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从请求body获取
|
||||||
|
type createUserRequest struct {
|
||||||
|
Name string `json:"name" form:"name" binding:"required"`
|
||||||
|
Password string `json:"password" form:"password" binding:"required"`
|
||||||
|
Age int `json:"age" form:"age" binding:"gte=0,lte=120"`
|
||||||
|
Email string `json:"email" form:"email" binding:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateUser(c *gin.Context) {
|
||||||
|
form := &createUserRequest{}
|
||||||
|
err := c.ShouldBindJSON(form)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"msg": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从请求url参数获取
|
||||||
|
type getUserRequest struct {
|
||||||
|
Page int `json:"page" form:"page" binding:"gte=0"`
|
||||||
|
Size int `json:"size" form:"size" binding:"gt=0"`
|
||||||
|
Sort string `json:"sort" form:"sort" binding:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUsers(c *gin.Context) {
|
||||||
|
form := &getUserRequest{}
|
||||||
|
err := c.ShouldBindQuery(form)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := getUsers(form)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"users": users})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUsers(req *getUserRequest) ([]User,error){}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue