mirror of https://github.com/zhufuyi/sponge
init
This commit is contained in:
parent
1dab814b5b
commit
083a0499c1
|
@ -4,6 +4,7 @@
|
|||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
cmd/sponge/sponge
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
@ -12,4 +13,10 @@
|
|||
*.out
|
||||
|
||||
# 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
|
||||
microservice framework.
|
||||
## sponge
|
||||
|
||||
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