This commit is contained in:
ideaa 2024-06-18 18:08:39 +08:00
parent af402cc224
commit b9be165fd2
132 changed files with 7964 additions and 0 deletions

33
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

212
docs/zh-CN.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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))
}

View 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)]
}

View 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
View 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))
}

View 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
View 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
View 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), "[]")
}

View 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])
}

View File

@ -0,0 +1,8 @@
package format
import "testing"
func TestBite(t *testing.T) {
bytes := Bites(2624954)
t.Log(bytes)
}

View 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)
}

View 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
View 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
View 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
View 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
View 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 &GType{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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View File

@ -0,0 +1,6 @@
package sensitive
import "embed"
//go:embed words
var Words embed.FS

View 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, '*')
}

View File

33
utils/slice_compare.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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