mirror of
https://github.com/wonli/aqi.git
synced 2024-06-28 09:57:05 +08:00
init
This commit is contained in:
parent
af402cc224
commit
b9be165fd2
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# Project
|
||||
examples/data
|
||||
examples/**.yaml
|
||||
|
||||
# Binaries
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Mac
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# VS Code
|
||||
.vscode
|
||||
*.project
|
||||
*.factorypath
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/*
|
||||
!.idea/icon.png
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
# Test binary
|
||||
*.test
|
||||
|
||||
# Output
|
||||
*.out
|
||||
|
210
README.md
Normal file
210
README.md
Normal file
@ -0,0 +1,210 @@
|
||||
# Aqi
|
||||
|
||||
Aqi is a Golang Websocket business framework that supports net/http, gin, chi, etc. It integrates underlying third-party libraries such as viper, gorm, gobwa/ws, gjson, zap, asynq, which facilitates the rapid development of Websocket applications.
|
||||
|
||||
### Installation
|
||||
|
||||
`go get -u github.com/wonli/aqi`
|
||||
|
||||
[简体中文](./docs/zh-CN.md)
|
||||
|
||||
### Usage
|
||||
|
||||
On the first run, a `config-dev.yaml` configuration file will be automatically generated in the working directory. You can configure the application start port, database, and other settings.
|
||||
|
||||
After the service starts, use [wscat](https://github.com/websockets/wscat) to establish a websocket connection with the server. Here is a screenshot of the operation.
|
||||
|
||||
|
||||
|
||||
![img](./docs/assets/img.png)
|
||||
|
||||
|
||||
|
||||
### Interaction Protocol
|
||||
|
||||
Input and output uniformly use `JSON`. Here, `Action` is the name registered in the routing, and `Params` are in JSON string format.
|
||||
|
||||
```go
|
||||
|
||||
type Context struct {
|
||||
...
|
||||
Action string
|
||||
Params string
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
Response Format:
|
||||
|
||||
```go
|
||||
type Action struct {
|
||||
Action string `json:"action"`
|
||||
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Quick Start
|
||||
|
||||
In `ahi.Init`, specify the configuration file via `aqi.ConfigFile`, which defaults to `yaml` format. The service name and port are specified in the `yaml` file path through `aqi.HttpServer`. The contents of the entry file are as follows:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/wonli/aqi"
|
||||
"github.com/wonli/aqi/ws"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := aqi.Init(
|
||||
aqi.ConfigFile("config.yaml"),
|
||||
aqi.HttpServer("Aqi", "port"),
|
||||
)
|
||||
|
||||
// Create router
|
||||
mux := http.NewServeMux()
|
||||
// WebSocket Handler
|
||||
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
ws.HttpHandler(w, r)
|
||||
})
|
||||
|
||||
// Register router
|
||||
wsr := ws.NewRouter()
|
||||
wsr.Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
|
||||
app.WithHttpServer(mux)
|
||||
|
||||
// 启动应用
|
||||
app.Start()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### With Gin
|
||||
|
||||
`aqi` can be very conveniently integrated with other WEB frameworks. You just need to correctly register `handler` and `app.WithHttpServer`. It supports any implementation of `http.Handler`.
|
||||
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/wonli/aqi"
|
||||
"github.com/wonli/aqi/ws"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := aqi.Init(
|
||||
aqi.ConfigFile("config.yaml"),
|
||||
aqi.HttpServer("Aqi", "port"),
|
||||
)
|
||||
|
||||
engine := gin.Default()
|
||||
// Handler
|
||||
engine.GET("/ws", func(c *gin.Context) {
|
||||
ws.HttpHandler(c.Writer, c.Request)
|
||||
})
|
||||
|
||||
// Router
|
||||
wsr := ws.NewRouter()
|
||||
wsr.Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
|
||||
app.WithHttpServer(engine)
|
||||
app.Start()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Middlewares
|
||||
|
||||
First, define a simple logging middleware that prints the received `action` before processing the request and prints the response content after processing.
|
||||
|
||||
```go
|
||||
func logMiddleware() func(a *ws.Context) {
|
||||
return func(a *ws.Context) {
|
||||
log.Printf("Request action: %s ", a.Action)
|
||||
a.Next()
|
||||
log.Printf("Reqponse data: %s ", a.Response.Data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register the middleware to the router using the `Use` method.
|
||||
|
||||
```go
|
||||
// 注册WebSocket路由
|
||||
wsr := ws.NewRouter()
|
||||
wsr.Use(logMiddleware()).Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
You can also use the form of router groups.
|
||||
|
||||
```go
|
||||
// Router
|
||||
wsr := ws.NewRouter()
|
||||
r1 := wsr.Use(logMiddleware())
|
||||
{
|
||||
r1.Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
|
||||
r1.Add("say", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"say": "hi",
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This way, the console will print logs before and after each request.
|
||||
|
||||
|
||||
|
||||
### Production Mode
|
||||
|
||||
Compiling `Aqi` directly will run in `dev` mode. To run in production mode, pass the following parameters during compilation. For more details, please refer to the `examples/Makefile` file.
|
||||
|
||||
|
||||
|
||||
```shell
|
||||
LDFLAGS = "-X '$(FLAGS_PKG).BuildDate=$(BUILD_DATE)' \
|
||||
-X '$(FLAGS_PKG).Branch=$(GIT_BRANCH)' \
|
||||
-X '$(FLAGS_PKG).CommitVersion=$(GIT_COMMIT)' \
|
||||
-X '$(FLAGS_PKG).Revision=$(GIT_REVISION)' \
|
||||
-extldflags '-static -s -w'"
|
||||
```
|
||||
|
||||
|
||||
|
24
apic/api.go
Normal file
24
apic/api.go
Normal file
@ -0,0 +1,24 @@
|
||||
package apic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Api api client interface.
|
||||
type Api interface {
|
||||
Url() string // Request API full URL.
|
||||
Path() string // Request path.
|
||||
Query() url.Values // URL query parameters.
|
||||
Headers() Params // Headers required for the request.
|
||||
PostBody() Params // Request parameters.
|
||||
FormData() Params // Form data as map[string]string.
|
||||
WWWFormData() Params // Form data as map[string]string.
|
||||
Setup(api Api, op *Options) (Api, error) // Setup for the API.
|
||||
HttpMethod() HttpMethod // HTTP method of the request.
|
||||
Debug() bool // Whether to run in debug mode.
|
||||
UseContext(ctx context.Context) error // Use context.
|
||||
OnRequest() error // Handle request data.
|
||||
OnHttpStatusError(code int, resp []byte) error // Handle HTTP status errors.
|
||||
OnResponse(resp []byte) (*ResponseData, error) // Process response data.
|
||||
}
|
14
apic/api_id.go
Normal file
14
apic/api_id.go
Normal file
@ -0,0 +1,14 @@
|
||||
package apic
|
||||
|
||||
type ApiId struct {
|
||||
Name string
|
||||
Client Api
|
||||
Request *RequestData
|
||||
Response *ResponseData
|
||||
}
|
||||
|
||||
// Named registers as a named interface.
|
||||
func (a *ApiId) Named() *ApiId {
|
||||
Apis.Named(a)
|
||||
return a
|
||||
}
|
11
apic/api_options.go
Normal file
11
apic/api_options.go
Normal file
@ -0,0 +1,11 @@
|
||||
package apic
|
||||
|
||||
import "net/url"
|
||||
|
||||
// Options request options
|
||||
type Options struct {
|
||||
Query url.Values
|
||||
PostBody Params
|
||||
Headers Params
|
||||
Setup Params
|
||||
}
|
56
apic/api_request.go
Normal file
56
apic/api_request.go
Normal file
@ -0,0 +1,56 @@
|
||||
package apic
|
||||
|
||||
import "net/url"
|
||||
|
||||
type RequestData struct {
|
||||
Url string `json:"url"`
|
||||
HttpMethod HttpMethod `json:"httpMethod,omitempty"`
|
||||
ApiId string `json:"apiId"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Query url.Values `json:"query,omitempty"`
|
||||
Form Params `json:"form,omitempty"`
|
||||
WWWForm Params `json:"WWWForm,omitempty"`
|
||||
PostBody Params `json:"post_body,omitempty"`
|
||||
Header Params `json:"header,omitempty"`
|
||||
Debug bool `json:"debug"`
|
||||
}
|
||||
|
||||
func (a *RequestData) InitFromApiClient(api Api) {
|
||||
if a.Url == "" {
|
||||
a.Url = api.Url()
|
||||
}
|
||||
|
||||
if a.Path == "" {
|
||||
a.Path = api.Path()
|
||||
}
|
||||
|
||||
if a.HttpMethod == "" {
|
||||
a.HttpMethod = api.HttpMethod()
|
||||
}
|
||||
|
||||
if a.Query == nil {
|
||||
a.Query = api.Query()
|
||||
}
|
||||
|
||||
if a.PostBody == nil {
|
||||
a.PostBody = api.PostBody()
|
||||
}
|
||||
|
||||
if a.Header == nil {
|
||||
a.Header = api.Headers()
|
||||
}
|
||||
|
||||
if a.Form == nil {
|
||||
a.Form = api.FormData()
|
||||
}
|
||||
|
||||
if a.WWWForm == nil {
|
||||
a.WWWForm = api.WWWFormData()
|
||||
}
|
||||
|
||||
a.Debug = api.Debug()
|
||||
}
|
||||
|
||||
func (a *RequestData) MarshalToString() (string, error) {
|
||||
return marshal(a)
|
||||
}
|
26
apic/api_response.go
Normal file
26
apic/api_response.go
Normal file
@ -0,0 +1,26 @@
|
||||
package apic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ResponseData struct {
|
||||
HttpStatus int `json:"http_status"`
|
||||
Header http.Header `json:"header,omitempty"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
func (a *ResponseData) MarshalToString() (string, error) {
|
||||
return marshal(a)
|
||||
}
|
||||
|
||||
func (a *ResponseData) BindJson(d any) error {
|
||||
err := json.Unmarshal(a.Data, d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
12
apic/api_util.go
Normal file
12
apic/api_util.go
Normal file
@ -0,0 +1,12 @@
|
||||
package apic
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
var marshal = func(a any) (string, error) {
|
||||
data, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
89
apic/apic.go
Normal file
89
apic/apic.go
Normal file
@ -0,0 +1,89 @@
|
||||
package apic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
// Apic is an empty implementation of the Api interface.
|
||||
// Introducing this in business logic can avoid writing too much boilerplate code.
|
||||
type Apic struct {
|
||||
Api
|
||||
}
|
||||
|
||||
func (a *Apic) Url() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *Apic) Path() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *Apic) Query() url.Values {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apic) Headers() Params {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apic) PostBody() Params {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apic) FormData() Params {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apic) WWWFormData() Params {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apic) Setup(api Api, op *Options) (Api, error) {
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func (a *Apic) HttpMethod() HttpMethod {
|
||||
return POST
|
||||
}
|
||||
|
||||
func (a *Apic) Debug() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *Apic) UseContext(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apic) OnRequest() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Apic) OnResponse(resp []byte) (*ResponseData, error) {
|
||||
return &ResponseData{Data: resp}, nil
|
||||
}
|
||||
|
||||
func (a *Apic) OnHttpStatusError(code int, resp []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AnyToParams converts any type to Params.
|
||||
func (a *Apic) AnyToParams(d any) Params {
|
||||
dByte, err := json.Marshal(d)
|
||||
if err != nil {
|
||||
logger.SugarLog.Errorf("Failed to convert type to byte %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
var p Params
|
||||
err = json.Unmarshal(dByte, &p)
|
||||
if err != nil {
|
||||
logger.SugarLog.Errorf("Failed to convert to Params %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
230
apic/http_client.go
Normal file
230
apic/http_client.go
Normal file
@ -0,0 +1,230 @@
|
||||
package apic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/guonaihong/gout"
|
||||
"github.com/guonaihong/gout/dataflow"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var Apis *ApiClients
|
||||
var once sync.Once
|
||||
|
||||
// ApiClients api clients list
|
||||
type ApiClients struct {
|
||||
ctx context.Context
|
||||
proxy string
|
||||
named map[string]*ApiId
|
||||
}
|
||||
|
||||
func Init() *ApiClients {
|
||||
once.Do(func() {
|
||||
Apis = &ApiClients{
|
||||
ctx: context.Background(),
|
||||
named: map[string]*ApiId{},
|
||||
}
|
||||
})
|
||||
|
||||
return Apis
|
||||
}
|
||||
|
||||
func (a *ApiClients) Named(api *ApiId) {
|
||||
_, ok := a.named[api.Name]
|
||||
if ok {
|
||||
panic("ApiId registered multiple times")
|
||||
}
|
||||
|
||||
a.named[api.Name] = api
|
||||
}
|
||||
|
||||
func (a *ApiClients) WithContext(ctx context.Context) *ApiClients {
|
||||
a.ctx = ctx
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ApiClients) WithProxy(proxy string) *ApiClients {
|
||||
a.proxy = proxy
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ApiClients) Call(id *ApiId, op *Options) error {
|
||||
_, err := a.getApiData(id, op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ApiClients) CallApi(id *ApiId, op *Options) (*ResponseData, error) {
|
||||
return a.getApiData(id, op)
|
||||
}
|
||||
|
||||
func (a *ApiClients) CallNamed(name string, op *Options) (*ResponseData, error) {
|
||||
id, ok := a.named[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("named api not registered")
|
||||
}
|
||||
|
||||
return a.getApiData(id, op)
|
||||
}
|
||||
|
||||
func (a *ApiClients) CallGJson(id *ApiId, op *Options) (*gjson.Result, error) {
|
||||
apiData, err := a.getApiData(id, op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := gjson.ParseBytes(apiData.Data)
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
func (a *ApiClients) CallBindJson(id *ApiId, resp any, op *Options) error {
|
||||
_, err := a.getApiData(id, op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return id.Response.BindJson(resp)
|
||||
}
|
||||
|
||||
func (a *ApiClients) CallFunc(id *ApiId, op *Options, callback func(a *Api, data []byte) error) error {
|
||||
apiData, err := a.getApiData(id, op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return callback(&id.Client, apiData.Data)
|
||||
}
|
||||
|
||||
func (a *ApiClients) getApiData(id *ApiId, op *Options) (*ResponseData, error) {
|
||||
api := id.Client
|
||||
if id.Request == nil {
|
||||
id.Request = &RequestData{}
|
||||
}
|
||||
|
||||
if op == nil {
|
||||
op = &Options{}
|
||||
}
|
||||
|
||||
id.Request.ApiId = id.Name
|
||||
id.Request.InitFromApiClient(id.Client)
|
||||
|
||||
//setup
|
||||
api, err := api.Setup(id.Client, op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = api.UseContext(a.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Merge query parameters
|
||||
if op.Query != nil {
|
||||
if id.Request.Query == nil {
|
||||
id.Request.Query = op.Query
|
||||
} else {
|
||||
for key, valData := range op.Query {
|
||||
if len(valData) == 1 {
|
||||
id.Request.Query.Add(key, valData[0])
|
||||
} else {
|
||||
for _, val := range valData {
|
||||
id.Request.Query.Add(key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//set postBody
|
||||
if op.PostBody != nil {
|
||||
if id.Request.PostBody == nil {
|
||||
id.Request.PostBody = op.PostBody
|
||||
} else {
|
||||
for key, val := range op.PostBody {
|
||||
id.Request.PostBody[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//set header
|
||||
if op.Headers != nil {
|
||||
if id.Request.Header == nil {
|
||||
id.Request.Header = op.Headers
|
||||
} else {
|
||||
for key, val := range op.Headers {
|
||||
id.Request.Header[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = api.OnRequest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiAddress = id.Request.Url + id.Request.Path
|
||||
var client *dataflow.DataFlow
|
||||
switch id.Request.HttpMethod {
|
||||
case POST:
|
||||
client = gout.POST(apiAddress)
|
||||
case DELETE:
|
||||
client = gout.DELETE(apiAddress)
|
||||
case HEAD:
|
||||
client = gout.HEAD(apiAddress)
|
||||
case OPTIONS:
|
||||
client = gout.OPTIONS(apiAddress)
|
||||
case PATCH:
|
||||
client = gout.OPTIONS(apiAddress)
|
||||
default:
|
||||
client = gout.GET(apiAddress)
|
||||
}
|
||||
|
||||
if a.proxy != "" {
|
||||
client.SetProxy(a.proxy)
|
||||
}
|
||||
|
||||
client.Debug(id.Request.Debug)
|
||||
if id.Request.Form != nil {
|
||||
client.SetForm(id.Request.Form)
|
||||
} else if id.Request.WWWForm != nil {
|
||||
client.SetWWWForm(id.Request.WWWForm)
|
||||
} else if id.Request.PostBody != nil {
|
||||
client.SetJSON(id.Request.PostBody)
|
||||
}
|
||||
|
||||
if id.Request.Query != nil {
|
||||
client.SetQuery(id.Request.Query)
|
||||
}
|
||||
|
||||
if id.Request.Header != nil {
|
||||
client.SetHeader(id.Request.Header)
|
||||
}
|
||||
|
||||
id.Response = &ResponseData{}
|
||||
err = client.Code(&id.Response.HttpStatus).
|
||||
BindHeader(&id.Response.Header).BindBody(&id.Response.Data).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id.Response.HttpStatus != http.StatusOK {
|
||||
err = api.OnHttpStatusError(id.Response.HttpStatus, id.Response.Data)
|
||||
if err != nil {
|
||||
return id.Response, err
|
||||
}
|
||||
}
|
||||
|
||||
responseData, err := api.OnResponse(id.Response.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return responseData, nil
|
||||
}
|
18
apic/http_method.go
Normal file
18
apic/http_method.go
Normal file
@ -0,0 +1,18 @@
|
||||
package apic
|
||||
|
||||
import "net/http"
|
||||
|
||||
// HttpMethod http method name
|
||||
type HttpMethod string
|
||||
|
||||
const (
|
||||
GET HttpMethod = http.MethodGet
|
||||
HEAD HttpMethod = http.MethodHead
|
||||
POST HttpMethod = http.MethodPost
|
||||
PUT HttpMethod = http.MethodPut
|
||||
PATCH HttpMethod = http.MethodPatch
|
||||
DELETE HttpMethod = http.MethodDelete
|
||||
CONNECT HttpMethod = http.MethodConnect
|
||||
OPTIONS HttpMethod = http.MethodOptions
|
||||
TRACE HttpMethod = http.MethodTrace
|
||||
)
|
30
apic/http_params.go
Normal file
30
apic/http_params.go
Normal file
@ -0,0 +1,30 @@
|
||||
package apic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Params map[string]any
|
||||
type Params map[string]any
|
||||
|
||||
func (p Params) With(key string, val any) Params {
|
||||
p[key] = val
|
||||
return p
|
||||
}
|
||||
|
||||
func (p Params) WithParams(params map[string]any) Params {
|
||||
for key, val := range params {
|
||||
p[key] = val
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p Params) Marshal() []byte {
|
||||
bytes, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
209
app.go
Normal file
209
app.go
Normal file
@ -0,0 +1,209 @@
|
||||
package aqi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wonli/aqi/config"
|
||||
"github.com/wonli/aqi/logger"
|
||||
"github.com/wonli/aqi/validate"
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
|
||||
//运行时数据存储基础路径
|
||||
DataPath string
|
||||
|
||||
//应用日志文件配置路径
|
||||
LogPathKey string
|
||||
|
||||
//默认语言
|
||||
Language string
|
||||
|
||||
//开发模式
|
||||
devMode bool
|
||||
|
||||
//服务名称,support.Version
|
||||
//当指定 HttpServerPortFindPath 时,在配置读取之后从配置路径获取http端口
|
||||
Servername []string
|
||||
ServerPort string
|
||||
HttpServerPortFindPath string
|
||||
|
||||
ConfigType string //配置文件类型
|
||||
ConfigPath string //配置文件路径
|
||||
ConfigName string //配置文件名称
|
||||
|
||||
Configs map[string]any
|
||||
|
||||
HttpServer http.Handler //http server
|
||||
|
||||
RemoteProvider *RemoteProvider //远程配置支持etcd, consul
|
||||
|
||||
WatchHandler func()
|
||||
}
|
||||
|
||||
var acf *AppConfig
|
||||
|
||||
func Init(options ...Option) *AppConfig {
|
||||
acf = &AppConfig{
|
||||
Language: "zh",
|
||||
ConfigType: "yaml",
|
||||
ConfigName: "config",
|
||||
ServerPort: "1091",
|
||||
LogPathKey: "log",
|
||||
DataPath: "data",
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
if opt != nil {
|
||||
err := opt(acf)
|
||||
if err != nil {
|
||||
color.Red("error %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if acf.ConfigPath == "" {
|
||||
workerDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
color.Red("Failed to get the configuration file directory: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
acf.ConfigPath = workerDir
|
||||
}
|
||||
|
||||
if CommitVersion == "" {
|
||||
acf.devMode = true
|
||||
acf.ConfigName = fmt.Sprintf("%s-dev", acf.ConfigName)
|
||||
}
|
||||
|
||||
// 设置环境变量的前缀
|
||||
// 自动将环境变量绑定到 Viper 配置中
|
||||
viper.SetEnvPrefix("")
|
||||
viper.AutomaticEnv()
|
||||
|
||||
//设置配置文件
|
||||
viper.SetConfigName(acf.ConfigName)
|
||||
viper.SetConfigType(acf.ConfigType)
|
||||
|
||||
viper.AddConfigPath(acf.ConfigPath)
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
if acf.RemoteProvider == nil {
|
||||
err = acf.WriteDefaultConfig()
|
||||
if err != nil {
|
||||
color.Red("Error gen default config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
color.Red("failed to read config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
color.Red("Remote configuration will be used: %s", err.Error())
|
||||
} else {
|
||||
acf.Configs = viper.AllSettings()
|
||||
}
|
||||
|
||||
if acf.LogPathKey == "" {
|
||||
color.Red("Please specify LogPathKey")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
isSetDevMode := viper.IsSet("devMode")
|
||||
if isSetDevMode {
|
||||
setDevModel := viper.GetBool("devMode")
|
||||
acf.devMode = setDevModel
|
||||
}
|
||||
|
||||
viper.Set("devMode", acf.devMode)
|
||||
if acf.RemoteProvider != nil {
|
||||
_ = viper.AddRemoteProvider(string(acf.RemoteProvider.Name), acf.RemoteProvider.Endpoint, acf.RemoteProvider.Path)
|
||||
viper.SetConfigType(acf.RemoteProvider.Type)
|
||||
|
||||
err := viper.ReadRemoteConfig()
|
||||
if err != nil {
|
||||
color.Red("Failed to read remote config")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go func() {
|
||||
t := time.NewTicker(time.Minute * 30)
|
||||
for range t.C {
|
||||
err2 := viper.WatchRemoteConfig()
|
||||
if err2 != nil {
|
||||
logger.SugarLog.Errorf("unable to read remote config: %v", err2)
|
||||
continue
|
||||
}
|
||||
|
||||
if acf.WatchHandler != nil {
|
||||
acf.WatchHandler()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//处理http服务端口信息
|
||||
if acf.HttpServerPortFindPath != "" {
|
||||
port := viper.GetString(acf.HttpServerPortFindPath)
|
||||
if port == "" {
|
||||
port = acf.ServerPort
|
||||
}
|
||||
|
||||
if strings.Contains(port, ":") {
|
||||
s := strings.Split(port, ":")
|
||||
port = s[len(s)-1]
|
||||
}
|
||||
|
||||
acf.ServerPort = port
|
||||
acf.Servername = append(acf.Servername, "is now running at http://0.0.0.0:"+port)
|
||||
}
|
||||
|
||||
//打印系统信息
|
||||
if acf.Servername != nil {
|
||||
AsciiLogo(acf.Servername...)
|
||||
}
|
||||
|
||||
if CommitVersion == "" {
|
||||
color.Green("dev mode -- use config %s", acf.ConfigName+"."+acf.ConfigType)
|
||||
}
|
||||
|
||||
var c config.Logger
|
||||
err = viper.UnmarshalKey(acf.LogPathKey, &c)
|
||||
if err != nil {
|
||||
color.Red("failed to init app log")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(c.LogPath) {
|
||||
c.LogPath = acf.GetDataPath(c.LogPath)
|
||||
}
|
||||
|
||||
//初始化日志库
|
||||
logger.Init(c)
|
||||
|
||||
//validate语言配置
|
||||
validate.InitTranslator(acf.Language)
|
||||
|
||||
//配置文件更新回调
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
logger.SugarLog.Infof("config file changed: %s", e.Name)
|
||||
if acf.WatchHandler != nil {
|
||||
acf.WatchHandler()
|
||||
}
|
||||
})
|
||||
|
||||
//监听配置
|
||||
viper.WatchConfig()
|
||||
return acf
|
||||
}
|
35
app_asciilogo.go
Normal file
35
app_asciilogo.go
Normal file
@ -0,0 +1,35 @@
|
||||
package aqi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var (
|
||||
Branch string
|
||||
Revision string
|
||||
BuildDate string
|
||||
CommitVersion string
|
||||
)
|
||||
|
||||
var asciiLogo = `
|
||||
%s Started on %s
|
||||
|
||||
██▀▄─██─▄▄▄─█▄─▄█ Branch : %s-%s
|
||||
██─▀─██─██▀─██─██ Commit : %s
|
||||
▀▄▄▀▄▄▀───▄▄▀▄▄▄▀ Build at : %s
|
||||
|
||||
`
|
||||
|
||||
func AsciiLogo(serverName ...string) {
|
||||
color.Cyan(asciiLogo,
|
||||
strings.TrimSpace(strings.Join(serverName, " ")),
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
Branch,
|
||||
Revision,
|
||||
CommitVersion,
|
||||
BuildDate,
|
||||
)
|
||||
}
|
40
app_config.go
Normal file
40
app_config.go
Normal file
@ -0,0 +1,40 @@
|
||||
package aqi
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/wonli/aqi/config"
|
||||
)
|
||||
|
||||
func (a *AppConfig) GetDataPath(dir string) string {
|
||||
return filepath.Join(a.ConfigPath, a.DataPath, dir)
|
||||
}
|
||||
|
||||
func (a *AppConfig) IsDevMode() bool {
|
||||
return a.devMode
|
||||
}
|
||||
|
||||
func (a *AppConfig) WriteDefaultConfig() error {
|
||||
workerDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err := config.GetDefaultConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := filepath.Join(workerDir, a.ConfigName+"."+a.ConfigType)
|
||||
err = os.WriteFile(filename, []byte(ctx), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Green("Configuration file has been created: " + filename)
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
34
app_server.go
Normal file
34
app_server.go
Normal file
@ -0,0 +1,34 @@
|
||||
package aqi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/wonli/aqi/ws"
|
||||
)
|
||||
|
||||
func (a *AppConfig) WithHttpServer(svr http.Handler) {
|
||||
a.HttpServer = svr
|
||||
}
|
||||
|
||||
func (a *AppConfig) Start() {
|
||||
if a.HttpServer == nil {
|
||||
color.Red("HttpServer not config")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if a.HttpServer != nil {
|
||||
server := ws.NewServer(a.HttpServer)
|
||||
server.SetDataPath(a.DataPath)
|
||||
server.SetIsDev(a.devMode)
|
||||
server.Init()
|
||||
}
|
||||
|
||||
err := http.ListenAndServe(":"+a.ServerPort, a.HttpServer)
|
||||
if err != nil {
|
||||
color.Red("Listener error: %s", err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
11
app_use.go
Normal file
11
app_use.go
Normal file
@ -0,0 +1,11 @@
|
||||
package aqi
|
||||
|
||||
type Aqi struct {
|
||||
AppConfig *AppConfig
|
||||
}
|
||||
|
||||
func Use() *Aqi {
|
||||
return &Aqi{
|
||||
AppConfig: acf,
|
||||
}
|
||||
}
|
50
config/config.go
Normal file
50
config/config.go
Normal file
@ -0,0 +1,50 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
//go:embed config.yaml
|
||||
var defaultConfig []byte
|
||||
|
||||
// DefaultConfigTpl store template data.
|
||||
type DefaultConfigTpl struct {
|
||||
JwtSecurity string
|
||||
}
|
||||
|
||||
func generateRandomHex(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func GetDefaultConfig() (string, error) {
|
||||
jwtSecurity, err := generateRandomHex(16)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config := DefaultConfigTpl{
|
||||
JwtSecurity: jwtSecurity,
|
||||
}
|
||||
|
||||
tmpl, err := template.New("config").Parse(string(defaultConfig))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var renderedConfig bytes.Buffer
|
||||
err = tmpl.Execute(&renderedConfig, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return renderedConfig.String(), nil
|
||||
}
|
36
config/config.yaml
Normal file
36
config/config.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
port: 2015
|
||||
devMode: true
|
||||
jwtSecurity: {{.JwtSecurity}}
|
||||
jwtLifetime: 30d
|
||||
log:
|
||||
logFile: error.log
|
||||
logPath: logs
|
||||
maxSize: 200
|
||||
maxBackups: 3
|
||||
maxAge: 30
|
||||
compress: true
|
||||
useCaller: true
|
||||
redis:
|
||||
store:
|
||||
addr: 127.0.0.1:6379
|
||||
username: ""
|
||||
pwd: ""
|
||||
db: 1
|
||||
minIdleConns: 10
|
||||
idleTimeout: 5m0s
|
||||
mysql:
|
||||
logic:
|
||||
host: 127.0.0.1
|
||||
port: 3306
|
||||
user: root
|
||||
password: 123456
|
||||
database: test
|
||||
prefix: t_
|
||||
idle: 10
|
||||
idleTime: 1h0m0s
|
||||
maxLifetime: 1h0m0s
|
||||
heartBeatTime: 30s
|
||||
LogLevel: 4
|
||||
active: 50
|
||||
maxOpen: 20
|
||||
enable: 1
|
11
config/config_dialog.go
Normal file
11
config/config_dialog.go
Normal file
@ -0,0 +1,11 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type Dialog struct {
|
||||
OpInterval time.Duration `yaml:"opInterval" json:"opInterval,omitempty"` // Interval for sending op messages
|
||||
IdleInterval time.Duration `yaml:"idleInterval" json:"idleInterval,omitempty"` // Interval for inserting system time in the session list
|
||||
SessionExpire time.Duration `yaml:"sessionExpire" json:"sessionExpire,omitempty"` // Session expiration duration
|
||||
GuardInterval time.Duration `yaml:"guardInterval" json:"guardInterval,omitempty"` // Scan interval duration
|
||||
AssignInterval time.Duration `yaml:"assignInterval" json:"assignInterval,omitempty"` // Assignment interval time
|
||||
}
|
49
config/config_logger.go
Normal file
49
config/config_logger.go
Normal file
@ -0,0 +1,49 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
LogFile string
|
||||
LogPath string `yaml:"logPath"` // Path of the log file
|
||||
MaxSize int `yaml:"maxSize"` // Maximum log file size in MB
|
||||
MaxBackups int `yaml:"maxBackups"` // Maximum number of log file backups
|
||||
MaxAge int `yaml:"maxAge"` // Maximum number of days to retain log files
|
||||
Compress bool `yaml:"compress"` // Whether to enable gzip compression
|
||||
UseCaller bool `yaml:"useCaller"` // Whether to enable Zap Caller
|
||||
}
|
||||
|
||||
// GetEncoder 根据模式获取编码器
|
||||
func (config *Logger) GetEncoder(mode string) zapcore.Encoder {
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.FullCallerEncoder,
|
||||
}
|
||||
|
||||
if config.UseCaller {
|
||||
encoderConfig.CallerKey = "caller"
|
||||
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
|
||||
}
|
||||
|
||||
encoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
if mode == "file" {
|
||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
||||
}
|
||||
|
||||
//控制台模式下显示颜色
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
||||
}
|
35
config/config_mysql.go
Normal file
35
config/config_mysql.go
Normal file
@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MySQL struct {
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port int `yaml:"port" json:"port"`
|
||||
User string `yaml:"user" json:"user"`
|
||||
Password string `yaml:"password" json:"password"`
|
||||
Database string `yaml:"database" json:"database"`
|
||||
Prefix string `yaml:"prefix" json:"prefix"`
|
||||
LogLevel int `yaml:"logLevel"`
|
||||
Idle int `yaml:"idle" json:"idle"`
|
||||
IdleTime time.Duration `yaml:"idleTime" json:"idleTime,omitempty"`
|
||||
MaxLifetime time.Duration `yaml:"maxLifetime" json:"maxLifetime,omitempty"` // Maximum time a connection can be reused
|
||||
HeartBeatTime time.Duration `yaml:"heartBeatTime" json:"heartBeatTime,omitempty"` // Heartbeat check time for MySQL server connections
|
||||
Active int `yaml:"active" json:"active"` // Active connections
|
||||
MaxOpen int `yaml:"maxOpen" json:"maxOpen"` // Maximum open connections
|
||||
Enable int `yaml:"enable" json:"enable"` // 0, disabled; 1, enabled
|
||||
|
||||
AutoMigrateTables bool // Whether to synchronize table structures
|
||||
}
|
||||
|
||||
func (dbc *MySQL) GetDsn() string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
dbc.User,
|
||||
dbc.Password,
|
||||
dbc.Host,
|
||||
dbc.Port,
|
||||
dbc.Database,
|
||||
)
|
||||
}
|
15
config/config_redis.go
Normal file
15
config/config_redis.go
Normal file
@ -0,0 +1,15 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Redis struct {
|
||||
Addr string `yaml:"addr" json:"addr"`
|
||||
Username string `yaml:"username" json:"username"`
|
||||
Pwd string `yaml:"pwd" json:"pwd"`
|
||||
Db int `yaml:"db" json:"db"`
|
||||
LogLevel int `yaml:"logLevel"`
|
||||
MinIdleConns int `yaml:"minIdleConns" json:"minIdleConns"` // Minimum number of idle connections, useful when establishing new connections is slow.
|
||||
IdleTimeout time.Duration `yaml:"idleTimeout" json:"idleTimeout,omitempty"` // Time after which idle connections are closed by the client, default is 5 minutes, -1 disables the setting.
|
||||
}
|
14
config/config_sqlite.go
Normal file
14
config/config_sqlite.go
Normal file
@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Sqlite struct {
|
||||
Database string `yaml:"database"` // Path to the database file
|
||||
Prefix string `yaml:"prefix"` // Table prefix
|
||||
MaxIdleConns int `yaml:"maxIdleConns"` // Maximum number of idle connections in the pool
|
||||
MaxOpenConns int `yaml:"maxOpenConns"` // Maximum number of open connections to the database
|
||||
LogLevel int `yaml:"logLevel"` // Log level
|
||||
ConnMaxLifetime time.Duration `yaml:"connMaxLifetime"` // Maximum lifetime of connections
|
||||
}
|
35
config/config_sqlserver.go
Normal file
35
config/config_sqlserver.go
Normal file
@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SqlServer struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
Pwd string `yaml:"pwd"`
|
||||
Database string `yaml:"database"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Encrypt string `yaml:"encrypt"`
|
||||
LogLevel int `yaml:"logLevel"`
|
||||
TrustServerCertificate string `yaml:"trustServerCertificate"`
|
||||
Idle int `yaml:"idle"`
|
||||
IdleTime time.Duration `yaml:"idleTime"`
|
||||
MaxLifetime time.Duration `yaml:"maxLifetime"` // Maximum time a connection can be reused
|
||||
MaxOpen int `yaml:"maxOpen"`
|
||||
}
|
||||
|
||||
func (m *SqlServer) GetDsn() string {
|
||||
return fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s&encrypt=%s&trustServerCertificate=%s",
|
||||
m.User,
|
||||
url.QueryEscape(m.Pwd),
|
||||
m.Host,
|
||||
m.Port,
|
||||
m.Database,
|
||||
m.Encrypt,
|
||||
m.TrustServerCertificate,
|
||||
)
|
||||
}
|
BIN
docs/assets/img.png
Normal file
BIN
docs/assets/img.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 241 KiB |
212
docs/zh-CN.md
Normal file
212
docs/zh-CN.md
Normal file
@ -0,0 +1,212 @@
|
||||
# Aqi
|
||||
|
||||
Aqi是一个Golang Websocket业务框架,支持`net/http`, `gin`, `chi`等,底层整合了`viper`, `gorm`, `gobwa/ws`, `gjson`, `zap`, `asynq`等优秀第三方库,方便快速展开Websocket业务
|
||||
|
||||
### 安装
|
||||
|
||||
`go get -u github.com/wonli/aqi`
|
||||
|
||||
|
||||
|
||||
### 使用
|
||||
|
||||
第一次运行时会在工作目录下自动生成`config-dev.yaml`配置文件,你可以配置程序启动端口、数据库等信息。
|
||||
|
||||
|
||||
|
||||
服务启动后使用 [wscat](https://github.com/websockets/wscat) 与服务器建立websocket链接,运行截图如下。
|
||||
|
||||
|
||||
|
||||
![img](./assets/img.png)
|
||||
|
||||
|
||||
|
||||
### 交互协议
|
||||
|
||||
输入输出统一使用`JSON`,其中 `Action`为路由中注册的名字, `Params`为JSON格式字符串
|
||||
|
||||
```go
|
||||
|
||||
type Context struct {
|
||||
...
|
||||
Action string
|
||||
Params string
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
响应内容格式:
|
||||
|
||||
```go
|
||||
type Action struct {
|
||||
Action string `json:"action"`
|
||||
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 快速开始
|
||||
|
||||
在`ahi.Init`中通过`aqi.ConfigFile`指定配置文件,默认使用`yaml`格式,通过`aqi.HttpServer`指定服务名称和端口在`yaml`文件中的路径, 入口文件内容如下:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/wonli/aqi"
|
||||
"github.com/wonli/aqi/ws"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := aqi.Init(
|
||||
aqi.ConfigFile("config.yaml"),
|
||||
aqi.HttpServer("Aqi", "port"),
|
||||
)
|
||||
|
||||
// 创建路由
|
||||
mux := http.NewServeMux()
|
||||
// WebSocket Handler
|
||||
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
ws.HttpHandler(w, r)
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
wsr := ws.NewRouter()
|
||||
wsr.Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
|
||||
app.WithHttpServer(mux)
|
||||
|
||||
// 启动应用
|
||||
app.Start()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 与Gin整合
|
||||
|
||||
`aqi`能非常方便的与其他WEB框架整合,只需要正确注册`handler`和`app.WithHttpServer`,只要实现了 `http.Handler`都支持。
|
||||
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/wonli/aqi"
|
||||
"github.com/wonli/aqi/ws"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := aqi.Init(
|
||||
aqi.ConfigFile("config.yaml"),
|
||||
aqi.HttpServer("Aqi", "port"),
|
||||
)
|
||||
|
||||
engine := gin.Default()
|
||||
// 注册handler
|
||||
engine.GET("/ws", func(c *gin.Context) {
|
||||
ws.HttpHandler(c.Writer, c.Request)
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
wsr := ws.NewRouter()
|
||||
wsr.Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
|
||||
app.WithHttpServer(engine)
|
||||
app.Start()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 中间件
|
||||
|
||||
先定义一个简单的日志中间件,在处理请求前先打印当前接收到的`action`,在处理完成后打印响应内容。
|
||||
|
||||
```go
|
||||
func logMiddleware() func(a *ws.Context) {
|
||||
return func(a *ws.Context) {
|
||||
log.Printf("Request action: %s ", a.Action)
|
||||
a.Next()
|
||||
log.Printf("Reqponse data: %s ", a.Response.Data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用`Use`方法将中间件注册到路由中
|
||||
|
||||
```go
|
||||
// 注册WebSocket路由
|
||||
wsr := ws.NewRouter()
|
||||
wsr.Use(logMiddleware()).Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
当然也可以用路由组的形式
|
||||
|
||||
```go
|
||||
// 注册WebSocket路由
|
||||
wsr := ws.NewRouter()
|
||||
r1 := wsr.Use(logMiddleware())
|
||||
{
|
||||
r1.Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
|
||||
r1.Add("say", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"say": "hi",
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
这样控制台在每个请求前后都会打印日志
|
||||
|
||||
|
||||
|
||||
### 生产模式
|
||||
|
||||
直接编译`Aqi`会以`dev`模式运行,以生产模式运行请在编译时传入以下参数,详细内容请查看`examples/Makefile`文件
|
||||
|
||||
|
||||
|
||||
```shell
|
||||
LDFLAGS = "-X '$(FLAGS_PKG).BuildDate=$(BUILD_DATE)' \
|
||||
-X '$(FLAGS_PKG).Branch=$(GIT_BRANCH)' \
|
||||
-X '$(FLAGS_PKG).CommitVersion=$(GIT_COMMIT)' \
|
||||
-X '$(FLAGS_PKG).Revision=$(GIT_REVISION)' \
|
||||
-extldflags '-static -s -w'"
|
||||
```
|
||||
|
||||
|
||||
|
46
examples/Makefile
Normal file
46
examples/Makefile
Normal file
@ -0,0 +1,46 @@
|
||||
APP_NAME = aqi
|
||||
APP_PATH = ./
|
||||
|
||||
# build dist
|
||||
BUILD_PATH := ./dist
|
||||
|
||||
# build at
|
||||
BUILD_DATE = $(shell date +'%F %T')
|
||||
|
||||
# git versions
|
||||
GIT_BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
|
||||
GIT_COMMIT = $(shell git rev-list --count HEAD)
|
||||
GIT_REVISION = $(shell git rev-parse --short HEAD)
|
||||
GIT_COMMITAT = $(shell git --no-pager log -1 --format="%at")
|
||||
|
||||
# flags
|
||||
FLAGS_PKG = github.com/wonli/aqi
|
||||
LDFLAGS = "-X '$(FLAGS_PKG).BuildDate=$(BUILD_DATE)' \
|
||||
-X '$(FLAGS_PKG).Branch=$(GIT_BRANCH)' \
|
||||
-X '$(FLAGS_PKG).CommitVersion=$(GIT_COMMIT)' \
|
||||
-X '$(FLAGS_PKG).Revision=$(GIT_REVISION)' \
|
||||
-extldflags '-static -s -w'"
|
||||
|
||||
# params
|
||||
GO_FLAGS = -ldflags $(LDFLAGS) -trimpath -tags netgo
|
||||
|
||||
# Go build fn 1-GOOS 2-GOARCH 3-FILE EXT
|
||||
define go/build
|
||||
GOOS=$(1) GOARCH=$(2) CGO_ENABLED=0 go build $(GO_FLAGS) -o $(BUILD_PATH)/$(APP_NAME)-$(1)-$(2)-latest$(3) ${APP_PATH}
|
||||
endef
|
||||
|
||||
# PHONY
|
||||
.PHONY: darwin linux windows
|
||||
|
||||
darwin:
|
||||
$(call go/build,darwin,amd64)
|
||||
$(call go/build,darwin,arm64)
|
||||
|
||||
linux:
|
||||
$(call go/build,linux,amd64)
|
||||
|
||||
windows:
|
||||
$(call go/build,windows,amd64,.exe)
|
||||
|
||||
linux64:
|
||||
$(call go/build,linux,amd64)
|
39
examples/main.go
Normal file
39
examples/main.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/wonli/aqi"
|
||||
"github.com/wonli/aqi/ws"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := aqi.Init(
|
||||
aqi.ConfigFile("config.yaml"),
|
||||
aqi.HttpServer("Aqi", "port"),
|
||||
)
|
||||
|
||||
engine := gin.Default()
|
||||
engine.GET("/", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "Hi aqi!")
|
||||
})
|
||||
|
||||
// Websocket
|
||||
engine.GET("/ws", func(c *gin.Context) {
|
||||
ws.HttpHandler(c.Writer, c.Request)
|
||||
})
|
||||
|
||||
// Router
|
||||
wsr := ws.NewRouter()
|
||||
wsr.Add("hi", func(a *ws.Context) {
|
||||
a.Send(ws.H{
|
||||
"hi": time.Now(),
|
||||
})
|
||||
})
|
||||
|
||||
app.WithHttpServer(engine)
|
||||
app.Start()
|
||||
}
|
111
go.mod
Normal file
111
go.mod
Normal file
@ -0,0 +1,111 @@
|
||||
module github.com/wonli/aqi
|
||||
|
||||
go 1.22.1
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6
|
||||
github.com/alibabacloud-go/darabonba-stream v0.0.1
|
||||
github.com/alibabacloud-go/tea v1.2.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/guonaihong/gout v0.3.9
|
||||
github.com/hibiken/asynq v0.24.1
|
||||
github.com/importcjj/sensitive v0.0.0-20200106142752-42d1c505be7b
|
||||
github.com/mozillazg/go-pinyin v0.20.0
|
||||
github.com/redis/go-redis/v9 v9.0.3
|
||||
github.com/shirou/gopsutil/v3 v3.24.4
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/datatypes v1.2.0
|
||||
gorm.io/driver/mysql v1.5.6
|
||||
gorm.io/driver/sqlite v1.4.3
|
||||
gorm.io/driver/sqlserver v1.4.1
|
||||
gorm.io/gorm v1.25.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/credentials-go v1.3.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/microsoft/go-mssqldb v0.17.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // 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/v2 v2.2.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.3.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
417
go.sum
Normal file
417
go.sum
Normal file
@ -0,0 +1,417 @@
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6 h1:y1K+zKhpWcxso8zqI03CcYuwgyZPFwQdwAQOXAeuOVM=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.6/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI=
|
||||
github.com/alibabacloud-go/darabonba-stream v0.0.1 h1:e80xWsu1QkwHGKkLxHHH3QNP6GLZs3InOkN7UR2Fu6w=
|
||||
github.com/alibabacloud-go/darabonba-stream v0.0.1/go.mod h1:/RbIC3XJDnXMCneEbwnqc+CNyOOs36tJAJJ3z2kpVdc=
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||
github.com/alibabacloud-go/debug v1.0.0 h1:3eIEQWfay1fB24PQIEzXAswlVJtdQok8f3EVN5VrBnA=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
|
||||
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5 h1:EUakYEUAwr6L3wLT0vejIw2rc0IA1RSXDwLnIb3f2vU=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1 h1:uq/0v7kWrxmoLGpqjx7vtQ/s03f0zR//0br/xWDTE28=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
|
||||
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/guonaihong/gout v0.3.9 h1:bWaoD9mgAQwzIZhau+QOsLOxgRSZRL+wAIYVqqkwPEw=
|
||||
github.com/guonaihong/gout v0.3.9/go.mod h1:wDXeuyeZR6MtaHbytO9RLcKW4iCDrWD6/KF1QwDtbRc=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
|
||||
github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
|
||||
github.com/importcjj/sensitive v0.0.0-20200106142752-42d1c505be7b h1:9hudrgWUhyfR4FRMOfL9KB1uYw48DUdHkkgr9ODOw7Y=
|
||||
github.com/importcjj/sensitive v0.0.0-20200106142752-42d1c505be7b/go.mod h1:zLVdX6Ed2SvCbEamKmve16U0E03UkdJo4ls1TBfmc8Q=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA=
|
||||
github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
|
||||
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ=
|
||||
github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
|
||||
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco=
|
||||
gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04=
|
||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
|
||||
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
|
||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
|
||||
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
|
||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
59
logger/zap.go
Normal file
59
logger/zap.go
Normal file
@ -0,0 +1,59 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/wonli/aqi/config"
|
||||
)
|
||||
|
||||
var ZapLog *zap.Logger
|
||||
var SugarLog *zap.SugaredLogger
|
||||
|
||||
func Init(c config.Logger) {
|
||||
if c.LogPath == "" {
|
||||
c.LogPath = "."
|
||||
}
|
||||
|
||||
if c.LogFile == "" {
|
||||
c.LogFile = "error.log"
|
||||
}
|
||||
|
||||
isAbsPath := filepath.IsAbs(c.LogPath)
|
||||
if !isAbsPath {
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
color.Red("Failed to get the runtime directory %s", err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
c.LogPath = filepath.Join(path, c.LogPath)
|
||||
err = os.MkdirAll(c.LogPath, 0755)
|
||||
if err != nil {
|
||||
color.Red("Failed to create log directory %s", err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
hook := lumberjack.Logger{
|
||||
Filename: filepath.Join(c.LogPath, c.LogFile),
|
||||
MaxSize: c.MaxSize,
|
||||
MaxBackups: c.MaxBackups,
|
||||
MaxAge: c.MaxAge,
|
||||
Compress: c.Compress,
|
||||
}
|
||||
|
||||
stdLog := zapcore.NewCore(c.GetEncoder(""), zapcore.AddSync(os.Stdout), zap.DebugLevel)
|
||||
fileLog := zapcore.NewCore(c.GetEncoder("file"), zapcore.AddSync(&hook), zap.DebugLevel)
|
||||
|
||||
//ZapLog
|
||||
ZapLog = zap.New(zapcore.NewTee(stdLog, fileLog), zap.AddCaller(), zap.Development())
|
||||
|
||||
//sugar
|
||||
SugarLog = ZapLog.Sugar()
|
||||
}
|
84
options.go
Normal file
84
options.go
Normal file
@ -0,0 +1,84 @@
|
||||
package aqi
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Option func(config *AppConfig) error
|
||||
|
||||
func LogConfig(configKeyPath string) Option {
|
||||
return func(config *AppConfig) error {
|
||||
config.LogPathKey = configKeyPath
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DataPath(path string) Option {
|
||||
return func(config *AppConfig) error {
|
||||
config.DataPath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func devMode(isDev bool) Option {
|
||||
return func(config *AppConfig) error {
|
||||
config.devMode = isDev
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigFile(file string) Option {
|
||||
if !filepath.IsAbs(file) {
|
||||
workerDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("获取工作目录失败: %s", err.Error())
|
||||
}
|
||||
|
||||
file = filepath.Join(workerDir, file)
|
||||
}
|
||||
|
||||
return func(config *AppConfig) error {
|
||||
configPath := filepath.Dir(file)
|
||||
config.ConfigPath = configPath
|
||||
|
||||
fileType := filepath.Ext(file)
|
||||
config.ConfigType = fileType[1:]
|
||||
|
||||
filename := filepath.Base(file)
|
||||
config.ConfigName = strings.TrimSuffix(filename, fileType)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func Server(name ...string) Option {
|
||||
return func(config *AppConfig) error {
|
||||
config.Servername = name
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func Language(lng string) Option {
|
||||
return func(config *AppConfig) error {
|
||||
config.Language = lng
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func HttpServer(name, portFindPath string) Option {
|
||||
return func(config *AppConfig) error {
|
||||
config.Servername = append(config.Servername, name)
|
||||
config.HttpServerPortFindPath = portFindPath
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WatchHandler(handler func()) Option {
|
||||
return func(config *AppConfig) error {
|
||||
config.WatchHandler = handler
|
||||
return nil
|
||||
}
|
||||
}
|
66
remote_provider.go
Normal file
66
remote_provider.go
Normal file
@ -0,0 +1,66 @@
|
||||
package aqi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
type Provider string
|
||||
|
||||
const ProviderConsul Provider = "consul"
|
||||
const ProviderEtcd Provider = "etcd"
|
||||
|
||||
type RemoteProvider struct {
|
||||
Name Provider //服务商名称
|
||||
Path string //路径
|
||||
Endpoint string //服务器地址
|
||||
Type string //json, yaml等
|
||||
}
|
||||
|
||||
// ParseRemoteProvider 格式 provider[s]://endpoint/path.type
|
||||
// 例:consul://localhost:8500/a.yaml
|
||||
// 表示远程配置中心为consul,服务器地址为http://localhost:8500, path为a, 配置类型是yaml
|
||||
// scheme加s表示服务器支持ssl
|
||||
func ParseRemoteProvider(s string) *RemoteProvider {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
logger.SugarLog.Errorf("Parse remote provider endpoint error: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if u.Scheme == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := strings.TrimLeft(u.Path, "/")
|
||||
fileType := filepath.Ext(path)
|
||||
if fileType == "" {
|
||||
fileType = "yaml"
|
||||
} else {
|
||||
path = path[:len(path)-len(fileType)]
|
||||
fileType = fileType[1:]
|
||||
}
|
||||
|
||||
p := &RemoteProvider{
|
||||
Path: path,
|
||||
Type: fileType,
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "consul", "consuls":
|
||||
p.Name = ProviderConsul
|
||||
case "etcd", "etcds":
|
||||
p.Name = ProviderEtcd
|
||||
}
|
||||
|
||||
if strings.HasSuffix(u.Scheme, "s") {
|
||||
p.Endpoint = "https://" + u.Host
|
||||
} else {
|
||||
p.Endpoint = "http://" + u.Host
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
231
stats/stats.go
Normal file
231
stats/stats.go
Normal file
@ -0,0 +1,231 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/load"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
snet "github.com/shirou/gopsutil/v3/net"
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
|
||||
"github.com/wonli/aqi/ws"
|
||||
)
|
||||
|
||||
type NetStats struct {
|
||||
LastSent uint64
|
||||
LastRecv uint64
|
||||
LastTime time.Time
|
||||
}
|
||||
|
||||
// Stats contains statistical data on CPU and memory usage
|
||||
type Stats struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
SvrCPUUsage float64 `json:"svrCPUUsage"` // Overall CPU usage rate
|
||||
SvrMemoryPct float64 `json:"svrMemoryPct"` // Total memory
|
||||
LoadAverage [2]float64 `json:"loadAverage"` // 1, 5 minutes average load
|
||||
|
||||
CPUUsage float64 `json:"CPUUsage"` // Current process's CPU usage rate
|
||||
MemoryUsage float64 `json:"memoryUsage"` // Current process's memory usage
|
||||
MemoryUsagePct float64 `json.json:"memoryUsagePct"` // Current process's memory usage percentage
|
||||
ThreadCount int `json:"threadCount"` // Current process's thread count
|
||||
Goroutines int `json:"goroutines"` // Number of Go coroutines
|
||||
HeapAlloc float64 `json:"heapAlloc"` // Memory allocated in the heap currently
|
||||
HeapSys float64 `json:"heapSys"` // Total heap memory obtained from the system
|
||||
HeapInuse float64 `json:"heapInuse"` // Heap memory in use
|
||||
HeapPct float64 `json:"heapPct"` // Percentage of heap memory obtained from the system
|
||||
|
||||
LoginCount int `json:"loginCount"` // Online users
|
||||
GuestCount int `json.json:"guestCount"` // Visitors
|
||||
Connections int `json:"connections"` // Current process's network connections
|
||||
|
||||
SentRate float64 `json:"sentRate"` // Sending rate KB/s
|
||||
RecvRate float64 `json:"recvRate"` // Receiving rate KB/s
|
||||
|
||||
MaxMemoryUsage float64 `json:"maxMemoryUsage"` // Maximum memory
|
||||
MaxGoroutines int `json:"maxGoroutines"` // Maximum coroutines
|
||||
}
|
||||
|
||||
// Collector manages the collection and storage of statistical data
|
||||
type Collector struct {
|
||||
mu sync.Mutex // mu
|
||||
stats []Stats // Slice for storing statistical data
|
||||
netStats NetStats // transmission rate
|
||||
|
||||
capacity int // Maximum capacity of the slice
|
||||
interval2 time.Duration
|
||||
}
|
||||
|
||||
var capacity = 30
|
||||
var once sync.Once
|
||||
var collectorInstance *Collector
|
||||
|
||||
func InitStatsCollector() *Collector {
|
||||
once.Do(func() {
|
||||
collectorInstance = &Collector{
|
||||
stats: make([]Stats, 0, capacity),
|
||||
capacity: capacity,
|
||||
}
|
||||
})
|
||||
|
||||
return collectorInstance
|
||||
}
|
||||
|
||||
// Collect starts collecting statistical data
|
||||
func (sc *Collector) Collect(interval time.Duration) {
|
||||
go func() {
|
||||
sc.doCollect(100 * time.Millisecond)
|
||||
for {
|
||||
sc.doCollect(0)
|
||||
time.Sleep(interval)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (sc *Collector) doCollect(interval time.Duration) {
|
||||
currentStats := Stats{}
|
||||
|
||||
if ws.Hub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// User data
|
||||
currentStats.LoginCount = ws.Hub.LoginCount
|
||||
currentStats.GuestCount = ws.Hub.GuestCount
|
||||
|
||||
// Get CPU usage rate
|
||||
cpuPercentages, err := cpu.Percent(interval, false)
|
||||
if err == nil && len(cpuPercentages) > 0 {
|
||||
currentStats.SvrCPUUsage = cpuPercentages[0]
|
||||
}
|
||||
|
||||
// Get average load
|
||||
avgLoad, err := load.Avg()
|
||||
if err == nil {
|
||||
currentStats.LoadAverage = [2]float64{avgLoad.Load1, avgLoad.Load5}
|
||||
}
|
||||
|
||||
// Get network interface statistics
|
||||
netIOCounters, err := snet.IOCounters(true)
|
||||
if err == nil {
|
||||
var totalBytesSent, totalBytesRecv uint64
|
||||
for _, counter := range netIOCounters {
|
||||
totalBytesSent += counter.BytesSent
|
||||
totalBytesRecv += counter.BytesRecv
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
if !sc.netStats.LastTime.IsZero() {
|
||||
// Calculate time difference (seconds)
|
||||
duration := currentTime.Sub(sc.netStats.LastTime).Seconds()
|
||||
|
||||
// Calculate byte differences and convert to KB
|
||||
sentDiff := float64(totalBytesSent-sc.netStats.LastSent) / 1024.0
|
||||
recvDiff := float64(totalBytesRecv-sc.netStats.LastRecv) / 1024.0
|
||||
|
||||
// Calculate rate per second
|
||||
currentStats.SentRate = sentDiff / duration
|
||||
currentStats.RecvRate = recvDiff / duration
|
||||
}
|
||||
|
||||
// Update the last statistics
|
||||
sc.netStats.LastSent = totalBytesSent
|
||||
sc.netStats.LastRecv = totalBytesRecv
|
||||
sc.netStats.LastTime = currentTime
|
||||
}
|
||||
|
||||
// Get memory statistics
|
||||
vmem, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
currentStats.SvrMemoryPct = vmem.UsedPercent
|
||||
}
|
||||
|
||||
// Get current process ID and process object
|
||||
pid := os.Getpid()
|
||||
proc, err := process.NewProcess(int32(pid))
|
||||
if err == nil {
|
||||
// Get current process's CPU usage rate
|
||||
procPercent, err := proc.Percent(interval)
|
||||
if err == nil {
|
||||
currentStats.CPUUsage = procPercent
|
||||
}
|
||||
|
||||
// Get current process's memory statistics
|
||||
memInfo, err := proc.MemoryInfo()
|
||||
if err == nil && memInfo != nil {
|
||||
currentStats.MemoryUsage = formatMegabytes(bytesToMegabytes(memInfo.RSS)) // RSS is Resident Set Size
|
||||
vmem, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
currentStats.MemoryUsagePct = float64(memInfo.RSS) / float64(vmem.Total) * 100
|
||||
}
|
||||
|
||||
if currentStats.MemoryUsage > currentStats.MaxMemoryUsage {
|
||||
currentStats.MaxMemoryUsage = currentStats.MemoryUsage
|
||||
}
|
||||
}
|
||||
|
||||
// Get current process's thread count
|
||||
threads, err := proc.NumThreads()
|
||||
if err == nil {
|
||||
currentStats.ThreadCount = int(threads)
|
||||
}
|
||||
}
|
||||
|
||||
// Get current process's network connection information
|
||||
connections, err := snet.ConnectionsPid("all", int32(pid))
|
||||
if err == nil {
|
||||
currentStats.Connections = len(connections)
|
||||
}
|
||||
|
||||
// Get Go runtime memory statistics
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
currentStats.Goroutines = runtime.NumGoroutine()
|
||||
currentStats.HeapAlloc = formatMegabytes(bytesToMegabytes(memStats.HeapAlloc))
|
||||
currentStats.HeapSys = formatMegabytes(bytesToMegabytes(memStats.HeapSys))
|
||||
currentStats.HeapInuse = formatMegabytes(bytesToMegabytes(memStats.HeapInuse))
|
||||
if currentStats.Goroutines > currentStats.MaxGoroutines {
|
||||
currentStats.MaxGoroutines = currentStats.Goroutines
|
||||
}
|
||||
|
||||
// Get heap memory usage
|
||||
if currentStats.HeapSys > 0 {
|
||||
currentStats.HeapPct = currentStats.HeapInuse / currentStats.HeapSys * 100
|
||||
}
|
||||
|
||||
// Timestamp when statistics are completed
|
||||
currentStats.Timestamp = time.Now()
|
||||
|
||||
// Lock and update statistical data
|
||||
sc.mu.Lock()
|
||||
sc.stats = append(sc.stats, currentStats)
|
||||
if len(sc.stats) > sc.capacity {
|
||||
sc.stats = sc.stats[1:]
|
||||
}
|
||||
sc.mu.Unlock()
|
||||
|
||||
// Publish data
|
||||
ws.Hub.PubSub.Pub("sys:status", currentStats)
|
||||
}
|
||||
|
||||
// GetStats returns all the collected statistical data
|
||||
func (sc *Collector) GetStats() []Stats {
|
||||
sc.mu.Lock()
|
||||
defer sc.mu.Unlock()
|
||||
// Return a copy of the slice to avoid external modification
|
||||
statsCopy := make([]Stats, len(sc.stats))
|
||||
copy(statsCopy, sc.stats)
|
||||
return statsCopy
|
||||
}
|
||||
|
||||
func bytesToMegabytes(bytes uint64) float64 {
|
||||
return float64(bytes) / 1024.0 / 1024.0
|
||||
}
|
||||
|
||||
func formatMegabytes(mb float64) float64 {
|
||||
return math.Round(mb*100) / 100
|
||||
}
|
17
store/use.go
Normal file
17
store/use.go
Normal file
@ -0,0 +1,17 @@
|
||||
package store
|
||||
|
||||
func DB(configKey string) *MySQLStore {
|
||||
return &MySQLStore{configKey: configKey}
|
||||
}
|
||||
|
||||
func SQLite(configKey string) *SQLiteStore {
|
||||
return &SQLiteStore{configKey: configKey}
|
||||
}
|
||||
|
||||
func Redis(configKey string) *RedisStore {
|
||||
return &RedisStore{configKey: configKey}
|
||||
}
|
||||
|
||||
func SqlServer(configKey string) *SqlServerStore {
|
||||
return &SqlServerStore{configKey: configKey}
|
||||
}
|
65
store/use_mysql.go
Normal file
65
store/use_mysql.go
Normal file
@ -0,0 +1,65 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
gormLogger "gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/wonli/aqi/config"
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
type MySQLStore struct {
|
||||
configKey string
|
||||
}
|
||||
|
||||
func (m *MySQLStore) Config() *config.MySQL {
|
||||
var r *config.MySQL
|
||||
err := viper.UnmarshalKey(m.configKey, &r)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (m *MySQLStore) Use() *gorm.DB {
|
||||
r := m.Config()
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.Enable == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
conf := &gorm.Config{
|
||||
Logger: gormLogger.Default.LogMode(gormLogger.LogLevel(r.LogLevel)),
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: r.Prefix,
|
||||
},
|
||||
}
|
||||
|
||||
db, err := gorm.Open(mysql.Open(r.GetDsn()), conf)
|
||||
if err != nil {
|
||||
logger.SugarLog.Error("Failed to connect to MySQL database", zap.String("error", err.Error()))
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
logger.SugarLog.Error("Error pinging database", zap.String("error", err.Error()))
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlDB.SetMaxIdleConns(r.Idle)
|
||||
sqlDB.SetConnMaxLifetime(r.MaxLifetime)
|
||||
if r.MaxOpen > 0 {
|
||||
sqlDB.SetMaxOpenConns(r.MaxOpen)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
40
store/use_redis.go
Normal file
40
store/use_redis.go
Normal file
@ -0,0 +1,40 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wonli/aqi/config"
|
||||
)
|
||||
|
||||
type RedisStore struct {
|
||||
configKey string
|
||||
}
|
||||
|
||||
func (s *RedisStore) Config() *config.Redis {
|
||||
var r *config.Redis
|
||||
err := viper.UnmarshalKey(s.configKey, &r)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (s *RedisStore) Use() *redis.Client {
|
||||
r := s.Config()
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: r.Addr,
|
||||
Username: r.Username,
|
||||
Password: r.Pwd,
|
||||
DB: r.Db,
|
||||
MinIdleConns: r.MinIdleConns,
|
||||
ConnMaxIdleTime: r.IdleTimeout,
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
64
store/use_sqlite.go
Normal file
64
store/use_sqlite.go
Normal file
@ -0,0 +1,64 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
gormLogger "gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/wonli/aqi/config"
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
type SQLiteStore struct {
|
||||
configKey string
|
||||
}
|
||||
|
||||
func (m *SQLiteStore) Config() *config.Sqlite {
|
||||
var r *config.Sqlite
|
||||
err := viper.UnmarshalKey(m.configKey, &r)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (m *SQLiteStore) Use() *gorm.DB {
|
||||
r := m.Config()
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
conf := &gorm.Config{
|
||||
Logger: gormLogger.Default.LogMode(gormLogger.LogLevel(r.LogLevel)),
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: r.Prefix,
|
||||
},
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(r.Database), conf)
|
||||
if err != nil {
|
||||
logger.SugarLog.Error("Connect to SQLite error", zap.String("error", err.Error()))
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
logger.SugarLog.Error("Ping SQLite error",
|
||||
zap.String("error", err.Error()),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
sqlDB.SetMaxIdleConns(r.MaxIdleConns)
|
||||
sqlDB.SetConnMaxLifetime(r.ConnMaxLifetime)
|
||||
if r.MaxOpenConns > 0 {
|
||||
sqlDB.SetMaxOpenConns(r.MaxOpenConns)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
67
store/use_sqlserver.go
Normal file
67
store/use_sqlserver.go
Normal file
@ -0,0 +1,67 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
gormLogger "gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/wonli/aqi/config"
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
type SqlServerStore struct {
|
||||
configKey string
|
||||
}
|
||||
|
||||
func (m *SqlServerStore) Config() *config.SqlServer {
|
||||
var r *config.SqlServer
|
||||
err := viper.UnmarshalKey(m.configKey, &r)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (m *SqlServerStore) Use() *gorm.DB {
|
||||
r := m.Config()
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
conf := &gorm.Config{
|
||||
Logger: gormLogger.Default.LogMode(gormLogger.LogLevel(r.LogLevel)),
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: r.Prefix,
|
||||
},
|
||||
}
|
||||
|
||||
conf = &gorm.Config{}
|
||||
db, err := gorm.Open(sqlserver.Open(r.GetDsn()), conf)
|
||||
if err != nil {
|
||||
logger.SugarLog.Errorf("%s (gorm.open)", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
logger.SugarLog.Errorf("%s (ping)", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.Idle > 0 {
|
||||
sqlDB.SetMaxIdleConns(r.Idle)
|
||||
}
|
||||
|
||||
if r.MaxLifetime > 0 {
|
||||
sqlDB.SetConnMaxLifetime(r.MaxLifetime)
|
||||
}
|
||||
|
||||
if r.MaxOpen > 0 {
|
||||
sqlDB.SetMaxOpenConns(r.MaxOpen)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
139
utils/ali_ocr.go
Normal file
139
utils/ali_ocr.go
Normal file
@ -0,0 +1,139 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
stream "github.com/alibabacloud-go/darabonba-stream/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type AliOcr struct {
|
||||
uploadBody []byte
|
||||
apiParams *openapi.Params
|
||||
|
||||
client *openapi.Client
|
||||
initError error
|
||||
|
||||
response any
|
||||
apiResponse any
|
||||
|
||||
accessKey string
|
||||
secret string
|
||||
}
|
||||
|
||||
func NewAliOcr(accessKey, secret string) *AliOcr {
|
||||
instance := &AliOcr{
|
||||
accessKey: accessKey,
|
||||
secret: secret,
|
||||
}
|
||||
|
||||
//STS see:
|
||||
//https://help.aliyun.com/document_detail/378661.html
|
||||
client, err := instance.createClient(tea.String(accessKey), tea.String(secret))
|
||||
if err != nil {
|
||||
instance.initError = err
|
||||
}
|
||||
|
||||
instance.client = client
|
||||
return instance
|
||||
}
|
||||
|
||||
func (ali *AliOcr) Request() error {
|
||||
if ali.initError != nil {
|
||||
return ali.initError
|
||||
}
|
||||
|
||||
if ali.uploadBody == nil {
|
||||
return fmt.Errorf("获取上传内容失败")
|
||||
}
|
||||
|
||||
if ali.client == nil {
|
||||
return fmt.Errorf("初始化阿里客户端失败")
|
||||
}
|
||||
|
||||
if ali.apiParams == nil {
|
||||
return fmt.Errorf("请求参数不能为空")
|
||||
}
|
||||
|
||||
// runtime options
|
||||
runtime := &util.RuntimeOptions{}
|
||||
request := &openapi.OpenApiRequest{
|
||||
Stream: stream.ReadFromBytes(ali.uploadBody),
|
||||
}
|
||||
|
||||
// 复制代码运行请自行打印 API 的返回值
|
||||
// 返回值为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。
|
||||
res, err := ali.client.CallApi(ali.apiParams, request, runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ali.apiResponse = res
|
||||
apiJson, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statusCode := gjson.Get(string(apiJson), "statusCode").Int()
|
||||
if statusCode != 200 {
|
||||
return fmt.Errorf("返回状态码不正确")
|
||||
}
|
||||
|
||||
if ali.response != nil {
|
||||
bodyData := gjson.Get(string(apiJson), "body.Data").String()
|
||||
err = json.Unmarshal([]byte(bodyData), ali.response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ali *AliOcr) WithResponse(s any) {
|
||||
ali.response = s
|
||||
}
|
||||
|
||||
func (ali *AliOcr) WithApiName(apiName string) {
|
||||
//RecognizeDrivingLicense
|
||||
ali.apiParams = &openapi.Params{
|
||||
// 接口名称
|
||||
Action: tea.String(apiName),
|
||||
// 接口版本
|
||||
Version: tea.String("2021-07-07"),
|
||||
// 接口协议
|
||||
Protocol: tea.String("HTTPS"),
|
||||
// 接口 HTTP 方法
|
||||
Method: tea.String("POST"),
|
||||
AuthType: tea.String("AK"),
|
||||
Style: tea.String("V3"),
|
||||
// 接口 PATH
|
||||
Pathname: tea.String("/"),
|
||||
// 接口请求体内容格式
|
||||
ReqBodyType: tea.String("json"),
|
||||
// 接口响应体内容格式
|
||||
BodyType: tea.String("json"),
|
||||
}
|
||||
}
|
||||
|
||||
func (ali *AliOcr) WithBody(body []byte) {
|
||||
ali.uploadBody = body
|
||||
}
|
||||
|
||||
func (ali *AliOcr) createClient(accessKeyId *string, accessKeySecret *string) (res *openapi.Client, err error) {
|
||||
config := &openapi.Config{
|
||||
// 必填,您的 AccessKey ID
|
||||
AccessKeyId: accessKeyId,
|
||||
// 必填,您的 AccessKey Secret
|
||||
AccessKeySecret: accessKeySecret,
|
||||
}
|
||||
// 访问的域名
|
||||
config.Endpoint = tea.String("ocr-api.cn-hangzhou.aliyuncs.com")
|
||||
res = &openapi.Client{}
|
||||
res, err = openapi.NewClient(config)
|
||||
return res, err
|
||||
}
|
123
utils/bytefmt/bytefmt.go
Normal file
123
utils/bytefmt/bytefmt.go
Normal file
@ -0,0 +1,123 @@
|
||||
// Package bytefmt contains helper methods and constants for converting
|
||||
// to and from a human-readable byte format.
|
||||
//
|
||||
// bytefmt.ByteSize(100.5*bytefmt.MEGABYTE) // "100.5M"
|
||||
// bytefmt.ByteSize(uint64(1024)) // "1K"
|
||||
package bytefmt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
BYTE = 1 << (10 * iota)
|
||||
KILOBYTE
|
||||
MEGABYTE
|
||||
GIGABYTE
|
||||
TERABYTE
|
||||
PETABYTE
|
||||
EXABYTE
|
||||
)
|
||||
|
||||
var invalidByteQuantityError = errors.New("byte quantity must be a positive integer with a unit of measurement like M, MB, MiB, G, GiB, or GB")
|
||||
|
||||
// ByteSize returns a human-readable byte string of the form 10M, 12.5K, and so forth. The following units are available:
|
||||
//
|
||||
// E: Exabyte
|
||||
// P: Petabyte
|
||||
// T: Terabyte
|
||||
// G: Gigabyte
|
||||
// M: Megabyte
|
||||
// K: Kilobyte
|
||||
// B: Byte
|
||||
//
|
||||
// The unit that results in the smallest number greater than or equal to 1 is always chosen.
|
||||
func ByteSize(bytes uint64) string {
|
||||
unit := ""
|
||||
value := float64(bytes)
|
||||
|
||||
switch {
|
||||
case bytes >= EXABYTE:
|
||||
unit = "E"
|
||||
value = value / EXABYTE
|
||||
case bytes >= PETABYTE:
|
||||
unit = "P"
|
||||
value = value / PETABYTE
|
||||
case bytes >= TERABYTE:
|
||||
unit = "T"
|
||||
value = value / TERABYTE
|
||||
case bytes >= GIGABYTE:
|
||||
unit = "G"
|
||||
value = value / GIGABYTE
|
||||
case bytes >= MEGABYTE:
|
||||
unit = "M"
|
||||
value = value / MEGABYTE
|
||||
case bytes >= KILOBYTE:
|
||||
unit = "K"
|
||||
value = value / KILOBYTE
|
||||
case bytes >= BYTE:
|
||||
unit = "B"
|
||||
case bytes == 0:
|
||||
return "0B"
|
||||
}
|
||||
|
||||
result := strconv.FormatFloat(value, 'f', 1, 64)
|
||||
result = strings.TrimSuffix(result, ".0")
|
||||
return result + unit
|
||||
}
|
||||
|
||||
// ToMegabytes parses a string formatted by ByteSize as megabytes.
|
||||
func ToMegabytes(s string) (uint64, error) {
|
||||
bytes, err := ToBytes(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return bytes / MEGABYTE, nil
|
||||
}
|
||||
|
||||
// ToBytes parses a string formatted by ByteSize as bytes. Note binary-prefixed and SI prefixed units both mean a base-2 units
|
||||
// KB = K = KiB = 1024
|
||||
// MB = M = MiB = 1024 * K
|
||||
// GB = G = GiB = 1024 * M
|
||||
// TB = T = TiB = 1024 * G
|
||||
// PB = P = PiB = 1024 * T
|
||||
// EB = E = EiB = 1024 * P
|
||||
func ToBytes(s string) (uint64, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.ToUpper(s)
|
||||
|
||||
i := strings.IndexFunc(s, unicode.IsLetter)
|
||||
|
||||
if i == -1 {
|
||||
return 0, invalidByteQuantityError
|
||||
}
|
||||
|
||||
bytesString, multiple := s[:i], s[i:]
|
||||
bytes, err := strconv.ParseFloat(bytesString, 64)
|
||||
if err != nil || bytes < 0 {
|
||||
return 0, invalidByteQuantityError
|
||||
}
|
||||
|
||||
switch multiple {
|
||||
case "E", "EB", "EIB":
|
||||
return uint64(bytes * EXABYTE), nil
|
||||
case "P", "PB", "PIB":
|
||||
return uint64(bytes * PETABYTE), nil
|
||||
case "T", "TB", "TIB":
|
||||
return uint64(bytes * TERABYTE), nil
|
||||
case "G", "GB", "GIB":
|
||||
return uint64(bytes * GIGABYTE), nil
|
||||
case "M", "MB", "MIB":
|
||||
return uint64(bytes * MEGABYTE), nil
|
||||
case "K", "KB", "KIB":
|
||||
return uint64(bytes * KILOBYTE), nil
|
||||
case "B":
|
||||
return uint64(bytes), nil
|
||||
default:
|
||||
return 0, invalidByteQuantityError
|
||||
}
|
||||
}
|
33
utils/createfile.go
Normal file
33
utils/createfile.go
Normal file
@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func CreateFileIfNotExists(filePath string) error {
|
||||
// Check if the file exists
|
||||
_, err := os.Stat(filePath)
|
||||
if err == nil {
|
||||
// File exists
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create the directory
|
||||
dir := filepath.Dir(filePath)
|
||||
err = os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
// Failed to create directory
|
||||
return err
|
||||
}
|
||||
|
||||
// Create an empty file
|
||||
f, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
// Failed to create file
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
return nil
|
||||
}
|
63
utils/encrypt/azdg.go
Normal file
63
utils/encrypt/azdg.go
Normal file
@ -0,0 +1,63 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/wonli/aqi/utils"
|
||||
)
|
||||
|
||||
type Azdg struct {
|
||||
cipherHash string
|
||||
}
|
||||
|
||||
func NewAzdg(key string) *Azdg {
|
||||
cipherHash := fmt.Sprintf("%x", md5.Sum([]byte(key)))
|
||||
return &Azdg{cipherHash: cipherHash}
|
||||
}
|
||||
|
||||
func (a *Azdg) Encrypt(sourceText string) string {
|
||||
noise := utils.GetRandomString(32)
|
||||
inputData := []byte(sourceText)
|
||||
loopCount := len(inputData)
|
||||
outData := make([]byte, loopCount*2)
|
||||
|
||||
for i, j := 0, 0; i < loopCount; i, j = i+1, j+1 {
|
||||
outData[j] = noise[i%32]
|
||||
j++
|
||||
outData[j] = inputData[i] ^ noise[i%32]
|
||||
}
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(a.cipherEncode(fmt.Sprintf("%s", outData))))
|
||||
}
|
||||
|
||||
func (a *Azdg) Decrypt(sourceText string) string {
|
||||
buf, err := base64.RawURLEncoding.DecodeString(sourceText)
|
||||
if err != nil {
|
||||
fmt.Printf("Decode(%q) failed: %v", sourceText, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
inputData := []byte(a.cipherEncode(fmt.Sprintf("%s", buf)))
|
||||
loopCount := len(inputData)
|
||||
outData := make([]byte, loopCount)
|
||||
|
||||
var p int
|
||||
for i, j := 0, 0; i < loopCount; i, j = i+2, j+1 {
|
||||
p = p + 1
|
||||
outData[j] = inputData[i] ^ inputData[i+1]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s", outData[:p])
|
||||
}
|
||||
|
||||
func (a *Azdg) cipherEncode(sourceText string) string {
|
||||
inputData := []byte(sourceText)
|
||||
loopCount := len(inputData)
|
||||
outData := make([]byte, loopCount)
|
||||
for i := 0; i < loopCount; i++ {
|
||||
outData[i] = inputData[i] ^ a.cipherHash[i%32]
|
||||
}
|
||||
return fmt.Sprintf("%s", outData)
|
||||
}
|
26
utils/encrypt/azdg_test.go
Normal file
26
utils/encrypt/azdg_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAzdg(t *testing.T) {
|
||||
t1 := time.Now()
|
||||
amap := map[string]bool{}
|
||||
azdg := NewAzdg("123")
|
||||
for i := 0; i < 10000; i++ {
|
||||
s := azdg.Encrypt("hello world 我是中文")
|
||||
_, ok := amap[s]
|
||||
if !ok {
|
||||
amap[s] = true
|
||||
} else {
|
||||
t.Error("出错了")
|
||||
}
|
||||
|
||||
_ = azdg.Decrypt(s)
|
||||
}
|
||||
|
||||
fmt.Println(time.Now().Sub(t1))
|
||||
}
|
73
utils/encrypt/encrypt_aes.go
Normal file
73
utils/encrypt/encrypt_aes.go
Normal file
@ -0,0 +1,73 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
// AesEncrypt AES Encrypt,CBC
|
||||
func AesEncrypt(origData []byte, key string) ([]byte, error) {
|
||||
encryptKey := getKey(key)
|
||||
block, err := aes.NewCipher(encryptKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
origData = pkcs7Padding(origData, blockSize)
|
||||
blockMode := cipher.NewCBCEncrypter(block, encryptKey[:blockSize])
|
||||
encrypted := make([]byte, len(origData))
|
||||
blockMode.CryptBlocks(encrypted, origData)
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// AesDecrypt AES Decrypt
|
||||
func AesDecrypt(encrypted []byte, key string) ([]byte, error) {
|
||||
encryptKey := getKey(key)
|
||||
block, err := aes.NewCipher(encryptKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, encryptKey[:blockSize])
|
||||
origData := make([]byte, len(encrypted))
|
||||
blockMode.CryptBlocks(origData, encrypted)
|
||||
err, data := pkcs7UnPadding(origData)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func getKey(key string) []byte {
|
||||
sha := sha1.New()
|
||||
_, err := sha.Write([]byte(key))
|
||||
if err != nil {
|
||||
logger.SugarLog.Errorf("Gen key fail %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
byteKey := []byte(hex.EncodeToString(sha.Sum(nil)))
|
||||
return byteKey[:32]
|
||||
}
|
||||
|
||||
func pkcs7Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padText...)
|
||||
}
|
||||
|
||||
func pkcs7UnPadding(origData []byte) (error, []byte) {
|
||||
length := len(origData)
|
||||
unPadding := int(origData[length-1])
|
||||
|
||||
if length-unPadding < 0 {
|
||||
return errors.New("PKCS7 fail"), nil
|
||||
}
|
||||
|
||||
return nil, origData[:(length - unPadding)]
|
||||
}
|
25
utils/encrypt/encrypt_aes_test.go
Normal file
25
utils/encrypt/encrypt_aes_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAes(t *testing.T) {
|
||||
t1 := time.Now()
|
||||
for i := 0; i < 10000; i++ {
|
||||
s, _ := AesEncrypt([]byte("hello world 我是中文"), "hello")
|
||||
ss := base64.RawURLEncoding.EncodeToString(s)
|
||||
//fmt.Println(ss)
|
||||
|
||||
ss1, _ := base64.RawURLEncoding.DecodeString(ss)
|
||||
_, err := AesDecrypt(ss1, "hello")
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(time.Now().Sub(t1))
|
||||
}
|
12
utils/encrypt/md5.go
Normal file
12
utils/encrypt/md5.go
Normal file
@ -0,0 +1,12 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func MD5(v string) string {
|
||||
m := md5.New()
|
||||
m.Write([]byte(v))
|
||||
return hex.EncodeToString(m.Sum(nil))
|
||||
}
|
27
utils/encrypt/sha256_rsa.go
Normal file
27
utils/encrypt/sha256_rsa.go
Normal file
@ -0,0 +1,27 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SignSHA256WithRSA generates a signature for a string using the SHA256WithRSA algorithm with a given private key.
|
||||
func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) {
|
||||
if privateKey == nil {
|
||||
return "", fmt.Errorf("private key should not be nil")
|
||||
}
|
||||
h := crypto.Hash.New(crypto.SHA256)
|
||||
_, err = h.Write([]byte(source))
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
hashed := h.Sum(nil)
|
||||
signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(signatureByte), nil
|
||||
}
|
15
utils/file_exists.go
Normal file
15
utils/file_exists.go
Normal file
@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import "os"
|
||||
|
||||
func FileExists(filePath string) (bool, error) {
|
||||
_, err := os.Stat(filePath)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
35
utils/filter_string.go
Normal file
35
utils/filter_string.go
Normal file
@ -0,0 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
func FilterStringId(stringIds string, filterId int) string {
|
||||
var result []int
|
||||
strArray := strings.Split(stringIds, ",")
|
||||
for _, strId := range strArray {
|
||||
strIdIntVal, err := strconv.Atoi(strId)
|
||||
if err != nil {
|
||||
logger.SugarLog.Errorf("translate id error %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if strIdIntVal != filterId {
|
||||
result = append(result, strIdIntVal)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) > 0 {
|
||||
return arrayToString(result, ",")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func arrayToString(a []int, delim string) string {
|
||||
return strings.Trim(strings.Replace(fmt.Sprint(a), " ", delim, -1), "[]")
|
||||
}
|
18
utils/format/format_byte.go
Normal file
18
utils/format/format_byte.go
Normal file
@ -0,0 +1,18 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
func Bites(size float64) string {
|
||||
unit := []string{"b", "kb", "mb", "gb", "tb", "pb"}
|
||||
s := math.Floor(math.Log(size) / math.Log(1024))
|
||||
i := int(s)
|
||||
|
||||
if i < len(unit) {
|
||||
return fmt.Sprintf("%.2f %s", size/math.Pow(1024, s), unit[i])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%f %s", size, unit[0])
|
||||
}
|
8
utils/format/format_byte_test.go
Normal file
8
utils/format/format_byte_test.go
Normal file
@ -0,0 +1,8 @@
|
||||
package format
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBite(t *testing.T) {
|
||||
bytes := Bites(2624954)
|
||||
t.Log(bytes)
|
||||
}
|
64
utils/format/format_time.go
Normal file
64
utils/format/format_time.go
Normal file
@ -0,0 +1,64 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FriendTime struct {
|
||||
t int64
|
||||
format string
|
||||
startTime int64
|
||||
suffix string
|
||||
}
|
||||
|
||||
func NewFriendTime(unixTime int64) *FriendTime {
|
||||
return &FriendTime{
|
||||
t: unixTime,
|
||||
format: "2006-01-02 15:04:05",
|
||||
suffix: "前",
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FriendTime) SetFormat(format string) {
|
||||
f.format = format
|
||||
}
|
||||
|
||||
func (f *FriendTime) SetStartTime(startTime int64) {
|
||||
f.startTime = startTime
|
||||
}
|
||||
|
||||
func (f *FriendTime) SetSuffix(suffix string) {
|
||||
f.suffix = suffix
|
||||
}
|
||||
|
||||
func (f *FriendTime) Format() string {
|
||||
startTime := f.startTime
|
||||
if startTime == 0 {
|
||||
startTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
delta := startTime - f.t
|
||||
if delta < 63072000 {
|
||||
conf := []struct {
|
||||
Duration int64
|
||||
Label string
|
||||
}{
|
||||
{31536000, "年"},
|
||||
{2592000, "个月"},
|
||||
{604800, "星期"},
|
||||
{86400, "天"},
|
||||
{3600, "小时"},
|
||||
{60, "分钟"},
|
||||
{1, "秒"},
|
||||
}
|
||||
|
||||
for _, diff := range conf {
|
||||
if c := delta / diff.Duration; c != 0 {
|
||||
return fmt.Sprintf("%d%s%s", c, diff.Label, f.suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return time.Unix(f.t, 0).Format(f.format)
|
||||
}
|
11
utils/format/format_time_test.go
Normal file
11
utils/format/format_time_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTime(t *testing.T) {
|
||||
fTime := NewFriendTime(1691724579)
|
||||
fTime.SetSuffix("哈哈")
|
||||
t.Log(fTime.Format())
|
||||
}
|
39
utils/geo/coordinate.go
Normal file
39
utils/geo/coordinate.go
Normal file
@ -0,0 +1,39 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Coordinate represents a specific location on Earth
|
||||
type Coordinate struct {
|
||||
Lat, Lng float64
|
||||
}
|
||||
|
||||
// Constants needed for distance calculations
|
||||
const (
|
||||
EarthRadius = 6371 * Kilometer
|
||||
DoubleEarthRadius = 2 * EarthRadius
|
||||
PiOver180 = math.Pi / 180
|
||||
)
|
||||
|
||||
// DistanceBetween calculates the distance between two coordinates
|
||||
func DistanceBetween(a, b Coordinate) Distance {
|
||||
value := 0.5 - math.Cos((b.Lat-a.Lat)*PiOver180)/2 + math.Cos(a.Lat*PiOver180)*math.Cos(b.Lat*PiOver180)*(1-math.Cos((b.Lng-a.Lng)*PiOver180))/2
|
||||
return DoubleEarthRadius * Distance(math.Asin(math.Sqrt(value)))
|
||||
}
|
||||
|
||||
// DistanceTo calculates the distance from this coordinate to another coordinate
|
||||
func (c Coordinate) DistanceTo(other Coordinate) Distance {
|
||||
return DistanceBetween(c, other)
|
||||
}
|
||||
|
||||
// String implements Stringer, returns a string representation of the coordinate
|
||||
func (c Coordinate) String() string {
|
||||
return fmt.Sprintf(
|
||||
"(%s, %s)",
|
||||
strconv.FormatFloat(c.Lat, 'f', -1, 64),
|
||||
strconv.FormatFloat(c.Lng, 'f', -1, 64),
|
||||
)
|
||||
}
|
69
utils/geo/distance.go
Normal file
69
utils/geo/distance.go
Normal file
@ -0,0 +1,69 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Distance represents a spacial distance. Fundamentally, the underlying float64 represents the raw
|
||||
// number of meters
|
||||
type Distance float64
|
||||
|
||||
// MilesPerKilometer Constants for conversions
|
||||
const MilesPerKilometer = 0.6213712
|
||||
|
||||
// Standard length constants
|
||||
const (
|
||||
Millimeter = Distance(0.001)
|
||||
Centimeter = Distance(0.01)
|
||||
Meter = Distance(1)
|
||||
Kilometer = Distance(1000)
|
||||
Mile = Distance(1 / MilesPerKilometer * 1000)
|
||||
)
|
||||
|
||||
// Millimeters gets the number of total millimeters represented by the distance
|
||||
func (d Distance) Millimeters() float64 {
|
||||
return d.Meters() * 1000
|
||||
}
|
||||
|
||||
// Centimeters gets the number of total centimeters represented by the distance
|
||||
func (d Distance) Centimeters() float64 {
|
||||
return d.Meters() * 100
|
||||
}
|
||||
|
||||
// Meters gets the number of total meters represented by the distance
|
||||
func (d Distance) Meters() float64 {
|
||||
return float64(d)
|
||||
}
|
||||
|
||||
// Kilometers gets the number of total kilometers represented by the distance
|
||||
func (d Distance) Kilometers() float64 {
|
||||
return float64(d) / 1000
|
||||
}
|
||||
|
||||
// Miles gets the number of total miles represented by the distance
|
||||
func (d Distance) Miles() float64 {
|
||||
return d.Kilometers() * MilesPerKilometer
|
||||
}
|
||||
|
||||
// String implements Stringer and returns a formatted string representation of the distance
|
||||
func (d Distance) String() string {
|
||||
if d < 0.01 {
|
||||
return strconv.FormatFloat(d.Millimeters(), 'f', 2, 64) + "mm"
|
||||
}
|
||||
|
||||
if d < 1 {
|
||||
return strconv.FormatFloat(d.Centimeters(), 'f', 2, 64) + "cm"
|
||||
}
|
||||
|
||||
if d < 100 {
|
||||
return strconv.FormatFloat(d.Meters(), 'f', 2, 64) + "m"
|
||||
}
|
||||
|
||||
return strconv.FormatFloat(d.Kilometers(), 'f', 2, 64) + "km"
|
||||
}
|
||||
|
||||
func (d Distance) Equals(other, tolerance Distance) bool {
|
||||
difference := math.Abs(float64(d - other))
|
||||
return difference <= float64(tolerance)
|
||||
}
|
8
utils/helper.go
Normal file
8
utils/helper.go
Normal file
@ -0,0 +1,8 @@
|
||||
package utils
|
||||
|
||||
func HidePhoneNumber(phoneNumber string) string {
|
||||
if len(phoneNumber) != 11 {
|
||||
return phoneNumber
|
||||
}
|
||||
return phoneNumber[:3] + "****" + phoneNumber[7:]
|
||||
}
|
37
utils/i18n/gorm.go
Normal file
37
utils/i18n/gorm.go
Normal file
@ -0,0 +1,37 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type GType struct {
|
||||
Data any
|
||||
lng string
|
||||
}
|
||||
|
||||
func NewGType(data any, lng string) *GType {
|
||||
return >ype{Data: data, lng: lng}
|
||||
}
|
||||
|
||||
func (i *GType) GormDataType() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func (i *GType) Scan(value interface{}) error {
|
||||
v, ok := value.([]byte)
|
||||
if ok {
|
||||
i.Data = string(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *GType) String() string {
|
||||
return fmt.Sprintf("%s", i.Data)
|
||||
}
|
||||
|
||||
func (i *GType) Value() (driver.Value, error) {
|
||||
return driver.Value(i.Data), nil
|
||||
}
|
103
utils/ip/real_ip.go
Normal file
103
utils/ip/real_ip.go
Normal file
@ -0,0 +1,103 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HostAndPort struct {
|
||||
Host string
|
||||
Port string
|
||||
}
|
||||
|
||||
// ipRange - a structure that holds the start and end of a range of ip addresses
|
||||
type ipRange struct {
|
||||
start net.IP
|
||||
end net.IP
|
||||
}
|
||||
|
||||
// inRange - check to see if a given ip address is within a range given
|
||||
func inRange(r ipRange, ipAddress net.IP) bool {
|
||||
// strcmp type byte comparison
|
||||
if bytes.Compare(ipAddress, r.start) >= 0 && bytes.Compare(ipAddress, r.end) < 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var privateRanges = []ipRange{
|
||||
{
|
||||
start: net.ParseIP("10.0.0.0"),
|
||||
end: net.ParseIP("10.255.255.255"),
|
||||
},
|
||||
{
|
||||
start: net.ParseIP("100.64.0.0"),
|
||||
end: net.ParseIP("100.127.255.255"),
|
||||
},
|
||||
{
|
||||
start: net.ParseIP("172.16.0.0"),
|
||||
end: net.ParseIP("172.31.255.255"),
|
||||
},
|
||||
{
|
||||
start: net.ParseIP("192.0.0.0"),
|
||||
end: net.ParseIP("192.0.0.255"),
|
||||
},
|
||||
{
|
||||
start: net.ParseIP("192.168.0.0"),
|
||||
end: net.ParseIP("192.168.255.255"),
|
||||
},
|
||||
{
|
||||
start: net.ParseIP("198.18.0.0"),
|
||||
end: net.ParseIP("198.19.255.255"),
|
||||
},
|
||||
}
|
||||
|
||||
// isPrivateSubnet - check to see if this ip is in a private subnet
|
||||
func isPrivateSubnet(ipAddress net.IP) bool {
|
||||
// my use case is only concerned with ipv4 atm
|
||||
if ipCheck := ipAddress.To4(); ipCheck != nil {
|
||||
// iterate over all our ranges
|
||||
for _, r := range privateRanges {
|
||||
// check if this ip is in a private range
|
||||
if inRange(r, ipAddress) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetIPAddress(r *http.Request) string {
|
||||
for _, h := range []string{"X-Forwarded-For", "X-Real-Ip"} {
|
||||
addresses := strings.Split(r.Header.Get(h), ",")
|
||||
// march from right to left until we get a public address
|
||||
// that will be the address right before our proxy.
|
||||
for i := len(addresses) - 1; i >= 0; i-- {
|
||||
ip := strings.TrimSpace(addresses[i])
|
||||
// header can contain spaces too, strip those out.
|
||||
realIP := net.ParseIP(ip)
|
||||
if !realIP.IsGlobalUnicast() || isPrivateSubnet(realIP) {
|
||||
// bad address, go to next
|
||||
continue
|
||||
}
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
func GetIpAndPort(r *http.Request) (error, *HostAndPort) {
|
||||
ipAddr := GetIPAddress(r)
|
||||
host, port, err := net.SplitHostPort(ipAddr)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
return nil, &HostAndPort{
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
}
|
49
utils/jwt/jwt.go
Normal file
49
utils/jwt/jwt.go
Normal file
@ -0,0 +1,49 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func GenerateToken(uid, authCode, channel string) (string, error) {
|
||||
|
||||
if viper.GetString("jwtSecurity") == "" {
|
||||
return "", errors.New("jwt security not configured")
|
||||
}
|
||||
|
||||
jwtLifetime := viper.GetDuration("jwtLifetime")
|
||||
if jwtLifetime == 0 {
|
||||
jwtLifetime = time.Hour * 48
|
||||
}
|
||||
|
||||
expire := time.Now().Add(jwtLifetime)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &LoginClaims{
|
||||
UID: uid,
|
||||
Channel: channel,
|
||||
AuthCode: authCode,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expire.Unix(),
|
||||
},
|
||||
})
|
||||
|
||||
return token.SignedString([]byte(viper.GetString("jwtSecurity")))
|
||||
}
|
||||
|
||||
func ValidToken(t string) (*LoginClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(t, &LoginClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(viper.GetString("jwtSecurity")), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*LoginClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
} else {
|
||||
return nil, errors.New("failed to validate token")
|
||||
}
|
||||
}
|
28
utils/jwt/login_claims.go
Normal file
28
utils/jwt/login_claims.go
Normal file
@ -0,0 +1,28 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
type LoginClaims struct {
|
||||
Channel string
|
||||
UID string
|
||||
AuthCode string
|
||||
StandardClaims jwt.StandardClaims
|
||||
}
|
||||
|
||||
func (l *LoginClaims) Valid() error {
|
||||
if l.UID == "" || l.Channel == "" {
|
||||
return errors.New("illegal tokens")
|
||||
}
|
||||
|
||||
t := time.Now().Unix()
|
||||
if t > l.StandardClaims.ExpiresAt {
|
||||
return errors.New("token has expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
31
utils/path.go
Normal file
31
utils/path.go
Normal file
@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetFilenamePath(prefix string) string {
|
||||
//gen ymd path
|
||||
p := time.Now().Format("/2006/01/02/")
|
||||
if prefix == "" {
|
||||
return p
|
||||
}
|
||||
|
||||
return strings.TrimRight(prefix, "/") + p
|
||||
}
|
||||
|
||||
// PathExists check path
|
||||
func PathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
112
utils/pem/pem.go
Normal file
112
utils/pem/pem.go
Normal file
@ -0,0 +1,112 @@
|
||||
package pem
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoadCertificate loads a certificate from its textual content.
|
||||
func LoadCertificate(certificateStr string) (certificate *x509.Certificate, err error) {
|
||||
block, _ := pem.Decode([]byte(certificateStr))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("decode certificate err")
|
||||
}
|
||||
|
||||
if block.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("the kind of PEM should be CERTIFICATE")
|
||||
}
|
||||
|
||||
certificate, err = x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse certificate err:%s", err.Error())
|
||||
}
|
||||
|
||||
return certificate, nil
|
||||
}
|
||||
|
||||
// LoadPrivateKey loads a private key from its textual content.
|
||||
func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) {
|
||||
block, _ := pem.Decode([]byte(privateKeyStr))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("decode private key err")
|
||||
}
|
||||
if block.Type != "PRIVATE KEY" {
|
||||
return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY")
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse private key err:%s", err.Error())
|
||||
}
|
||||
privateKey, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is not rsa private key", privateKeyStr)
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// LoadPublicKey loads a public key from its textual text content.
|
||||
func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) {
|
||||
block, _ := pem.Decode([]byte(publicKeyStr))
|
||||
if block == nil {
|
||||
return nil, errors.New("decode public key error")
|
||||
}
|
||||
if block.Type != "PUBLIC KEY" {
|
||||
return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY")
|
||||
}
|
||||
key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse public key err:%s", err.Error())
|
||||
}
|
||||
publicKey, ok := key.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr)
|
||||
}
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// LoadCertificateWithPath loads a certificate from a file path.
|
||||
func LoadCertificateWithPath(path string) (certificate *x509.Certificate, err error) {
|
||||
certificateBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read certificate pem file err:%s", err.Error())
|
||||
}
|
||||
return LoadCertificate(string(certificateBytes))
|
||||
}
|
||||
|
||||
// LoadPrivateKeyWithPath loads a private key from a file path.
|
||||
func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) {
|
||||
privateKeyBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read private pem file err:%s", err.Error())
|
||||
}
|
||||
return LoadPrivateKey(string(privateKeyBytes))
|
||||
}
|
||||
|
||||
// LoadPublicKeyWithPath load public key
|
||||
func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) {
|
||||
publicKeyBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read certificate pem file err:%s", err.Error())
|
||||
}
|
||||
return LoadPublicKey(string(publicKeyBytes))
|
||||
}
|
||||
|
||||
// GetCertificateSerialNumber retrieves the serial number from a certificate.
|
||||
func GetCertificateSerialNumber(certificate x509.Certificate) string {
|
||||
return fmt.Sprintf("%X", certificate.SerialNumber.Bytes())
|
||||
}
|
||||
|
||||
// IsCertificateExpired checks if the certificate is expired at a specific time.
|
||||
func IsCertificateExpired(certificate x509.Certificate, now time.Time) bool {
|
||||
return now.After(certificate.NotAfter)
|
||||
}
|
||||
|
||||
// IsCertificateValid checks if the certificate is valid at a specific time.
|
||||
func IsCertificateValid(certificate x509.Certificate, now time.Time) bool {
|
||||
return now.After(certificate.NotBefore) && now.Before(certificate.NotAfter)
|
||||
}
|
234
utils/pem/pem_test.go
Normal file
234
utils/pem/pem_test.go
Normal file
@ -0,0 +1,234 @@
|
||||
// Copyright 2021 Tencent Inc. All rights reserved.
|
||||
|
||||
package pem
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
testPemUtilCertificateStrWithoutTags = `MIIEhDCCA2ygAwIBAgIUDErvNLiTQIgbsrJSJjk9wuR/CUswDQYJKoZIhvcNAQEF
|
||||
BQAwRjEbMBkGA1UEAwwSVGVucGF5LmNvbSBVc2VyIENBMRIwEAYDVQQLDAlDQSBD
|
||||
ZW50ZXIxEzARBgNVBAoMClRlbnBheS5jb20wHhcNMjAwODA0MTAwMTI3WhcNMjUw
|
||||
ODAzMTAwMTI3WjCBlTEYMBYGA1UEAwwPVGVucGF5LmNvbSBzaWduMSUwIwYJKoZI
|
||||
hvcNAQkBFhZzdXBwb3J0QHN6aXRydXMuY29tLmNuMR0wGwYDVQQLDBRUZW5wYXku
|
||||
Y29tIENBIENlbnRlcjETMBEGA1UECgwKVGVucGF5LmNvbTERMA8GA1UEBwwIU2hl
|
||||
blpoZW4xCzAJBgNVBAYTAkNOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEAxTHf8ijqgucDt1PZEZ+FvGxR5po2fmw2pEzr2WK7KlbZYlNFMzo8OlAa38eU
|
||||
SIWBL6E70gnfVEnKxdMxMgSLrhf8nwy48X90lpm6NX1PYVJX8i/B5n0rS9hgOB35
|
||||
x0EjwpOeMHTyx9tWW+5/JmWcaUfF587eGoUpHlT3kciB6nDV1/yNMHoDw5vB2E9w
|
||||
LaiuGdWREhERYxsUCPyZZ1mltm5ClKAfrpPHWGSvarKI/G8ooDm3jXcgp2ajHNqB
|
||||
ErWP9yBTes42IT7mjmG++Ss4WyB5H91eTy7Xdj1FNQYgDHtNMVmnoggwdV6X4OBx
|
||||
biSJoKvpaghIoIdIlV7yTuDc/QIDAQABo4IBGDCCARQwCQYDVR0TBAIwADALBgNV
|
||||
HQ8EBAMCBsAwTwYIKwYBBQUHAQEEQzBBMD8GCCsGAQUFBzAChjNvY3NwLGh0dHA6
|
||||
Ly9Zb3VyX1NlcnZlcl9OYW1lOlBvcnQvVG9wQ0EvbG9kcF9CYXNlRE4waQYDVR0f
|
||||
BGIwYDBeoFygWoZYaHR0cDovLzkuMTkuMTYxLjQ6ODA4MC9Ub3BDQS9wdWJsaWMv
|
||||
aXRydXNjcmw/Q0E9MzlCNDk3QUJDOEFFODg1NzQ1QkY1NjgxRTRGMDNCOEI2NDdG
|
||||
MjhFQTAfBgNVHSMEGDAWgBROc805tvupF/jOiYapcvSklvPrLjAdBgNVHQ4EFgQU
|
||||
YqSE0znX254pZnVDEe1rpCzs5u8wDQYJKoZIhvcNAQEFBQADggEBABvRHEHSW9KK
|
||||
e6Dj5LGFO9Av20SWGMYVUNlwN4uWdoYZAesLl7Nog/znwHSVgyieqRUnKjm12L+h
|
||||
J2mIKtwvoZhGWlN7KA6zLrlio/w22oZfGbKYvU8GEHAQ/N483HvH3byYltDTvd8R
|
||||
YbxuS2D1GPYI3drRUXuEr9Qq8lcqHi0qVFvVKYm3VwXU+Rr7BOT9ebSGxH456IU8
|
||||
D17FsyucjhF/KRBGbN2pul0l7i1qMGkhNY18RkzrhWE8GB3PpaeWufOqgPgqUUPV
|
||||
Bii2fY18BZkSIos9s4yYMcPrA4ApHG+Fpb2NgfRNICEvIdXbhnEVMeWEqmW5SD9y
|
||||
mBlsiHvszAM=`
|
||||
testPemUtilCertificateStr = `-----BEGIN CERTIFICATE-----
|
||||
MIIEhDCCA2ygAwIBAgIUDErvNLiTQIgbsrJSJjk9wuR/CUswDQYJKoZIhvcNAQEF
|
||||
BQAwRjEbMBkGA1UEAwwSVGVucGF5LmNvbSBVc2VyIENBMRIwEAYDVQQLDAlDQSBD
|
||||
ZW50ZXIxEzARBgNVBAoMClRlbnBheS5jb20wHhcNMjAwODA0MTAwMTI3WhcNMjUw
|
||||
ODAzMTAwMTI3WjCBlTEYMBYGA1UEAwwPVGVucGF5LmNvbSBzaWduMSUwIwYJKoZI
|
||||
hvcNAQkBFhZzdXBwb3J0QHN6aXRydXMuY29tLmNuMR0wGwYDVQQLDBRUZW5wYXku
|
||||
Y29tIENBIENlbnRlcjETMBEGA1UECgwKVGVucGF5LmNvbTERMA8GA1UEBwwIU2hl
|
||||
blpoZW4xCzAJBgNVBAYTAkNOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEAxTHf8ijqgucDt1PZEZ+FvGxR5po2fmw2pEzr2WK7KlbZYlNFMzo8OlAa38eU
|
||||
SIWBL6E70gnfVEnKxdMxMgSLrhf8nwy48X90lpm6NX1PYVJX8i/B5n0rS9hgOB35
|
||||
x0EjwpOeMHTyx9tWW+5/JmWcaUfF587eGoUpHlT3kciB6nDV1/yNMHoDw5vB2E9w
|
||||
LaiuGdWREhERYxsUCPyZZ1mltm5ClKAfrpPHWGSvarKI/G8ooDm3jXcgp2ajHNqB
|
||||
ErWP9yBTes42IT7mjmG++Ss4WyB5H91eTy7Xdj1FNQYgDHtNMVmnoggwdV6X4OBx
|
||||
biSJoKvpaghIoIdIlV7yTuDc/QIDAQABo4IBGDCCARQwCQYDVR0TBAIwADALBgNV
|
||||
HQ8EBAMCBsAwTwYIKwYBBQUHAQEEQzBBMD8GCCsGAQUFBzAChjNvY3NwLGh0dHA6
|
||||
Ly9Zb3VyX1NlcnZlcl9OYW1lOlBvcnQvVG9wQ0EvbG9kcF9CYXNlRE4waQYDVR0f
|
||||
BGIwYDBeoFygWoZYaHR0cDovLzkuMTkuMTYxLjQ6ODA4MC9Ub3BDQS9wdWJsaWMv
|
||||
aXRydXNjcmw/Q0E9MzlCNDk3QUJDOEFFODg1NzQ1QkY1NjgxRTRGMDNCOEI2NDdG
|
||||
MjhFQTAfBgNVHSMEGDAWgBROc805tvupF/jOiYapcvSklvPrLjAdBgNVHQ4EFgQU
|
||||
YqSE0znX254pZnVDEe1rpCzs5u8wDQYJKoZIhvcNAQEFBQADggEBABvRHEHSW9KK
|
||||
e6Dj5LGFO9Av20SWGMYVUNlwN4uWdoYZAesLl7Nog/znwHSVgyieqRUnKjm12L+h
|
||||
J2mIKtwvoZhGWlN7KA6zLrlio/w22oZfGbKYvU8GEHAQ/N483HvH3byYltDTvd8R
|
||||
YbxuS2D1GPYI3drRUXuEr9Qq8lcqHi0qVFvVKYm3VwXU+Rr7BOT9ebSGxH456IU8
|
||||
D17FsyucjhF/KRBGbN2pul0l7i1qMGkhNY18RkzrhWE8GB3PpaeWufOqgPgqUUPV
|
||||
Bii2fY18BZkSIos9s4yYMcPrA4ApHG+Fpb2NgfRNICEvIdXbhnEVMeWEqmW5SD9y
|
||||
mBlsiHvszAM=
|
||||
-----END CERTIFICATE-----`
|
||||
testPemUtilCertificateSerial = `0C4AEF34B89340881BB2B25226393DC2E47F094B`
|
||||
|
||||
testPemUtilPrivateKeyStrWithoutTags = `MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZUJN33V+dSfvd
|
||||
fL0Mu+39XrZNXFFMQSy1V15FpncHeV47SmV0TzTqZc7hHB0ddqAdDi8Z5k3TKqb7
|
||||
6sOwYr5TcAfuR6PIPaleyE0/0KrljBum2Isa2Nyq7Dgc3ElBQ6YN4l/a+DpvKaz1
|
||||
FSKmKrhLNskqokWVSlu4g8OlKlbPXQ9ibII14MZRQrrkTmHYHzfi7GXXM0thAKuR
|
||||
0HNvyhTHBh4/lrYM3GaMvmWwkwvsMavnOex6+eioZHBOb1/EIZ/LzC6zuHArPpyW
|
||||
3daGaZ1rtQB1vVzTyERAVVFsXXgBHvfFud3w3ShsJYk8JvMwK2RpJ5/gV0QSARcm
|
||||
LDRUAlPzAgMBAAECggEBAMc7rDeUaXiWv6bMGbZ3BTXpg1FhdddnWUnYE8HfX/km
|
||||
OFI7XtBHXcgYFpcjYz4D5787pcsk7ezPidAj58zqenuclmjKnUmT3pfbI5eCA2v4
|
||||
C9HnbYDrmUPK1ZcADtka4D6ScDccpNYNa1g2TFHzkIrEa6H+q7S3O2fqxY/DRVtN
|
||||
0JIXalBb8daaqL5QVzSmM2BMVnHy+YITJWIkP2a3pKs9C0W65JGDsnG0wVrHinHF
|
||||
+cnhFZIbaPEI//DAFMc9NkrWOKVRTEgcCUxCFaHOZVNxDWZD7A2ZfJB2rK6eg//y
|
||||
gEiFDR2h6mTaDowMB4YF2n2dsIO4/dCG8vPHI20jn4ECgYEA/ZGu6lEMlO0XZnam
|
||||
AZGtiNgLcCfM/C2ZERZE7QTRPZH1WdK92Al9ndldsswFw4baJrJLCmghjF/iG4zi
|
||||
hhBvLnOLksnZUfjdumxoHDWXo2QBWbI5QsWIE7AuTiWgWj1I7X4fCXSQf6i+M/y2
|
||||
6TogQ7d0ANpZFyOkTNMn/tiJvLECgYEA22XqlamG/yfAGWery5KNH2DGlTIyd6xJ
|
||||
WtJ9j3jU99lZ0bCQ5xhiBbU9ImxCi3zgTsoqLWgA/p00HhNFNoUcTl9ofc0G3zwT
|
||||
D1y0ZzcnVKxGJdZ6ohW52V0hJStAigtjYAsUgjm7//FH7PiQDBDP1Wa6xSRkDQU/
|
||||
aSbQxvEE8+MCgYEA3bb8krW7opyM0XL9RHH0oqsFlVO30Oit5lrqebS0oHl3Zsr2
|
||||
ZGgoBlWBsEzk3UqUhTFwm/DhJLTSJ/TQPRkxnhQ5/mewNhS9C7yua7wQkzVmWN+V
|
||||
YeUGTvDGDF6qDz12/vJAgSwDDRym8x4NcXD5tTw7mmNRcwIfL22SkysThIECgYAV
|
||||
BgccoEoXWS/HP2/u6fQr9ZIR6eV8Ij5FPbZacTG3LlS1Cz5XZra95UgebFFUHHtC
|
||||
EY1JHJY7z8SWvTH8r3Su7eWNaIAoFBGffzqqSVazfm6aYZsOvRY6BfqPHT3p/H1h
|
||||
Tq6AbBffxrcltgvXnCTORjHPglU0CjSxVs7awW3AEQKBgB5WtaC8VLROM7rkfVIq
|
||||
+RXqE5vtJfa3e3N7W3RqxKp4zHFAPfr82FK5CX2bppEaxY7SEZVvVInKDc5gKdG/
|
||||
jWNRBmvvftZhY59PILHO2X5vO4FXh7suEjy6VIh0gsnK36mmRboYIBGsNuDHjXLe
|
||||
BDa+8mDLkWu5nHEhOxy2JJZl`
|
||||
testPemUtilPrivateKeyStr = `-----BEGIN TESTING KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDZUJN33V+dSfvd
|
||||
fL0Mu+39XrZNXFFMQSy1V15FpncHeV47SmV0TzTqZc7hHB0ddqAdDi8Z5k3TKqb7
|
||||
6sOwYr5TcAfuR6PIPaleyE0/0KrljBum2Isa2Nyq7Dgc3ElBQ6YN4l/a+DpvKaz1
|
||||
FSKmKrhLNskqokWVSlu4g8OlKlbPXQ9ibII14MZRQrrkTmHYHzfi7GXXM0thAKuR
|
||||
0HNvyhTHBh4/lrYM3GaMvmWwkwvsMavnOex6+eioZHBOb1/EIZ/LzC6zuHArPpyW
|
||||
3daGaZ1rtQB1vVzTyERAVVFsXXgBHvfFud3w3ShsJYk8JvMwK2RpJ5/gV0QSARcm
|
||||
LDRUAlPzAgMBAAECggEBAMc7rDeUaXiWv6bMGbZ3BTXpg1FhdddnWUnYE8HfX/km
|
||||
OFI7XtBHXcgYFpcjYz4D5787pcsk7ezPidAj58zqenuclmjKnUmT3pfbI5eCA2v4
|
||||
C9HnbYDrmUPK1ZcADtka4D6ScDccpNYNa1g2TFHzkIrEa6H+q7S3O2fqxY/DRVtN
|
||||
0JIXalBb8daaqL5QVzSmM2BMVnHy+YITJWIkP2a3pKs9C0W65JGDsnG0wVrHinHF
|
||||
+cnhFZIbaPEI//DAFMc9NkrWOKVRTEgcCUxCFaHOZVNxDWZD7A2ZfJB2rK6eg//y
|
||||
gEiFDR2h6mTaDowMB4YF2n2dsIO4/dCG8vPHI20jn4ECgYEA/ZGu6lEMlO0XZnam
|
||||
AZGtiNgLcCfM/C2ZERZE7QTRPZH1WdK92Al9ndldsswFw4baJrJLCmghjF/iG4zi
|
||||
hhBvLnOLksnZUfjdumxoHDWXo2QBWbI5QsWIE7AuTiWgWj1I7X4fCXSQf6i+M/y2
|
||||
6TogQ7d0ANpZFyOkTNMn/tiJvLECgYEA22XqlamG/yfAGWery5KNH2DGlTIyd6xJ
|
||||
WtJ9j3jU99lZ0bCQ5xhiBbU9ImxCi3zgTsoqLWgA/p00HhNFNoUcTl9ofc0G3zwT
|
||||
D1y0ZzcnVKxGJdZ6ohW52V0hJStAigtjYAsUgjm7//FH7PiQDBDP1Wa6xSRkDQU/
|
||||
aSbQxvEE8+MCgYEA3bb8krW7opyM0XL9RHH0oqsFlVO30Oit5lrqebS0oHl3Zsr2
|
||||
ZGgoBlWBsEzk3UqUhTFwm/DhJLTSJ/TQPRkxnhQ5/mewNhS9C7yua7wQkzVmWN+V
|
||||
YeUGTvDGDF6qDz12/vJAgSwDDRym8x4NcXD5tTw7mmNRcwIfL22SkysThIECgYAV
|
||||
BgccoEoXWS/HP2/u6fQr9ZIR6eV8Ij5FPbZacTG3LlS1Cz5XZra95UgebFFUHHtC
|
||||
EY1JHJY7z8SWvTH8r3Su7eWNaIAoFBGffzqqSVazfm6aYZsOvRY6BfqPHT3p/H1h
|
||||
Tq6AbBffxrcltgvXnCTORjHPglU0CjSxVs7awW3AEQKBgB5WtaC8VLROM7rkfVIq
|
||||
+RXqE5vtJfa3e3N7W3RqxKp4zHFAPfr82FK5CX2bppEaxY7SEZVvVInKDc5gKdG/
|
||||
jWNRBmvvftZhY59PILHO2X5vO4FXh7suEjy6VIh0gsnK36mmRboYIBGsNuDHjXLe
|
||||
BDa+8mDLkWu5nHEhOxy2JJZl
|
||||
-----END TESTING KEY-----`
|
||||
|
||||
testPemUtilPublicKeyStrWithoutTags = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2VCTd91fnUn73Xy9DLvt
|
||||
/V62TVxRTEEstVdeRaZ3B3leO0pldE806mXO4RwdHXagHQ4vGeZN0yqm++rDsGK+
|
||||
U3AH7kejyD2pXshNP9Cq5YwbptiLGtjcquw4HNxJQUOmDeJf2vg6byms9RUipiq4
|
||||
SzbJKqJFlUpbuIPDpSpWz10PYmyCNeDGUUK65E5h2B834uxl1zNLYQCrkdBzb8oU
|
||||
xwYeP5a2DNxmjL5lsJML7DGr5znsevnoqGRwTm9fxCGfy8wus7hwKz6clt3Whmmd
|
||||
a7UAdb1c08hEQFVRbF14AR73xbnd8N0obCWJPCbzMCtkaSef4FdEEgEXJiw0VAJT
|
||||
8wIDAQAB`
|
||||
testPemUtilPublicKeyStr = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2VCTd91fnUn73Xy9DLvt
|
||||
/V62TVxRTEEstVdeRaZ3B3leO0pldE806mXO4RwdHXagHQ4vGeZN0yqm++rDsGK+
|
||||
U3AH7kejyD2pXshNP9Cq5YwbptiLGtjcquw4HNxJQUOmDeJf2vg6byms9RUipiq4
|
||||
SzbJKqJFlUpbuIPDpSpWz10PYmyCNeDGUUK65E5h2B834uxl1zNLYQCrkdBzb8oU
|
||||
xwYeP5a2DNxmjL5lsJML7DGr5znsevnoqGRwTm9fxCGfy8wus7hwKz6clt3Whmmd
|
||||
a7UAdb1c08hEQFVRbF14AR73xbnd8N0obCWJPCbzMCtkaSef4FdEEgEXJiw0VAJT
|
||||
8wIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
)
|
||||
|
||||
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
|
||||
|
||||
func TestLoadCertificate(t *testing.T) {
|
||||
type args struct {
|
||||
certificateStr string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "load certificate from str success",
|
||||
args: args{certificateStr: testPemUtilCertificateStr},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "error loading certificate without tags",
|
||||
args: args{certificateStr: testPemUtilCertificateStrWithoutTags},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := LoadCertificate(tt.args.certificateStr)
|
||||
assert.Equal(t, tt.wantErr, err != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrivateKey(t *testing.T) {
|
||||
type args struct {
|
||||
privateKeyStr string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "load private key from str success",
|
||||
args: args{privateKeyStr: testingKey(testPemUtilPrivateKeyStr)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "error loading private key without tags",
|
||||
args: args{privateKeyStr: testPemUtilPrivateKeyStrWithoutTags},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := LoadPrivateKey(tt.args.privateKeyStr)
|
||||
assert.Equal(t, tt.wantErr, err != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPublicKey(t *testing.T) {
|
||||
type args struct {
|
||||
publicKeyStr string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "load public key from str success",
|
||||
args: args{publicKeyStr: testPemUtilPublicKeyStr},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "error loading public key without tags",
|
||||
args: args{publicKeyStr: testPemUtilPublicKeyStrWithoutTags},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := LoadPublicKey(tt.args.publicKeyStr)
|
||||
assert.Equal(t, tt.wantErr, err != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCertificateSerialNumber(t *testing.T) {
|
||||
certificate, err := LoadCertificate(testPemUtilCertificateStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
serial := GetCertificateSerialNumber(*certificate)
|
||||
|
||||
assert.Equal(t, testPemUtilCertificateSerial, serial)
|
||||
}
|
16
utils/random.go
Normal file
16
utils/random.go
Normal file
@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func GetRandomString(n int) string {
|
||||
randBytes := make([]byte, n/2)
|
||||
_, err := rand.Read(randBytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", randBytes)
|
||||
}
|
38
utils/random_nickname.go
Normal file
38
utils/random_nickname.go
Normal file
@ -0,0 +1,38 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func GetRandomNickname() string {
|
||||
nicknames := []string{
|
||||
"小兔", "蛋黄酥", "奶糖", "糯米团子", "芒果冰", "奶茶",
|
||||
"小甜心", "棉花糖", "蓝莓布丁", "糖豆",
|
||||
"蜜桃仙子", "小可爱", "蜜汁糖葫芦", "草莓蛋糕",
|
||||
"奇异果冰沙", "樱花泡芙", "抹茶红豆", "小橘子", "香蕉酥", "水蜜桃",
|
||||
"珍珠奶茶", "软糯甜筒", "杏仁曲奇", "蜜糖柚子",
|
||||
"草莓巧克力", "牛奶小饼干", "蜜桃凉粉", "巧克力酥球", "椰子树",
|
||||
"柠檬蛋挞", "蜜汁菠萝", "蓝莓芝士", "奇奇怪怪果冻", "绿色小葡萄",
|
||||
"香草冰淇淋", "奶黄包", "芝士薯片", "小鲸鱼",
|
||||
"榴莲糖", "红豆沙", "黑森林蛋糕", "脆皮炸鸡", "哈密瓜冰茶",
|
||||
"蔓越莓曲奇", "奶香葡萄干", "草莓玛奇朵", "爆米花",
|
||||
"蛋黄派", "牛奶软糖", "紫薯布丁", "可乐果冻", "葡萄柚汁",
|
||||
"小丸子", "网红", "阿飞", "小喵咪", "文艺青年", "格调", "暖阳", "吐槽王",
|
||||
"小野猫", "混迹者", "闲云野鹤", "懒癌晚期", "老干部", "奶油小生", "微笑天使",
|
||||
"快乐侠", "黑桃Ace", "初心者",
|
||||
"缘来如此", "小清新", "游戏玩家", "思维者",
|
||||
"匠人", "森林之子", "锐气", "眼镜男", "摄影师",
|
||||
"时尚达人", "自然主义", "慢活族", "独立思考", "不二神探",
|
||||
"小资", "创作者", "云游者", "美食家", "旅行达人",
|
||||
"品酒师", "画家", "宅舞者", "健身达人", "跳蚤市场", "剧情党",
|
||||
"音乐人", "手作达人", "文艺小清新", "设计师",
|
||||
"老司机", "资深玩家", "心灵鸡汤", "智者", "小贝壳", "沉稳", "生活行家", "老狼", "爱好者",
|
||||
"积极向上", "精神领袖", "滴水穿石", "构思家", "厨艺大师", "亲和者", "风度翩翩", "经验分享",
|
||||
"心灵导师", "文艺清新",
|
||||
"知识分子",
|
||||
}
|
||||
|
||||
// random return nickname
|
||||
nickname := nicknames[rand.Intn(len(nicknames))]
|
||||
return nickname
|
||||
}
|
9
utils/random_nickname_test.go
Normal file
9
utils/random_nickname_test.go
Normal file
@ -0,0 +1,9 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetRandomNickname(t *testing.T) {
|
||||
for i := 0; i <= 100; i++ {
|
||||
println(GetRandomNickname())
|
||||
}
|
||||
}
|
85
utils/regx/id.go
Normal file
85
utils/regx/id.go
Normal file
@ -0,0 +1,85 @@
|
||||
package regx
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CheckID(idCard string, justCheckLength bool) bool {
|
||||
// verify length
|
||||
var lengthValidate bool
|
||||
if len(idCard) == 18 {
|
||||
lengthValidate = true
|
||||
} else if len(idCard) == 15 {
|
||||
lengthValidate = true
|
||||
} else {
|
||||
lengthValidate = false
|
||||
}
|
||||
|
||||
if justCheckLength {
|
||||
return lengthValidate
|
||||
}
|
||||
|
||||
if !lengthValidate {
|
||||
return false
|
||||
}
|
||||
|
||||
cityCode := map[string]bool{
|
||||
"11": true, "12": true, "13": true, "14": true, "15": true,
|
||||
"21": true, "22": true, "23": true,
|
||||
"31": true, "32": true, "33": true, "34": true, "35": true, "36": true, "37": true,
|
||||
"41": true, "42": true, "43": true, "44": true, "45": true, "46": true,
|
||||
"50": true, "51": true, "52": true, "53": true, "54": true,
|
||||
"61": true, "62": true, "63": true, "64": true, "65": true,
|
||||
"71": true,
|
||||
"81": true, "82": true,
|
||||
"91": true,
|
||||
}
|
||||
|
||||
// verify area
|
||||
if _, ok := cityCode[idCard[0:2]]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
factor := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
|
||||
verifyNumberList := []string{"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"}
|
||||
|
||||
makeVerifyBit := func(idCard string) string {
|
||||
if len(idCard) != 17 {
|
||||
return ""
|
||||
}
|
||||
|
||||
checksum := 0
|
||||
for i := 0; i < 17; i++ {
|
||||
b, _ := strconv.Atoi(string(idCard[i]))
|
||||
checksum += b * factor[i]
|
||||
}
|
||||
|
||||
mod := checksum % 11
|
||||
return verifyNumberList[mod]
|
||||
}
|
||||
|
||||
if len(idCard) == 15 {
|
||||
if idCard[12:15] == "996" || idCard[12:15] == "997" || idCard[12:15] == "998" || idCard[12:15] == "999" {
|
||||
idCard = idCard[0:6] + "18" + idCard[6:9]
|
||||
} else {
|
||||
idCard = idCard[0:6] + "19" + idCard[6:9]
|
||||
}
|
||||
|
||||
idCard += makeVerifyBit(idCard)
|
||||
} else {
|
||||
if strings.ToUpper(idCard[17:]) != strings.ToUpper(makeVerifyBit(idCard[0:17])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// verify birthday
|
||||
birthDay := idCard[6:14]
|
||||
d, err := time.Parse("20060102", birthDay)
|
||||
if err != nil || d.Year() > time.Now().Year() || int(d.Month()) > 12 || d.Day() > 31 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
29
utils/regx/regx_date.go
Normal file
29
utils/regx/regx_date.go
Normal file
@ -0,0 +1,29 @@
|
||||
package regx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ValidateDates(dates []string) error {
|
||||
if dates == nil || len(dates) != 2 {
|
||||
return fmt.Errorf("please provide a start date and an end date")
|
||||
}
|
||||
|
||||
startDate, err1 := time.Parse("2006-01-02", dates[0])
|
||||
endDate, err2 := time.Parse("2006-01-02", dates[1])
|
||||
|
||||
if err1 != nil {
|
||||
return fmt.Errorf("invalid start date format: %v", err1)
|
||||
}
|
||||
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("onvalid end date format: %v", err2)
|
||||
}
|
||||
|
||||
if startDate.After(endDate) {
|
||||
return fmt.Errorf("start date cannot be later than end date")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
23
utils/regx/regx_phone.go
Normal file
23
utils/regx/regx_phone.go
Normal file
@ -0,0 +1,23 @@
|
||||
package regx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func VerifyPhoneNumber(phoneNumber string) error {
|
||||
if phoneNumber == "" {
|
||||
return fmt.Errorf("phone number cannot be empty")
|
||||
}
|
||||
|
||||
matched, err := regexp.MatchString(`^1[3456789]\d{9}$`, phoneNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !matched {
|
||||
return fmt.Errorf("invalid phone number")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
30
utils/regx/regx_time.go
Normal file
30
utils/regx/regx_time.go
Normal file
@ -0,0 +1,30 @@
|
||||
package regx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ValidateTimes(times []string) error {
|
||||
if times == nil || len(times) != 2 {
|
||||
return fmt.Errorf("incorrect input time format")
|
||||
}
|
||||
|
||||
startTime, err1 := time.Parse("15:04", times[0])
|
||||
endTime, err2 := time.Parse("15:04", times[1])
|
||||
|
||||
if err1 != nil {
|
||||
return fmt.Errorf("invalid start time format: %v", err1)
|
||||
}
|
||||
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("invalid end time format: %v", err2)
|
||||
}
|
||||
|
||||
// Check if start time is after end time
|
||||
if startTime.After(endTime) {
|
||||
return fmt.Errorf("start time cannot be later than end time")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
58
utils/rmb/yuan.go
Normal file
58
utils/rmb/yuan.go
Normal file
@ -0,0 +1,58 @@
|
||||
package rmb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func YuanToFen(yuan string, decimalPlaces float64) (int64, error) {
|
||||
parts := strings.Split(yuan, ".")
|
||||
integerPart, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fen := integerPart * 100
|
||||
|
||||
// If there is a decimal part
|
||||
if len(parts) > 1 {
|
||||
// Only get the first two decimal places, the rest will be ignored
|
||||
decimalPart := parts[1]
|
||||
if len(decimalPart) > 2 {
|
||||
decimalPart = decimalPart[:2]
|
||||
}
|
||||
|
||||
decimalValue, err := strconv.ParseInt(decimalPart, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Decide whether to add 10 or 100 based on the number of decimal places
|
||||
if len(decimalPart) == 1 {
|
||||
fen += decimalValue * 10
|
||||
} else {
|
||||
fen += decimalValue
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the percentage of the price
|
||||
fen = int64(float64(fen) * decimalPlaces)
|
||||
|
||||
return fen, nil
|
||||
}
|
||||
|
||||
func FloatYuanToFen(yuan float64, decimalPlaces int) (int64, error) {
|
||||
formatted := fmt.Sprintf("%.*f", decimalPlaces, yuan)
|
||||
|
||||
// Remove the decimal point
|
||||
formatted = strings.Replace(formatted, ".", "", -1)
|
||||
|
||||
// Convert the formatted string to int64
|
||||
fen, err := strconv.ParseInt(formatted, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return fen, nil
|
||||
}
|
5
utils/sensitive/README.md
Normal file
5
utils/sensitive/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
## 词库使用说明
|
||||
|
||||
Please place the word lists in the "words" directory, with the file format being TXT. Each line should contain one word, and you can create multiple word lists.
|
||||
|
||||
### 请将词库放在words目录下,文件格式为txt,一行一个,支持创建多个词库
|
6
utils/sensitive/embed.go
Normal file
6
utils/sensitive/embed.go
Normal file
@ -0,0 +1,6 @@
|
||||
package sensitive
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed words
|
||||
var Words embed.FS
|
33
utils/sensitive/sensitive_init.go
Normal file
33
utils/sensitive/sensitive_init.go
Normal file
@ -0,0 +1,33 @@
|
||||
package sensitive
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/importcjj/sensitive"
|
||||
)
|
||||
|
||||
var Entity *sensitive.Filter
|
||||
var sensitiveOnce sync.Once
|
||||
|
||||
func Init() {
|
||||
sensitiveOnce.Do(func() {
|
||||
Entity = sensitive.New()
|
||||
dd, err := Words.ReadDir("words")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range dd {
|
||||
data, err := Words.ReadFile("words/" + v.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
Entity.AddWord(strings.Split(string(data), "\n")...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Replace(msg string) string {
|
||||
return Entity.Replace(msg, '*')
|
||||
}
|
0
utils/sensitive/words/dict.txt
Normal file
0
utils/sensitive/words/dict.txt
Normal file
33
utils/slice_compare.go
Normal file
33
utils/slice_compare.go
Normal file
@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
// IdCompare
|
||||
// Change Increase Decrease
|
||||
func IdCompare(ids1, ids2 []uint) (change []uint, increase []uint, decrease []uint) {
|
||||
m := make(map[uint]bool)
|
||||
for _, item := range ids1 {
|
||||
m[item] = true
|
||||
}
|
||||
|
||||
sameMap := make(map[uint]bool)
|
||||
for _, id := range ids2 {
|
||||
_, ok := m[id]
|
||||
if ok {
|
||||
//changed
|
||||
change = append(change, id)
|
||||
sameMap[id] = true
|
||||
} else {
|
||||
//add
|
||||
increase = append(increase, id)
|
||||
}
|
||||
}
|
||||
|
||||
//decrease
|
||||
for _, id := range ids1 {
|
||||
_, ok := sameMap[id]
|
||||
if !ok {
|
||||
decrease = append(decrease, id)
|
||||
}
|
||||
}
|
||||
|
||||
return change, increase, decrease
|
||||
}
|
13
utils/slice_find.go
Normal file
13
utils/slice_find.go
Normal file
@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
func SliceFindIndex[T comparable](inputSlice []T, a T) int {
|
||||
findIndex := -1
|
||||
for index, element := range inputSlice {
|
||||
if element == a {
|
||||
findIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return findIndex
|
||||
}
|
13
utils/slice_unique.go
Normal file
13
utils/slice_unique.go
Normal file
@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
func UniqueSlice[T comparable](inputSlice []T) []T {
|
||||
uniqueSlice := make([]T, 0, len(inputSlice))
|
||||
seen := make(map[T]bool, len(inputSlice))
|
||||
for _, element := range inputSlice {
|
||||
if !seen[element] {
|
||||
uniqueSlice = append(uniqueSlice, element)
|
||||
seen[element] = true
|
||||
}
|
||||
}
|
||||
return uniqueSlice
|
||||
}
|
48
utils/string2id.go
Normal file
48
utils/string2id.go
Normal file
@ -0,0 +1,48 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func StringToIntSlice(str string) []uint {
|
||||
parts := strings.Split(str, ",")
|
||||
seen := make(map[uint]bool)
|
||||
nums := make([]uint, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
uNum := uint(num)
|
||||
if !seen[uNum] {
|
||||
nums = append(nums, uNum)
|
||||
seen[uNum] = true
|
||||
}
|
||||
}
|
||||
|
||||
return nums
|
||||
}
|
||||
|
||||
func IntSliceToString(nums ...uint) string {
|
||||
var strBuilder strings.Builder
|
||||
seen := make(map[uint]bool)
|
||||
for i, num := range nums {
|
||||
if seen[num] {
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
strBuilder.WriteString(",")
|
||||
}
|
||||
strBuilder.WriteString(strconv.FormatUint(uint64(num), 10))
|
||||
seen[num] = true
|
||||
}
|
||||
return strBuilder.String()
|
||||
}
|
24
utils/string2slice.go
Normal file
24
utils/string2slice.go
Normal file
@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func String2Slice(s string) []string {
|
||||
slc := strings.Split(s, ",")
|
||||
for i := range slc {
|
||||
slc[i] = strings.TrimSpace(slc[i])
|
||||
}
|
||||
|
||||
return slc
|
||||
}
|
||||
|
||||
func UintsToString(numbers ...uint) string {
|
||||
str := make([]string, len(numbers))
|
||||
for i, num := range numbers {
|
||||
str[i] = strconv.FormatUint(uint64(num), 10)
|
||||
}
|
||||
|
||||
return strings.Join(str, ",")
|
||||
}
|
14
utils/string_limit.go
Normal file
14
utils/string_limit.go
Normal file
@ -0,0 +1,14 @@
|
||||
package utils
|
||||
|
||||
func LimitString(str string, limit int) string {
|
||||
// Convert the string to a rune slice to properly handle Unicode characters
|
||||
runes := []rune(str)
|
||||
|
||||
if len(runes) > limit {
|
||||
// If the length exceeds the limit
|
||||
// truncate the slice to the specified length
|
||||
runes = runes[:limit]
|
||||
}
|
||||
|
||||
return string(runes)
|
||||
}
|
30
utils/struct2map.go
Normal file
30
utils/struct2map.go
Normal file
@ -0,0 +1,30 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func StructToMap(input any) map[string]any {
|
||||
out := make(map[string]any)
|
||||
v := reflect.ValueOf(input)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
// we only accept structs
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
typ := v.Type()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fi := typ.Field(i)
|
||||
// skip unexported fields
|
||||
if fi.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
out[fi.Name] = v.Field(i).Interface()
|
||||
}
|
||||
return out
|
||||
}
|
42
utils/struct2map_json.go
Normal file
42
utils/struct2map_json.go
Normal file
@ -0,0 +1,42 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type JsonMap struct {
|
||||
}
|
||||
|
||||
func (j *JsonMap) MarshalBinary() (data []byte, err error) {
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (j *JsonMap) UnmarshalBinary(data []byte) error {
|
||||
return json.Unmarshal(data, j)
|
||||
}
|
||||
|
||||
func (j *JsonMap) StructToMap(input any) map[string]any {
|
||||
out := make(map[string]any)
|
||||
v := reflect.ValueOf(input)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
// we only accept structs
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
typ := v.Type()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fi := typ.Field(i)
|
||||
// skip unexported fields
|
||||
if fi.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
out[fi.Name] = v.Field(i).Interface()
|
||||
}
|
||||
return out
|
||||
}
|
138
utils/t.go
Normal file
138
utils/t.go
Normal file
@ -0,0 +1,138 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Day = 24 * time.Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
LongTime = 37 * Year
|
||||
)
|
||||
|
||||
// Time formats a time into a relative string.
|
||||
//
|
||||
// Time(someT) -> "3 weeks ago"
|
||||
func Time(then time.Time) string {
|
||||
//return RelTime(then, time.Now(), "ago", "from now")
|
||||
return RelTime(then, time.Now(), "前", "刚刚")
|
||||
}
|
||||
|
||||
// A RelTimeMagnitude struct contains a relative time point at which
|
||||
// the relative format of time will switch to a new format string. A
|
||||
// slice of these in ascending order by their "D" field is passed to
|
||||
// CustomRelTime to format durations.
|
||||
//
|
||||
// The Format field is a string that may contain a "%s" which will be
|
||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||
// now") and a "%d" that will be replaced by the quantity.
|
||||
//
|
||||
// The DivBy field is the amount of time the time difference must be
|
||||
// divided by in order to display correctly.
|
||||
//
|
||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||
// DivBy should be time.Minute so whatever the duration is will be
|
||||
// expressed in minutes.
|
||||
type RelTimeMagnitude struct {
|
||||
D time.Duration
|
||||
Format string
|
||||
DivBy time.Duration
|
||||
}
|
||||
|
||||
//var defaultMagnitudes = []RelTimeMagnitude{
|
||||
// {time.Second, "now", time.Second},
|
||||
// {2 * time.Second, "1 second %s", 1},
|
||||
// {time.Minute, "%d seconds %s", time.Second},
|
||||
// {2 * time.Minute, "1 minute %s", 1},
|
||||
// {time.Hour, "%d minutes %s", time.Minute},
|
||||
// {2 * time.Hour, "1 hour %s", 1},
|
||||
// {Day, "%d hours %s", time.Hour},
|
||||
// {2 * Day, "1 day %s", 1},
|
||||
// {Week, "%d days %s", Day},
|
||||
// {2 * Week, "1 week %s", 1},
|
||||
// {Month, "%d weeks %s", Week},
|
||||
// {2 * Month, "1 month %s", 1},
|
||||
// {Year, "%d months %s", Month},
|
||||
// {18 * Month, "1 year %s", 1},
|
||||
// {2 * Year, "2 years %s", 1},
|
||||
// {LongTime, "%d years %s", Year},
|
||||
// {math.MaxInt64, "a long while %s", 1},
|
||||
//}
|
||||
|
||||
var defaultMagnitudes = []RelTimeMagnitude{
|
||||
{time.Second, "刚刚", time.Second},
|
||||
{2 * time.Second, "1 秒 %s", 1},
|
||||
{time.Minute, "%d 秒 %s", time.Second},
|
||||
{2 * time.Minute, "1 分钟 %s", 1},
|
||||
{time.Hour, "%d 分钟 %s", time.Minute},
|
||||
{2 * time.Hour, "1 小时 %s", 1},
|
||||
{Day, "%d 小时 %s", time.Hour},
|
||||
{2 * Day, "1 天 %s", 1},
|
||||
{Week, "%d 天 %s", Day},
|
||||
{2 * Week, "1 周 %s", 1},
|
||||
{Month, "%d 周 %s", Week},
|
||||
{2 * Month, "1 月 %s", 1},
|
||||
{Year, "%d 月 %s", Month},
|
||||
{18 * Month, "1 年 %s", 1},
|
||||
{2 * Year, "2 年 %s", 1},
|
||||
{LongTime, "%d years %s", Year},
|
||||
{math.MaxInt64, "%s", 1},
|
||||
}
|
||||
|
||||
// RelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times and two labels. In addition to the generic time
|
||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||
// the label corresponding to the smaller time is applied.
|
||||
//
|
||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||
}
|
||||
|
||||
// CustomRelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times two labels and a table of relative time formats.
|
||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||
// labels are used applied so that the label corresponding to the
|
||||
// smaller time is applied.
|
||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||
lbl := albl
|
||||
diff := b.Sub(a)
|
||||
|
||||
if a.After(b) {
|
||||
lbl = blbl
|
||||
diff = a.Sub(b)
|
||||
}
|
||||
|
||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||
return magnitudes[i].D > diff
|
||||
})
|
||||
|
||||
if n >= len(magnitudes) {
|
||||
n = len(magnitudes) - 1
|
||||
}
|
||||
mag := magnitudes[n]
|
||||
var args []interface{}
|
||||
escaped := false
|
||||
for _, ch := range mag.Format {
|
||||
if escaped {
|
||||
switch ch {
|
||||
case 's':
|
||||
args = append(args, lbl)
|
||||
case 'd':
|
||||
args = append(args, diff/mag.DivBy)
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = ch == '%'
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(mag.Format, args...)
|
||||
}
|
18
utils/time.go
Normal file
18
utils/time.go
Normal file
@ -0,0 +1,18 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func FriendlyTime(t time.Time) string {
|
||||
return Time(t)
|
||||
}
|
||||
|
||||
func TodayStartAndEndTime() (beginTime, endTime uint) {
|
||||
timeStr := time.Now().Format("2006-01-02")
|
||||
t, _ := time.ParseInLocation("2006-01-02", timeStr, time.Local)
|
||||
beginTime = uint(t.Unix())
|
||||
endTime = beginTime + 86400
|
||||
|
||||
return beginTime, endTime
|
||||
}
|
131
utils/tree/trie_tree.go
Normal file
131
utils/tree/trie_tree.go
Normal file
@ -0,0 +1,131 @@
|
||||
package tree
|
||||
|
||||
type Node struct {
|
||||
Val rune // The value of the node
|
||||
Depth int // The depth of the node in the tree
|
||||
Count int // Counts the number of branches
|
||||
Payload interface{} // The payload associated with the node
|
||||
Child map[rune]*Node // Children of the node, mapped by rune
|
||||
IsWord bool // Flag indicating if this node marks the end of a complete string
|
||||
}
|
||||
|
||||
// NewNode new node
|
||||
func NewNode() *Node {
|
||||
return &Node{Child: make(map[rune]*Node)}
|
||||
}
|
||||
|
||||
type Trie struct {
|
||||
Root *Node
|
||||
}
|
||||
|
||||
func NewTrie() *Trie {
|
||||
return &Trie{Root: NewNode()}
|
||||
}
|
||||
|
||||
// Insert node
|
||||
func (t *Trie) Insert(str string, p any) {
|
||||
if len(str) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
bt := []rune(str)
|
||||
node := t.Root
|
||||
for _, val := range bt {
|
||||
child, ok := node.Child[val]
|
||||
if !ok {
|
||||
child = NewNode()
|
||||
child.Val = val
|
||||
node.Child[val] = child
|
||||
node.Count += 1
|
||||
child.Depth = node.Depth + 1
|
||||
}
|
||||
node = child
|
||||
}
|
||||
|
||||
node.Payload = p
|
||||
node.IsWord = true
|
||||
}
|
||||
|
||||
func (t *Trie) Find(str string) (bool, interface{}) {
|
||||
bt := []rune(str)
|
||||
node := t.Root
|
||||
|
||||
for _, val := range bt {
|
||||
child, ok := node.Child[val]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
node = child
|
||||
}
|
||||
return node.IsWord, node.Payload
|
||||
}
|
||||
|
||||
// FindAll finds all strings that start with the given prefix and returns their payloads.
|
||||
func (t *Trie) FindAll(prefix string) []any {
|
||||
bt := []rune(prefix)
|
||||
node := t.Root
|
||||
|
||||
for _, val := range bt {
|
||||
child, ok := node.Child[val]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
node = child
|
||||
}
|
||||
|
||||
return t.collect(node)
|
||||
}
|
||||
|
||||
// collect Recursively collects all strings' payloads in the subtree rooted at the given node.
|
||||
func (t *Trie) collect(node *Node) (payloads []any) {
|
||||
if node.IsWord {
|
||||
payloads = append(payloads, node.Payload)
|
||||
}
|
||||
|
||||
for _, childNode := range node.Child {
|
||||
payloads = append(payloads, t.collect(childNode)...)
|
||||
}
|
||||
|
||||
return payloads
|
||||
}
|
||||
|
||||
// Del deletion of a node has the following cases:
|
||||
// 1. Prefix deletion: Check if Count is greater than 0, then set IsWord to false.
|
||||
// 3. String deletion:
|
||||
// a. If there is no branching, delete the entire string.
|
||||
// b. If there is branching, only delete the part that is not a common prefix.
|
||||
func (t *Trie) Del(str string) {
|
||||
bt := []rune(str)
|
||||
if len(str) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
node := t.Root
|
||||
var lastBranch *Node
|
||||
var delVal rune
|
||||
|
||||
for index, val := range bt {
|
||||
child, ok := node.Child[val]
|
||||
if ok {
|
||||
if child.Count > 1 {
|
||||
lastBranch = child
|
||||
delVal = bt[index+1]
|
||||
}
|
||||
}
|
||||
node = child
|
||||
}
|
||||
|
||||
if node.Count > 0 {
|
||||
// del prefix
|
||||
node.IsWord = false
|
||||
} else {
|
||||
if lastBranch == nil {
|
||||
// del charset
|
||||
lastBranch = t.Root
|
||||
delVal = bt[0]
|
||||
}
|
||||
delete(lastBranch.Child, delVal)
|
||||
lastBranch.Count -= 1
|
||||
}
|
||||
}
|
172
utils/trie_sensitive.go
Normal file
172
utils/trie_sensitive.go
Normal file
@ -0,0 +1,172 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mozillazg/go-pinyin"
|
||||
)
|
||||
|
||||
// SensitiveTrie 敏感词前缀树
|
||||
type SensitiveTrie struct {
|
||||
replaceChar rune // 敏感词替换的字符
|
||||
root *TrieNode
|
||||
}
|
||||
|
||||
// NewSensitiveTrie 构造敏感词前缀树实例
|
||||
func NewSensitiveTrie() *SensitiveTrie {
|
||||
return &SensitiveTrie{
|
||||
replaceChar: '*',
|
||||
root: &TrieNode{End: false},
|
||||
}
|
||||
}
|
||||
|
||||
// AddWords 批量添加敏感词
|
||||
func (s *SensitiveTrie) AddWords(sensitiveWords ...string) {
|
||||
a := pinyin.NewArgs()
|
||||
for _, sensitiveWord := range sensitiveWords {
|
||||
chnReg := regexp.MustCompile("[\u4e00-\u9fa5]")
|
||||
if chnReg.Match([]byte(sensitiveWord)) {
|
||||
// 只有中文才转
|
||||
lazyPy := pinyin.LazyPinyin(sensitiveWord, a)
|
||||
if lazyPy != nil {
|
||||
sFirstWords := ""
|
||||
for _, p := range lazyPy {
|
||||
sFirstWords += p[0:1]
|
||||
}
|
||||
|
||||
s.addWord(sFirstWords)
|
||||
s.addWord(strings.Join(lazyPy, ""))
|
||||
}
|
||||
}
|
||||
|
||||
s.addWord(sensitiveWord)
|
||||
}
|
||||
}
|
||||
|
||||
// Match 查找替换发现的敏感词
|
||||
func (s *SensitiveTrie) Match(text string) (sensitiveWords []string, replaceText string) {
|
||||
if s.root == nil {
|
||||
return nil, text
|
||||
}
|
||||
|
||||
// 过滤特殊字符
|
||||
filteredText := s.filterSpecialChar(text)
|
||||
sensitiveMap := make(map[string]*struct{}) // 利用map把相同的敏感词去重
|
||||
textChars := []rune(filteredText)
|
||||
textCharsCopy := make([]rune, len(textChars))
|
||||
copy(textCharsCopy, textChars)
|
||||
for i, textLen := 0, len(textChars); i < textLen; i++ {
|
||||
trieNode := s.root.findChild(textChars[i])
|
||||
if trieNode == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配到了敏感词的前缀,从后一个位置继续
|
||||
j := i + 1
|
||||
for ; j < textLen && trieNode != nil; j++ {
|
||||
if trieNode.End {
|
||||
// 完整匹配到了敏感词
|
||||
if _, ok := sensitiveMap[trieNode.Data]; !ok {
|
||||
sensitiveWords = append(sensitiveWords, trieNode.Data)
|
||||
}
|
||||
sensitiveMap[trieNode.Data] = nil
|
||||
|
||||
// 将匹配的文本的敏感词替换成 *
|
||||
s.replaceRune(textCharsCopy, i, j)
|
||||
}
|
||||
trieNode = trieNode.findChild(textChars[j])
|
||||
}
|
||||
|
||||
// 文本尾部命中敏感词情况
|
||||
if j == textLen && trieNode != nil && trieNode.End {
|
||||
if _, ok := sensitiveMap[trieNode.Data]; !ok {
|
||||
sensitiveWords = append(sensitiveWords, trieNode.Data)
|
||||
}
|
||||
sensitiveMap[trieNode.Data] = nil
|
||||
s.replaceRune(textCharsCopy, i, textLen)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sensitiveWords) > 0 {
|
||||
// 有敏感词
|
||||
replaceText = string(textCharsCopy)
|
||||
} else {
|
||||
// 没有则返回原来的文本
|
||||
replaceText = text
|
||||
}
|
||||
|
||||
return sensitiveWords, replaceText
|
||||
}
|
||||
|
||||
// AddWord 添加敏感词
|
||||
func (s *SensitiveTrie) addWord(sensitiveWord string) {
|
||||
// 添加前先过滤一遍
|
||||
sensitiveWord = s.filterSpecialChar(sensitiveWord)
|
||||
|
||||
// 将敏感词转换成utf-8编码后的rune类型(int32)
|
||||
tireNode := s.root
|
||||
sensitiveChars := []rune(sensitiveWord)
|
||||
for _, charInt := range sensitiveChars {
|
||||
// 添加敏感词到前缀树中
|
||||
tireNode = tireNode.addChild(charInt)
|
||||
}
|
||||
|
||||
tireNode.End = true
|
||||
tireNode.Data = sensitiveWord
|
||||
}
|
||||
|
||||
// replaceRune 字符替换
|
||||
func (s *SensitiveTrie) replaceRune(chars []rune, begin int, end int) {
|
||||
for i := begin; i < end; i++ {
|
||||
chars[i] = s.replaceChar
|
||||
}
|
||||
}
|
||||
|
||||
// filterSpecialChar 过滤特殊字符
|
||||
func (s *SensitiveTrie) filterSpecialChar(text string) string {
|
||||
text = strings.ToLower(text)
|
||||
text = strings.Replace(text, " ", "", -1) // 去除空格
|
||||
return text
|
||||
}
|
||||
|
||||
// TrieNode 敏感词前缀树节点
|
||||
type TrieNode struct {
|
||||
childMap map[rune]*TrieNode // 本节点下的所有子节点
|
||||
Data string // 在最后一个节点保存完整的一个内容
|
||||
End bool // 标识是否最后一个节点
|
||||
}
|
||||
|
||||
// addChild 前缀树添加字节点
|
||||
func (n *TrieNode) addChild(c rune) *TrieNode {
|
||||
|
||||
if n.childMap == nil {
|
||||
n.childMap = make(map[rune]*TrieNode)
|
||||
}
|
||||
|
||||
if trieNode, ok := n.childMap[c]; ok {
|
||||
// 存在不添加了
|
||||
return trieNode
|
||||
} else {
|
||||
// 不存在
|
||||
n.childMap[c] = &TrieNode{
|
||||
childMap: nil,
|
||||
End: false,
|
||||
}
|
||||
|
||||
return n.childMap[c]
|
||||
}
|
||||
}
|
||||
|
||||
// findChild 前缀树查找字节点
|
||||
func (n *TrieNode) findChild(c rune) *TrieNode {
|
||||
if n.childMap == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if trieNode, ok := n.childMap[c]; ok {
|
||||
return trieNode
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
21
utils/trie_sensitive_test.go
Normal file
21
utils/trie_sensitive_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSensitive(t *testing.T) {
|
||||
sensitiveWords := []string{
|
||||
"牛大大", "撒比",
|
||||
}
|
||||
|
||||
trie := NewSensitiveTrie()
|
||||
trie.AddWords(sensitiveWords...)
|
||||
|
||||
content := "今天,牛大大挑战sb灰大大"
|
||||
matchSensitiveWords, replaceText := trie.Match(content)
|
||||
|
||||
log.Printf("%v", trie)
|
||||
log.Println(matchSensitiveWords, replaceText)
|
||||
}
|
315
utils/wxc/wxbizmsgcrypt.go
Normal file
315
utils/wxc/wxbizmsgcrypt.go
Normal file
@ -0,0 +1,315 @@
|
||||
package wxc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
const (
|
||||
ValidateSignatureError int = -40001
|
||||
ParseXmlError int = -40002
|
||||
ComputeSignatureError int = -40003
|
||||
IllegalAesKey int = -40004
|
||||
ValidateCorpidError int = -40005
|
||||
EncryptAESError int = -40006
|
||||
DecryptAESError int = -40007
|
||||
IllegalBuffer int = -40008
|
||||
EncodeBase64Error int = -40009
|
||||
DecodeBase64Error int = -40010
|
||||
GenXmlError int = -40010
|
||||
ParseJsonError int = -40012
|
||||
GenJsonError int = -40013
|
||||
IllegalProtocolType int = -40014
|
||||
)
|
||||
|
||||
type ProtocolType int
|
||||
|
||||
const (
|
||||
XmlType ProtocolType = 1
|
||||
)
|
||||
|
||||
type CryptError struct {
|
||||
ErrCode int
|
||||
ErrMsg string
|
||||
}
|
||||
|
||||
func NewCryptError(err_code int, err_msg string) *CryptError {
|
||||
return &CryptError{ErrCode: err_code, ErrMsg: err_msg}
|
||||
}
|
||||
|
||||
type WXBizMsg4Recv struct {
|
||||
Tousername string `xml:"ToUserName"`
|
||||
Encrypt string `xml:"Encrypt"`
|
||||
Agentid string `xml:"AgentID"`
|
||||
}
|
||||
|
||||
type CDATA struct {
|
||||
Value string `xml:",cdata"`
|
||||
}
|
||||
|
||||
type WXBizMsg4Send struct {
|
||||
XMLName xml.Name `xml:"xml"`
|
||||
Encrypt CDATA `xml:"Encrypt"`
|
||||
Signature CDATA `xml:"MsgSignature"`
|
||||
Timestamp string `xml:"TimeStamp"`
|
||||
Nonce CDATA `xml:"Nonce"`
|
||||
}
|
||||
|
||||
func NewWXBizMsg4Send(encrypt, signature, timestamp, nonce string) *WXBizMsg4Send {
|
||||
return &WXBizMsg4Send{Encrypt: CDATA{Value: encrypt}, Signature: CDATA{Value: signature}, Timestamp: timestamp, Nonce: CDATA{Value: nonce}}
|
||||
}
|
||||
|
||||
type ProtocolProcessor interface {
|
||||
parse(src_data []byte) (*WXBizMsg4Recv, *CryptError)
|
||||
serialize(msg_send *WXBizMsg4Send) ([]byte, *CryptError)
|
||||
}
|
||||
|
||||
type WXBizMsgCrypt struct {
|
||||
token string
|
||||
encoding_aeskey string
|
||||
receiver_id string
|
||||
protocol_processor ProtocolProcessor
|
||||
}
|
||||
|
||||
type XmlProcessor struct {
|
||||
}
|
||||
|
||||
func (self *XmlProcessor) parse(src_data []byte) (*WXBizMsg4Recv, *CryptError) {
|
||||
var msg4_recv WXBizMsg4Recv
|
||||
err := xml.Unmarshal(src_data, &msg4_recv)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(ParseXmlError, "xml to msg fail")
|
||||
}
|
||||
return &msg4_recv, nil
|
||||
}
|
||||
|
||||
func (self *XmlProcessor) serialize(msg4_send *WXBizMsg4Send) ([]byte, *CryptError) {
|
||||
xml_msg, err := xml.Marshal(msg4_send)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(GenXmlError, err.Error())
|
||||
}
|
||||
return xml_msg, nil
|
||||
}
|
||||
|
||||
func NewWXBizMsgCrypt(token, encoding_aeskey, receiver_id string, protocol_type ProtocolType) *WXBizMsgCrypt {
|
||||
var protocol_processor ProtocolProcessor
|
||||
if protocol_type != XmlType {
|
||||
panic("unsupport protocal")
|
||||
} else {
|
||||
protocol_processor = new(XmlProcessor)
|
||||
}
|
||||
|
||||
return &WXBizMsgCrypt{token: token, encoding_aeskey: (encoding_aeskey + "="), receiver_id: receiver_id, protocol_processor: protocol_processor}
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) randString(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) pKCS7Padding(plaintext string, block_size int) []byte {
|
||||
padding := block_size - (len(plaintext) % block_size)
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(plaintext)
|
||||
buffer.Write(padtext)
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) pKCS7Unpadding(plaintext []byte, block_size int) ([]byte, *CryptError) {
|
||||
plaintext_len := len(plaintext)
|
||||
if nil == plaintext || plaintext_len == 0 {
|
||||
return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding error nil or zero")
|
||||
}
|
||||
if plaintext_len%block_size != 0 {
|
||||
return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding text not a multiple of the block size")
|
||||
}
|
||||
padding_len := int(plaintext[plaintext_len-1])
|
||||
return plaintext[:plaintext_len-padding_len], nil
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) cbcEncrypter(plaintext string) ([]byte, *CryptError) {
|
||||
aeskey, err := base64.StdEncoding.DecodeString(m.encoding_aeskey)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(DecodeBase64Error, err.Error())
|
||||
}
|
||||
const block_size = 32
|
||||
pad_msg := m.pKCS7Padding(plaintext, block_size)
|
||||
|
||||
block, err := aes.NewCipher(aeskey)
|
||||
if err != nil {
|
||||
return nil, NewCryptError(EncryptAESError, err.Error())
|
||||
}
|
||||
|
||||
ciphertext := make([]byte, len(pad_msg))
|
||||
iv := aeskey[:aes.BlockSize]
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
|
||||
mode.CryptBlocks(ciphertext, pad_msg)
|
||||
base64_msg := make([]byte, base64.StdEncoding.EncodedLen(len(ciphertext)))
|
||||
base64.StdEncoding.Encode(base64_msg, ciphertext)
|
||||
|
||||
return base64_msg, nil
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) cbcDecrypter(base64_encrypt_msg string) ([]byte, *CryptError) {
|
||||
aeskey, err := base64.StdEncoding.DecodeString(m.encoding_aeskey)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(DecodeBase64Error, err.Error())
|
||||
}
|
||||
|
||||
encrypt_msg, err := base64.StdEncoding.DecodeString(base64_encrypt_msg)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(DecodeBase64Error, err.Error())
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(aeskey)
|
||||
if err != nil {
|
||||
return nil, NewCryptError(DecryptAESError, err.Error())
|
||||
}
|
||||
|
||||
if len(encrypt_msg) < aes.BlockSize {
|
||||
return nil, NewCryptError(DecryptAESError, "encrypt_msg size is not valid")
|
||||
}
|
||||
|
||||
iv := aeskey[:aes.BlockSize]
|
||||
|
||||
if len(encrypt_msg)%aes.BlockSize != 0 {
|
||||
return nil, NewCryptError(DecryptAESError, "encrypt_msg not a multiple of the block size")
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
|
||||
mode.CryptBlocks(encrypt_msg, encrypt_msg)
|
||||
|
||||
return encrypt_msg, nil
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) calSignature(timestamp, nonce, data string) string {
|
||||
sort_arr := []string{m.token, timestamp, nonce, data}
|
||||
sort.Strings(sort_arr)
|
||||
var buffer bytes.Buffer
|
||||
for _, value := range sort_arr {
|
||||
buffer.WriteString(value)
|
||||
}
|
||||
|
||||
sha := sha1.New()
|
||||
sha.Write(buffer.Bytes())
|
||||
signature := fmt.Sprintf("%x", sha.Sum(nil))
|
||||
return string(signature)
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) ParsePlainText(plaintext []byte) ([]byte, uint32, []byte, []byte, *CryptError) {
|
||||
const block_size = 32
|
||||
plaintext, err := m.pKCS7Unpadding(plaintext, block_size)
|
||||
if nil != err {
|
||||
return nil, 0, nil, nil, err
|
||||
}
|
||||
|
||||
text_len := uint32(len(plaintext))
|
||||
if text_len < 20 {
|
||||
return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 1")
|
||||
}
|
||||
random := plaintext[:16]
|
||||
msg_len := binary.BigEndian.Uint32(plaintext[16:20])
|
||||
if text_len < (20 + msg_len) {
|
||||
return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 2")
|
||||
}
|
||||
|
||||
msg := plaintext[20 : 20+msg_len]
|
||||
receiver_id := plaintext[20+msg_len:]
|
||||
|
||||
return random, msg_len, msg, receiver_id, nil
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) VerifyURL(msg_signature, timestamp, nonce, echostr string) ([]byte, *CryptError) {
|
||||
signature := m.calSignature(timestamp, nonce, echostr)
|
||||
|
||||
if strings.Compare(signature, msg_signature) != 0 {
|
||||
return nil, NewCryptError(ValidateSignatureError, "signature not equal")
|
||||
}
|
||||
|
||||
plaintext, err := m.cbcDecrypter(echostr)
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _, msg, receiver_id, err := m.ParsePlainText(plaintext)
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(m.receiver_id) > 0 && strings.Compare(string(receiver_id), m.receiver_id) != 0 {
|
||||
fmt.Println(string(receiver_id), m.receiver_id, len(receiver_id), len(m.receiver_id))
|
||||
return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) EncryptMsg(reply_msg, timestamp, nonce string) ([]byte, *CryptError) {
|
||||
rand_str := m.randString(16)
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(rand_str)
|
||||
|
||||
msg_len_buf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(msg_len_buf, uint32(len(reply_msg)))
|
||||
buffer.Write(msg_len_buf)
|
||||
buffer.WriteString(reply_msg)
|
||||
buffer.WriteString(m.receiver_id)
|
||||
|
||||
tmp_ciphertext, err := m.cbcEncrypter(buffer.String())
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := string(tmp_ciphertext)
|
||||
|
||||
signature := m.calSignature(timestamp, nonce, ciphertext)
|
||||
|
||||
msg4_send := NewWXBizMsg4Send(ciphertext, signature, timestamp, nonce)
|
||||
return m.protocol_processor.serialize(msg4_send)
|
||||
}
|
||||
|
||||
func (m *WXBizMsgCrypt) DecryptMsg(msg_signature, timestamp, nonce string, post_data []byte) ([]byte, *CryptError) {
|
||||
msg4_recv, crypt_err := m.protocol_processor.parse(post_data)
|
||||
if nil != crypt_err {
|
||||
return nil, crypt_err
|
||||
}
|
||||
|
||||
signature := m.calSignature(timestamp, nonce, msg4_recv.Encrypt)
|
||||
|
||||
if strings.Compare(signature, msg_signature) != 0 {
|
||||
return nil, NewCryptError(ValidateSignatureError, "signature not equal")
|
||||
}
|
||||
|
||||
plaintext, crypt_err := m.cbcDecrypter(msg4_recv.Encrypt)
|
||||
if nil != crypt_err {
|
||||
return nil, crypt_err
|
||||
}
|
||||
|
||||
_, _, msg, receiver_id, crypt_err := m.ParsePlainText(plaintext)
|
||||
if nil != crypt_err {
|
||||
return nil, crypt_err
|
||||
}
|
||||
|
||||
if len(m.receiver_id) > 0 && strings.Compare(string(receiver_id), m.receiver_id) != 0 {
|
||||
return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
73
utils/zip.go
Normal file
73
utils/zip.go
Normal file
@ -0,0 +1,73 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Unzip(src, dest string) ([]string, error) {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var unzipFiles []string
|
||||
defer func() {
|
||||
_ = r.Close()
|
||||
}()
|
||||
|
||||
_ = os.MkdirAll(dest, 0755)
|
||||
|
||||
// Closure to address file descriptors issue with all the deferred .Close() methods
|
||||
extractAndWriteFile := func(f *zip.File) error {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = rc.Close()
|
||||
}()
|
||||
|
||||
path := filepath.Join(dest, f.Name)
|
||||
// Check for ZipSlip (Directory traversal)
|
||||
if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("illegal file path: %s", path)
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
_ = os.MkdirAll(path, f.Mode())
|
||||
} else {
|
||||
_ = os.MkdirAll(filepath.Dir(path), f.Mode())
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
_, err = io.Copy(f, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
unzipFiles = append(unzipFiles, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range r.File {
|
||||
err = extractAndWriteFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return unzipFiles, nil
|
||||
}
|
23
validate/util_gin.go
Normal file
23
validate/util_gin.go
Normal file
@ -0,0 +1,23 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var GinBinding *Manager
|
||||
|
||||
func GinValidator() error {
|
||||
// 修改gin框架中的Validator引擎属性
|
||||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||
v.RegisterTagNameFunc(vc.tagNameFunc)
|
||||
GinBinding = &Manager{
|
||||
Validator: v,
|
||||
Trans: vc.getTranslator(),
|
||||
}
|
||||
|
||||
return vc.registerTrans(v, GinBinding.Trans)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
32
validate/util_normal.go
Normal file
32
validate/util_normal.go
Normal file
@ -0,0 +1,32 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var once sync.Once
|
||||
var normalManager *Manager
|
||||
|
||||
func Normal(language string) *Manager {
|
||||
once.Do(func() {
|
||||
cc := InitTranslator(language)
|
||||
|
||||
validate := validator.New()
|
||||
validate.RegisterTagNameFunc(cc.tagNameFunc)
|
||||
|
||||
translator := cc.getTranslator()
|
||||
err := cc.registerTrans(validate, translator)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
normalManager = &Manager{
|
||||
Validator: validate,
|
||||
Trans: translator,
|
||||
}
|
||||
})
|
||||
|
||||
return normalManager
|
||||
}
|
97
validate/validate_init.go
Normal file
97
validate/validate_init.go
Normal file
@ -0,0 +1,97 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-playground/locales"
|
||||
"github.com/go-playground/locales/en"
|
||||
"github.com/go-playground/locales/zh"
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
enT "github.com/go-playground/validator/v10/translations/en"
|
||||
zhT "github.com/go-playground/validator/v10/translations/zh"
|
||||
)
|
||||
|
||||
var sg sync.Once
|
||||
var vc *validatorConfig
|
||||
|
||||
type validatorConfig struct {
|
||||
locale string
|
||||
zh locales.Translator
|
||||
en locales.Translator
|
||||
}
|
||||
|
||||
// InitTranslator validator默认仅支持中英文
|
||||
func InitTranslator(locale string) *validatorConfig {
|
||||
sg.Do(func() {
|
||||
zhl := zh.New() // 中文翻译器
|
||||
enl := en.New() // 英文翻译器
|
||||
|
||||
//赋值给valid
|
||||
vc = &validatorConfig{
|
||||
locale: locale,
|
||||
zh: zhl,
|
||||
en: enl,
|
||||
}
|
||||
})
|
||||
|
||||
return vc
|
||||
}
|
||||
|
||||
// 处理字段名称
|
||||
// 中文使用label标签,其他语言label+语言名称,没有设置时使用json名称
|
||||
func (a *validatorConfig) tagNameFunc(fld reflect.StructField) string {
|
||||
var name string
|
||||
switch a.locale {
|
||||
case "zh":
|
||||
name = fld.Tag.Get("label")
|
||||
default:
|
||||
name = fld.Tag.Get("label_" + a.locale)
|
||||
if name == "" {
|
||||
name = strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
|
||||
if name == "-" {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// translator
|
||||
func (a *validatorConfig) getTranslator() ut.Translator {
|
||||
// 第一个参数是备用(fallback)的语言环境
|
||||
// 后面的参数是应该支持的语言环境(支持多个)
|
||||
// uni := ut.New(zhl, zhl) 也是可以的
|
||||
uni := ut.New(a.en, a.zh, a.en)
|
||||
|
||||
// locale 通常取决于 http 请求头的 'Accept-Language'
|
||||
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
|
||||
trans, ok := uni.GetTranslator(a.locale)
|
||||
if !ok {
|
||||
color.Red("uni.GetTranslator(%s) failed", a.locale)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return trans
|
||||
}
|
||||
|
||||
// registerTrans
|
||||
func (a *validatorConfig) registerTrans(v *validator.Validate, trans ut.Translator) error {
|
||||
var err error
|
||||
switch a.locale {
|
||||
case "en":
|
||||
err = enT.RegisterDefaultTranslations(v, trans)
|
||||
case "zh":
|
||||
err = zhT.RegisterDefaultTranslations(v, trans)
|
||||
default:
|
||||
err = enT.RegisterDefaultTranslations(v, trans)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
79
validate/validate_manager.go
Normal file
79
validate/validate_manager.go
Normal file
@ -0,0 +1,79 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
//Trans
|
||||
Trans ut.Translator
|
||||
|
||||
//允许外部自定义验证方法
|
||||
Validator *validator.Validate
|
||||
}
|
||||
|
||||
// RegisterValidator 自定义简单验证方法
|
||||
func (g *Manager) RegisterValidator(tag, errMsg string, fn validator.Func) error {
|
||||
err := g.Validator.RegisterValidation(tag, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.Validator.RegisterTranslation(tag, g.Trans,
|
||||
func(ut ut.Translator) error {
|
||||
return ut.Add(tag, errMsg, true)
|
||||
},
|
||||
|
||||
func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T(tag, fe.Tag())
|
||||
return t
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// RegisterValidatorFunc 自定义方法封装
|
||||
func (g *Manager) RegisterValidatorFunc(tag string,
|
||||
fn validator.Func, rFn validator.RegisterTranslationsFunc, tFn validator.TranslationFunc) error {
|
||||
err := g.Validator.RegisterValidation(tag, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.Validator.RegisterTranslation(tag, g.Trans, rFn, tFn)
|
||||
}
|
||||
|
||||
// Translator 语言翻译
|
||||
func (g *Manager) Translator(e error) error {
|
||||
errs, ok := e.(validator.ValidationErrors)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
if g.Trans == nil {
|
||||
return errs
|
||||
}
|
||||
|
||||
errorsTranslations := errs.Translate(g.Trans)
|
||||
for _, err := range errs {
|
||||
namespace := err.Namespace()
|
||||
if s, ok := errorsTranslations[namespace]; ok {
|
||||
return fmt.Errorf(s)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// Validate 执行验证并翻译配置指定的语言
|
||||
func (g *Manager) Validate(dataStruct any) error {
|
||||
//处理数据
|
||||
err := g.Validator.Struct(dataStruct)
|
||||
if err != nil {
|
||||
return g.Translator(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
12
worker/task.go
Normal file
12
worker/task.go
Normal file
@ -0,0 +1,12 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
type Task interface {
|
||||
GetName() string
|
||||
ProcessTask(ctx context.Context, task *asynq.Task) error
|
||||
}
|
82
worker/worker.go
Normal file
82
worker/worker.go
Normal file
@ -0,0 +1,82 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
var Engine *EngineClient
|
||||
|
||||
type EngineClient struct {
|
||||
Running bool
|
||||
Router map[string]Task
|
||||
|
||||
Opt *asynq.RedisClientOpt
|
||||
Server *asynq.Server
|
||||
}
|
||||
|
||||
func InitEngine(rds *asynq.RedisClientOpt, config asynq.Config) *EngineClient {
|
||||
server := asynq.NewServer(rds, config)
|
||||
Engine = &EngineClient{
|
||||
Opt: rds,
|
||||
Server: server,
|
||||
Router: map[string]Task{},
|
||||
}
|
||||
|
||||
return Engine
|
||||
}
|
||||
|
||||
func (e *EngineClient) Register(t Task) {
|
||||
if e.Running {
|
||||
logger.SugarLog.Errorf("please register in router")
|
||||
return
|
||||
}
|
||||
|
||||
name := t.GetName()
|
||||
if name == "" {
|
||||
logger.SugarLog.Errorf("failed to register, name is empty")
|
||||
return
|
||||
}
|
||||
|
||||
e.Router[name] = t
|
||||
}
|
||||
|
||||
func (e *EngineClient) Add(task *asynq.Task) error {
|
||||
t := task.Type()
|
||||
if t == "" {
|
||||
return fmt.Errorf("task type is undefined")
|
||||
}
|
||||
|
||||
_, ok := e.Router[t]
|
||||
if !ok {
|
||||
return fmt.Errorf("task not registered")
|
||||
}
|
||||
|
||||
client := asynq.NewClient(e.Opt)
|
||||
defer client.Close()
|
||||
|
||||
_, err := client.Enqueue(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EngineClient) Run() {
|
||||
s := asynq.NewServeMux()
|
||||
for name, handler := range e.Router {
|
||||
s.Handle(name, handler)
|
||||
}
|
||||
|
||||
e.Running = true
|
||||
err := e.Server.Run(s)
|
||||
if err != nil {
|
||||
logger.SugarLog.Errorf("failed to start asynq service: :%s", err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
188
ws/client.go
Normal file
188
ws/client.go
Normal file
@ -0,0 +1,188 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
|
||||
"github.com/wonli/aqi/logger"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Hub *Hubc `json:"-"`
|
||||
Conn net.Conn `json:"-"`
|
||||
Send chan []byte `json:"-"`
|
||||
Endpoint string `json:"-"` //入口地址
|
||||
OnceId string `json:"-"` //临时ID,扫码登录等场景作为客户端唯一标识
|
||||
Disconnecting bool `json:"-"` //已被设置为断开状态(消息发送完之后断开连接)
|
||||
SyncMsg bool `json:"-"` //是否接收消息
|
||||
LastMsgId int `json:"-"` //最后一条消息ID
|
||||
RequiredValid bool `json:"-"` //人机验证标识
|
||||
Validated bool `json:"-"` //是否已验证
|
||||
ValidExpiry time.Time `json:"-"` //验证有效期
|
||||
ValidCacheData any `json:"-"` //验证相关缓存数据
|
||||
AuthCode string `json:"-"` //用于校验JWT中的code,如果相等识别为同一个用户的网络地址变更
|
||||
ErrorCount int `json:"-"` //错误次数
|
||||
Closed bool `json:"-"` //是否已经关闭
|
||||
|
||||
User *User `json:"user,omitempty"` //关联用户
|
||||
Scope string `json:"scope"` //登录jwt scope, 用于判断用户从哪里登录的
|
||||
AppId string `json:"appId"` //登录应用Id
|
||||
StoreId uint `json:"storeId"` //店铺ID
|
||||
MerchantId uint `json:"merchantId"` //商户ID
|
||||
TenantId uint `json:"tenantId"` //租户ID
|
||||
Platform string `json:"platform"` //登录平台
|
||||
GroupId string `json:"groupId"` //用户分组Id
|
||||
IsLogin bool `json:"isLogin"` //是否已登录
|
||||
LoginAction string `json:"loginAction"` //登录动作
|
||||
ForceDialogId string `json:"forceDialogId"` //打开聊天界面的会话ID
|
||||
IpAddress string `json:"ipAddress"` //IP地址
|
||||
IpLocation string `json:"ipLocation"` //通过IP转换获得的地理位置
|
||||
IpConnAddr string `json:"IpConnAddr"` //conn连接IP地址
|
||||
ConnectionTime time.Time `json:"connectionTime"`
|
||||
LastRequestTime time.Time `json:"lastRequestTime"`
|
||||
LastHeartbeatTime time.Time `json:"lastHeartbeatTime"`
|
||||
|
||||
mu sync.RWMutex
|
||||
Keys map[string]any
|
||||
}
|
||||
|
||||
// Reader 读取
|
||||
func (c *Client) Reader() {
|
||||
defer func() {
|
||||
c.Hub.Disconnect <- c
|
||||
}()
|
||||
|
||||
for {
|
||||
request, op, err := wsutil.ReadClientData(c.Conn)
|
||||
if err != nil {
|
||||
c.Log("xx", "Error reading data", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if op == ws.OpText {
|
||||
req := string(request)
|
||||
c.Log("<-", req)
|
||||
go Dispatcher(c, req)
|
||||
} else if op == ws.OpPing {
|
||||
err = wsutil.WriteServerMessage(c.Conn, ws.OpPong, nil)
|
||||
if err != nil {
|
||||
c.Log("xx", "Reply pong", err.Error())
|
||||
}
|
||||
} else {
|
||||
c.Log("xx", "Unrecognized action")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write 发送
|
||||
func (c *Client) Write() {
|
||||
timer := time.NewTicker(5 * time.Second)
|
||||
defer func() {
|
||||
timer.Stop()
|
||||
c.Hub.Disconnect <- c
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-c.Send:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err := wsutil.WriteServerMessage(c.Conn, ws.OpText, msg)
|
||||
if err != nil {
|
||||
c.Log("xx", "Send msg error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//如果设置为断开状态
|
||||
//在消息发送完成后将断开与服务器的连接
|
||||
if c.Disconnecting {
|
||||
return
|
||||
}
|
||||
|
||||
c.Log("->", string(msg))
|
||||
case <-timer.C:
|
||||
err := wsutil.WriteServerMessage(c.Conn, ws.OpPing, nil)
|
||||
if err != nil {
|
||||
c.Log("xx", "Error actively pinging the client", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.LastHeartbeatTime = time.Now()
|
||||
if c.User != nil {
|
||||
c.User.LastHeartbeatTime = c.LastHeartbeatTime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log websocket日志
|
||||
func (c *Client) Log(symbol string, msg ...string) {
|
||||
s := strings.Join(msg, ", ")
|
||||
if c.IsLogin {
|
||||
s = fmt.Sprintf("%s %s [%s-%s] %s", c.Conn.RemoteAddr(), symbol, c.User.Suid, c.AppId, s)
|
||||
} else {
|
||||
s = fmt.Sprintf("%s %s %s", c.Conn.RemoteAddr(), symbol, s)
|
||||
}
|
||||
|
||||
if len(s) > 300 {
|
||||
logger.SugarLog.Info(s[:300] + "...")
|
||||
} else {
|
||||
logger.SugarLog.Info(s)
|
||||
}
|
||||
}
|
||||
|
||||
// SendMsg 把消息加入发送队列
|
||||
func (c *Client) SendMsg(msg []byte) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.Hub.Disconnect <- c
|
||||
logger.SugarLog.Errorf("SendMsg recover error(%s): %s", c.IpConnAddr, err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Send <- msg
|
||||
}
|
||||
|
||||
// SendRawMsg 构造消息再发送
|
||||
func (c *Client) SendRawMsg(code int, action, msg string, data any) {
|
||||
a := &Action{
|
||||
Action: action,
|
||||
Code: code,
|
||||
Msg: msg,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
c.SendMsg(a.Encode())
|
||||
}
|
||||
|
||||
// Close 关闭客户端
|
||||
func (c *Client) Close() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.Log("xx", "recover!! -> ", fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if !c.Closed {
|
||||
//防止重复关闭
|
||||
c.Closed = true
|
||||
|
||||
//关闭通道
|
||||
close(c.Send)
|
||||
|
||||
//关闭网络连接
|
||||
_ = c.Conn.Close()
|
||||
|
||||
//打印日志
|
||||
c.Log("xx", fmt.Sprintf("Close client -> %s", c.IpConnAddr))
|
||||
}
|
||||
}
|
22
ws/client_keyval.go
Normal file
22
ws/client_keyval.go
Normal file
@ -0,0 +1,22 @@
|
||||
package ws
|
||||
|
||||
func (c *Client) SetKey(key string, value any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.Keys == nil {
|
||||
c.Keys = make(map[string]any)
|
||||
}
|
||||
|
||||
c.Keys[key] = value
|
||||
}
|
||||
|
||||
func (c *Client) GetKey(key string) *Value {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
value, exists := c.Keys[key]
|
||||
if !exists {
|
||||
return &Value{}
|
||||
}
|
||||
|
||||
return &Value{data: value}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user