游戏库

This commit is contained in:
liuxiaobo 2025-05-25 20:02:15 +08:00
parent dfe83bc0ec
commit 44c8f13453
55 changed files with 4121 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

66
db/clickhouse.go Normal file
View File

@ -0,0 +1,66 @@
package db
import (
"database/sql"
"fmt"
"github.com/ClickHouse/clickhouse-go/v2"
"time"
)
type ClickHouseRepo struct {
host string
port string
user string
password string
database string
connection *sql.DB
}
func NewClickHouseRepo(host, port, user, password, database string) *ClickHouseRepo {
return &ClickHouseRepo{
host: host,
port: port,
user: user,
password: password,
database: database,
}
}
func (c *ClickHouseRepo) Open() error {
c.connection = clickhouse.OpenDB(&clickhouse.Options{
Addr: []string{fmt.Sprintf("%v:%v", c.host, c.port)},
Auth: clickhouse.Auth{
Database: c.database,
Username: c.user,
Password: c.password,
},
Settings: clickhouse.Settings{
"max_execution_time": 60,
},
DialTimeout: 5 * time.Second,
Compression: &clickhouse.Compression{
Method: clickhouse.CompressionLZ4,
Level: 5,
},
Protocol: clickhouse.Native,
// Debug: true,
})
if c.connection == nil {
return fmt.Errorf("connect clickhouse fail")
}
c.connection.SetMaxIdleConns(5)
c.connection.SetMaxOpenConns(10)
c.connection.SetConnMaxLifetime(0)
return c.connection.Ping()
}
func (c *ClickHouseRepo) Close() {
_ = c.connection.Close()
}
func (c *ClickHouseRepo) Select(query string, args ...interface{}) (*sql.Rows, error) {
return c.connection.Query(query, args...)
}
func (c *ClickHouseRepo) Exec(query string, args ...interface{}) (sql.Result, error) {
return c.connection.Exec(query, args...)
}

97
db/clickhouse_test.go Normal file
View File

@ -0,0 +1,97 @@
package db
import (
"fmt"
"strings"
"testing"
)
func testCreateClickHouse(_ *testing.T) *ClickHouseRepo {
host := "192.168.2.224"
port := "9000"
// port = 8123
username := "default"
password := "123456"
database := "samba"
return NewClickHouseRepo(host, port, username, password, database)
}
func testSelect(t *testing.T) {
ch := testCreateClickHouse(t)
err := ch.Open()
if err != nil {
t.Fatal(err)
}
defer ch.Close()
rows, err := ch.Select("select user_id, coins from coins_flow limit 1")
if err != nil {
t.Fatal(err)
}
defer func() { _ = rows.Close() }()
cols, err := rows.Columns()
if err != nil {
t.Fatal(err)
}
fmt.Println(strings.Join(cols, ","))
// 一行数据使用any避开数据类型问题
var vRows = make([]any, len(cols))
// 存实际的值byte数组长度以列的数量为准
var values = make([][]byte, len(cols))
for i := 0; i < len(cols); i++ {
vRows[i] = &values[i]
}
for rows.Next() {
err = rows.Scan(vRows...)
if err != nil {
fmt.Println(err.Error())
break
}
var vString []string
for _, v := range values {
vString = append(vString, string(v))
}
fmt.Println(vString, ",")
}
}
func testInsert(t *testing.T) {
ch := testCreateClickHouse(t)
err := ch.Open()
if err != nil {
t.Fatal(err)
}
defer ch.Close()
_, err = ch.Exec("INSERT INTO coins_flow(user_id, coins) VALUES (11,11)", 11, 11)
if err != nil {
t.Fatal(err)
}
// last, err := result.LastInsertId()
// if err != nil {
// t.Fatal(err)
// }
// fmt.Println(last)
}
func testDelete(t *testing.T) {
ch := testCreateClickHouse(t)
err := ch.Open()
if err != nil {
t.Fatal(err)
}
defer ch.Close()
_, err = ch.Exec("DELETE FROM coins_flow WHERE user_id = 11 AND coins = 11")
if err != nil {
t.Fatal(err)
}
// last, err := result.LastInsertId()
// if err != nil {
// t.Fatal(err)
// }
// fmt.Println(last)
}
func TestClickHouse(t *testing.T) {
testSelect(t)
// testInsert(t)
testDelete(t)
}

36
db/db.go Normal file
View File

@ -0,0 +1,36 @@
package db
import (
"fmt"
"github.com/go-redis/redis/v8"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func InitMysql(username, password, address, port, dbName string) (*gorm.DB, error) {
dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", username, password, address, port, dbName)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: &dbLogger{}})
return db, err
}
func InitRedis(password, address, port string, dbName int) (*redis.Client, error) {
rdb := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("%v:%v", address, port), Password: password, DB: dbName})
if rdb == nil {
return nil, fmt.Errorf("init redis fail")
}
return rdb, nil
}
func InitClickHouse(host, port, user, password, database string) (*ClickHouseRepo, error) {
ch := NewClickHouseRepo(host, port, user, password, database)
return ch, ch.Open()
}
/*
sudo docker run -d \
--name my-redis \
-p 6379:6379 \
-e REDIS_PASSWORD=fox379@@zyxi \
redis:latest \
--requirepass fox379@@zyxi
*/

46
db/dblog.go Normal file
View File

@ -0,0 +1,46 @@
package db
import (
"context"
"fmt"
"github.com/fox/fox/log"
"gorm.io/gorm/logger"
"strings"
"time"
)
type dbLogger struct{}
func (l *dbLogger) LogMode(_ logger.LogLevel) logger.Interface {
// 可以在这里根据 logLevel 做出不同的处理
return l
}
func (l *dbLogger) Info(_ context.Context, msg string, args ...interface{}) {
log.InfoF(msg, args...)
}
func (l *dbLogger) Warn(_ context.Context, msg string, args ...interface{}) {
log.WarnF(msg, args...)
}
func (l *dbLogger) Error(_ context.Context, msg string, args ...interface{}) {
s := fmt.Sprintf(msg, args...)
if !strings.Contains(s, "record not found") {
// 只有当错误不是“record not found”时才记录
log.ErrorF(msg, args...)
}
}
func (l *dbLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if err != nil {
// SQL 执行错误
l.Error(ctx, "Error occurred while executing SQL: %v", err)
return
}
sql, rows := fc()
elapsed := time.Since(begin)
log.DebugF("SQL: %s, RowsAffected: %d, Elapsed: %s\n", sql, rows, elapsed)
}

65
etcd/etcd.go Normal file
View File

@ -0,0 +1,65 @@
package etcd
import (
"encoding/json"
"fmt"
"github.com/fox/fox/log"
"sync"
)
type resultT[T any] struct {
Value T
Err error
}
type Registry[T INode] struct {
*etcdRegistryImpl
nodes sync.Map
}
func NewRegistry[T INode](endpoints []string, rootKey, username, password string) (*Registry[T], error) {
var err error
e := &Registry[T]{}
e.etcdRegistryImpl, err = newServiceRegistryImpl(endpoints, rootKey, username, password, e.saveNode)
return e, err
}
func (e *Registry[T]) Register(node INode) error {
bs, err := json.Marshal(node)
if err != nil {
return err
}
return e.etcdRegistryImpl.Register(node.EtcdKey(), string(bs))
}
// 获取当前服务
func (sr *Registry[T]) saveNode(jsonBytes []byte) {
var tmp = resultT[T]{Err: nil}
if err := json.Unmarshal(jsonBytes, &tmp.Value); err != nil {
log.ErrorF(err.Error())
}
sr.nodes.Store(tmp.Value.MapKey(), tmp.Value)
}
// 获取当前根节点下所有节点信息
func (sr *Registry[T]) GetNodes() []T {
var nodes []T
sr.nodes.Range(func(k, v interface{}) bool {
nodes = append(nodes, v.(T))
return true
})
return nodes
}
// 获取当前根节点下所有节点信息
func (sr *Registry[T]) FindNode(key string) (T, error) {
var tmp = resultT[T]{Err: nil}
v, ok := sr.nodes.Load(key)
if !ok {
return tmp.Value, fmt.Errorf("%v not exist", key)
}
if tmp.Value, ok = v.(T); ok {
return tmp.Value, nil
}
return tmp.Value, fmt.Errorf("%v 类型转换失败", key)
}

167
etcd/etcdImpl.go Normal file
View File

@ -0,0 +1,167 @@
package etcd
import (
"context"
"fmt"
"github.com/fox/fox/ksync"
"github.com/fox/fox/log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
const (
DefaultDialTimeout = 3 * time.Second // 默认拨号超时时间
DefaultLeaseTTL = int64(10) // 默认租约TTL为60秒
DefaultKeepAliveInterval = 5 * time.Second // 默认续租间隔为30秒
KeepAliveFailCount = 100
)
type etcdRegistryImpl struct {
cli *clientv3.Client
leaseID clientv3.LeaseID
nodeKey string
cancelFunc context.CancelFunc
rootKey string
saveNodeFunc func(jsonBytes []byte)
}
// 创建服务注册中心
func newServiceRegistryImpl(endpoints []string, rootKey, username, password string, saveNode func([]byte)) (*etcdRegistryImpl, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: DefaultDialTimeout,
Username: username,
Password: password,
})
if err != nil {
return nil, err
}
return &etcdRegistryImpl{
cli: cli,
rootKey: rootKey,
saveNodeFunc: saveNode,
}, nil
}
// 注册服务 RegisterService
func (sr *etcdRegistryImpl) Register(key, value string) error {
// 生成唯一服务key
// /services/serviceType/serviceName
sr.nodeKey = key
log.DebugF("register %s to etcd", key)
// 创建租约
ctx, cancel := context.WithCancel(context.Background())
sr.cancelFunc = cancel
// 申请租约
resp, err := sr.cli.Grant(ctx, DefaultLeaseTTL)
if err != nil {
return err
}
sr.leaseID = resp.ID
// 序列化服务信息
// data, err := json.Marshal(node)
// if err != nil {
// return err
// }
// 写入ETCD
_, err = sr.cli.Put(ctx, key, value, clientv3.WithLease(sr.leaseID))
if err != nil {
return err
}
// 启动自动续租
go sr.keepAlive(ctx)
if err = sr.discoverServices(); err == nil {
// ss := sr.GetService()
// for _, s := range ss {
// log.Debug(fmt.Sprintf("Discovered services: %+v\n", s))
// }
}
return nil
}
// 自动续租逻辑
func (sr *etcdRegistryImpl) keepAlive(ctx context.Context) {
retryCount := 0
ticker := time.NewTicker(DefaultKeepAliveInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_, err := sr.cli.KeepAliveOnce(ctx, sr.leaseID)
if err != nil {
if retryCount > KeepAliveFailCount {
log.DebugF("KeepAlive failed after %d retries: %v", KeepAliveFailCount, err)
sr.UnregisterService()
return
}
retryCount++
// log.DebugF("KeepAlive error (retry %d/%d): %v", retryCount, KeepAliveFailCount, err)
} else {
retryCount = 0
}
case <-ctx.Done():
return
}
}
}
// 反注册服务
func (sr *etcdRegistryImpl) UnregisterService() {
if sr.cancelFunc != nil {
sr.cancelFunc()
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if _, err := sr.cli.Delete(ctx, sr.nodeKey); err != nil {
log.ErrorF("unregister:%v failed:%v from etcd", sr.nodeKey, err)
} else {
log.DebugF("unregister:%v from etcd", sr.nodeKey)
}
}
// 服务发现
func (sr *etcdRegistryImpl) discoverServices() error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
srv := fmt.Sprintf("/%s/", sr.rootKey)
resp, err := sr.cli.Get(ctx, srv, clientv3.WithPrefix())
if err != nil {
return err
}
// log.Debug(fmt.Sprintf("discoverServices srv:%s", srv))
for _, kv := range resp.Kvs {
sr.saveNodeFunc(kv.Value)
}
return nil
}
// 监控服务变化
func (sr *etcdRegistryImpl) WatchServices() {
watchKey := fmt.Sprintf("/%s/", sr.rootKey)
ksync.GoSafe(func() {
rch := sr.cli.Watch(context.Background(), watchKey, clientv3.WithPrefix())
for resp := range rch {
for range resp.Events {
// 当有变化时获取最新服务列表
if err := sr.discoverServices(); err != nil {
log.Error(err.Error())
}
}
}
}, nil)
}

91
etcd/etcd_test.go Normal file
View File

@ -0,0 +1,91 @@
package etcd
import (
"github.com/fox/fox/log"
"testing"
"time"
)
const (
etcdAddress1 = "192.168.232.128:2379"
etcdAddress2 = "114.132.124.145:2379"
)
/*
sudo docker run -d \
--name my-etcd \
-p 2379:2379 \
-p 2380:2380 \
quay.io/coreos/etcd:v3.6.0 \
etcd \
--name etcd-single \
--data-dir /etcd-data \
--initial-advertise-peer-urls http://0.0.0.0:2380 \
--listen-peer-urls http://0.0.0.0:2380 \
--advertise-client-urls http://0.0.0.0:2379 \
--listen-client-urls http://0.0.0.0:2379 \
--initial-cluster etcd-single=http://0.0.0.0:2380
sudo docker run -d --name etcdkeeper -p 8080:8080 evildecay/etcdkeeper
*/
func TestService(t *testing.T) {
_ = etcdAddress1
_ = etcdAddress2
log.Open("test.log", log.DebugL)
// 创建注册中心
registry, err := NewRegistry[ServiceNode]([]string{etcdAddress2}, rootKeyServices, "", "")
if err != nil {
log.Fatal(err.Error())
}
// 注册示例服务
service := &ServiceNode{
Name: "instance-1",
Type: "user-service",
Address: "localhost",
Port: 8080,
}
if err := registry.Register(service); err != nil {
log.Fatal(err.Error())
}
// 监控服务变化
registry.WatchServices()
time.Sleep(10 * time.Second)
registry.UnregisterService()
}
func TestTopicRegistry(t *testing.T) {
_ = etcdAddress1
_ = etcdAddress2
log.Open("test.log", log.DebugL)
// 创建注册中心
registry, err := NewRegistry[TopicNode]([]string{etcdAddress2}, rootKeyTopic, "", "")
if err != nil {
log.Fatal(err.Error())
}
// 注册示例服务
node := &TopicNode{
Name: "instance-1",
Creator: "instance-1",
}
if err := registry.Register(node); err != nil {
log.Fatal(err.Error())
}
// 监控服务变化
registry.WatchServices()
for _, n := range registry.GetNodes() {
log.DebugF("发现topic:%v, 创建者:%v", n.Name, n.Creator)
if v, err := registry.FindNode(n.MapKey()); err == nil {
log.DebugF("topic:%v exist", v.Name)
}
}
time.Sleep(60 * time.Second)
registry.UnregisterService()
}

53
etcd/inode.go Normal file
View File

@ -0,0 +1,53 @@
package etcd
import "fmt"
const (
rootKeyServices = "services"
rootKeyTopic = "topic"
)
type INode interface {
// 注册到etcd的key
EtcdKey() string
EtcdRootKey() string
MapKey() string
}
type ServiceNode struct {
Name string `json:"name"` // 服务名 多个同类服务依赖name区分:1,2,3等等
Type string `json:"type"` // 服务类型:lobby, game, gate等等
Address string `json:"address"` // 地址
Port int `json:"port"` // 端口
Version string `json:"version"` // 版本号
ServiceType ServiceType `json:"service_type"` // 服务类型
}
func (s ServiceNode) EtcdKey() string {
return fmt.Sprintf("/%s/%s/%s", rootKeyServices, s.Type, s.Name)
}
func (s ServiceNode) EtcdRootKey() string {
return rootKeyServices
}
func (s ServiceNode) MapKey() string {
return fmt.Sprintf("%s-%s", s.Type, s.Name)
}
type TopicNode struct {
Name string `json:"name"` // topic名
Creator string `json:"creator"` // topic创建者
}
func (s TopicNode) EtcdKey() string {
return fmt.Sprintf("/%s/%s/%s", rootKeyTopic, s.Creator, s.Name)
}
func (s TopicNode) EtcdRootKey() string {
return rootKeyTopic
}
func (s TopicNode) MapKey() string {
return fmt.Sprintf("%s", s.Name)
}

19
etcd/serviceType.go Normal file
View File

@ -0,0 +1,19 @@
package etcd
type ServiceType int
const (
Unique ServiceType = 1 // 唯一
Multiple ServiceType = 2 // 多个
)
func (s ServiceType) String() string {
switch s {
case Unique:
return "unique"
case Multiple:
return "multiple"
default:
return "unknown"
}
}

67
go.mod Normal file
View File

@ -0,0 +1,67 @@
module github.com/fox/fox
go 1.23.0
toolchain go1.23.7
require (
github.com/ClickHouse/clickhouse-go/v2 v2.34.0
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-module/carbon/v2 v2.6.5
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/nats-io/nats.go v1.42.0
github.com/nsqio/go-nsq v1.1.0
github.com/orcaman/concurrent-map/v2 v2.0.1
github.com/rabbitmq/amqp091-go v1.10.0
github.com/stretchr/testify v1.10.0
github.com/wanghuiyt/ding v0.0.2
go.etcd.io/etcd/client/v3 v3.5.19
go.uber.org/zap v1.27.0
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.26.1
)
require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/ClickHouse/ch-go v0.65.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dromara/carbon/v2 v2.6.5 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.19 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.19 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

198
go.sum Normal file
View File

@ -0,0 +1,198 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU=
github.com/ClickHouse/ch-go v0.65.1/go.mod h1:bsodgURwmrkvkBe5jw1qnGDgyITsYErfONKAHn05nv4=
github.com/ClickHouse/clickhouse-go/v2 v2.34.0 h1:Y4rqkdrRHgExvC4o/NTbLdY5LFQ3LHS77/RNFxFX3Co=
github.com/ClickHouse/clickhouse-go/v2 v2.34.0/go.mod h1:yioSINoRLVZkLyDzdMXPLRIqhDvel8iLBlwh6Iefso8=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
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/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/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/dromara/carbon/v2 v2.6.5 h1:OC1k8zGBpSnRoPjezlWeajx+3nCMq7xhZqAS4WWrKmE=
github.com/dromara/carbon/v2 v2.6.5/go.mod h1:7GXqCUplwN1s1b4whGk2zX4+g4CMCoDIZzmjlyt0vLY=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-module/carbon/v2 v2.6.5 h1:C3YpydJZmo77AyMsgQ1QpVDQeLJ0itOa5sQB4Y/jk4I=
github.com/golang-module/carbon/v2 v2.6.5/go.mod h1:JvSYEoe3+OcMnSQyRvUNQFpg0T/4xCq7moHu1S8ESXQ=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=
github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/wanghuiyt/ding v0.0.2 h1:6ZISlgCSy6MVeaFR8kAdniALMRqd56GyO9LlmYdTw/s=
github.com/wanghuiyt/ding v0.0.2/go.mod h1:T1vPz74YMmGCBVKZzVsen/YAYRZ2bvBYXldUyD7Y4vc=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/etcd/api/v3 v3.5.19 h1:w3L6sQZGsWPuBxRQ4m6pPP3bVUtV8rjW033EGwlr0jw=
go.etcd.io/etcd/api/v3 v3.5.19/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U=
go.etcd.io/etcd/client/pkg/v3 v3.5.19 h1:9VsyGhg0WQGjDWWlDI4VuaS9PZJGNbPkaHEIuLwtixk=
go.etcd.io/etcd/client/pkg/v3 v3.5.19/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0=
go.etcd.io/etcd/client/v3 v3.5.19 h1:+4byIz6ti3QC28W0zB0cEZWwhpVHXdrKovyycJh1KNo=
go.etcd.io/etcd/client/v3 v3.5.19/go.mod h1:FNzyinmMIl0oVsty1zA3hFeUrxXI/JpEnz4sG+POzjU=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.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/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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

76
ksync/ksync.go Normal file
View File

@ -0,0 +1,76 @@
package ksync
import (
"fmt"
"github.com/fox/fox/log"
"github.com/wanghuiyt/ding"
"runtime/debug"
"strings"
)
// 辅助函数列表
var (
DingAccessToken = "" // 钉钉token
DingSecret = "" // 钉钉加签
lastAttributeCode = "" // 最近一条error信息
)
func getAttributeCode(stackErr string) string {
lines := strings.Split(stackErr, "\n")
// 检查是否有足够的行数并提取第9行索引为8因为索引从0开始
if len(lines) >= 9 {
goIndex := strings.LastIndex(lines[8], ".go")
if goIndex != -1 {
filteredStr := lines[8][:goIndex]
filteredStr = strings.TrimLeft(filteredStr, "\t ")
return filteredStr
}
}
return ""
}
// 发送消息给钉钉
func sendMessage(msg interface{}, stackErr string, dingToken, dingSecret string) {
if dingToken != "" {
d := ding.Webhook{
AccessToken: dingToken, // 上面获取的 access_token
Secret: dingSecret, // 上面获取的加签的值
}
_ = d.SendMessageText(fmt.Sprintf("Recover panic:%v\n%v", msg, stackErr), "13145922265", "17353003985")
}
}
func Recover(recoverFunc func()) {
if msg := recover(); msg != nil {
stackErr := string(debug.Stack())
attributeCode := getAttributeCode(stackErr)
if lastAttributeCode != attributeCode {
lastAttributeCode = attributeCode
log.ErrorF("Recover panic:%v", msg)
log.Error(stackErr)
sendMessage(msg, stackErr, DingAccessToken, DingSecret)
}
if recoverFunc != nil {
recoverFunc()
}
}
}
// RunSafe runs the given fn, recovers if fn panics.
func RunSafe(fn func(), recoverFunc func()) {
defer Recover(recoverFunc)
fn()
}
// GoSafe runs the given fn using another goroutine, recovers if fn panics.
func GoSafe(fn func(), recoverFunc func()) {
go RunSafe(fn, recoverFunc)
}
// WrapSafe return a RunSafe wrap func
func WrapSafe(fn func(), recoverFunc func()) func() {
return func() {
RunSafe(fn, recoverFunc)
}
}

34
ksync/ksync_test.go Normal file
View File

@ -0,0 +1,34 @@
package ksync
import (
"fmt"
"github.com/fox/fox/log"
"github.com/wanghuiyt/ding"
"testing"
"time"
)
func TestEtcd(t *testing.T) {
log.Open("test.log", log.DebugL)
WrapSafe(func() { fmt.Println("hello world") }, nil)
}
func TestSendDingTalkMessage(t *testing.T) {
d := ding.Webhook{
AccessToken: "9802639f5dea7dd4aaff98a8f264b98c224f90ceeed3c26b438534e3a79222b1", // 上面获取的 access_token
Secret: "SECffd7d2afe9d0590fd04e36a0efa44d73553fb8f702eb1690e579996aec6c1386", // 上面获取的加签的值
}
_ = d.SendMessageText("这是普通的群消息", "13145922265", "17353003985")
}
func TestRecover(t *testing.T) {
log.Open("test.log", log.DebugL)
cb := func() {
i := new(int)
*i = 1
i = nil
*i = 2
}
GoSafe(cb, cb)
time.Sleep(time.Minute)
}

156
log/log.go Normal file
View File

@ -0,0 +1,156 @@
package log
import (
"fmt"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"runtime"
"time"
)
var (
logger *zap.Logger
file *os.File
)
const (
DebugL = zapcore.DebugLevel
InfoL = zapcore.InfoLevel
WarnL = zapcore.WarnLevel
ErrorL = zapcore.ErrorLevel
)
func Open(filepath string, level zapcore.Level) {
if level < DebugL || level > ErrorL {
level = DebugL
}
// 自定义时间编码器,不显示时区
customTimeEncoder := func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.000")) // 使用不含时区的格式
}
// 配置lumberjack
file := &lumberjack.Logger{
Filename: filepath, // 日志文件的位置
MaxSize: 100, // 每个日志文件保存的最大尺寸 单位MB
MaxBackups: 3, // 日志文件最多保存多少个备份
MaxAge: 28, // 文件最多保存多少天
Compress: true, // 是否压缩/归档旧文件
}
// var err error
// // 打开日志文件
// file, err = os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
// if err != nil {
// zap.L().Fatal("无法打开日志文件", zap.Error(err))
// }
// 设置编码器配置
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: customTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
// 创建控制台输出
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
consoleWriter := zapcore.AddSync(os.Stdout)
consoleCore := zapcore.NewCore(consoleEncoder, consoleWriter, zap.NewAtomicLevelAt(zap.DebugLevel))
// 创建文件输出
fileEncoder := zapcore.NewJSONEncoder(encoderConfig)
fileWriter := zapcore.AddSync(file)
fileCore := zapcore.NewCore(fileEncoder, fileWriter, zap.NewAtomicLevelAt(level))
// 将两个 Core 组合成一个 MultiWriteSyncer
core := zapcore.NewTee(consoleCore, fileCore)
// 创建logger对象
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
// Info = logger.Info
// Debug = logger.Debug
// Warn = logger.Warn
// Error = logger.Error
// Fatal = logger.Fatal
}
func Close() {
if logger != nil {
_ = logger.Sync()
logger = nil
}
if file != nil {
_ = file.Close()
file = nil
}
}
func StackTrace() string {
var pcs [32]uintptr
n := runtime.Callers(3, pcs[:]) // 跳过前3个栈帧即printStackTrace, caller, 和runtime.Callers本身
frames := runtime.CallersFrames(pcs[:n])
s := ""
for {
frame, more := frames.Next()
s += fmt.Sprintf("%+v\n", frame)
if !more {
break
}
}
return s
}
func InfoF(format string, args ...any) {
logger.Info(fmt.Sprintf(format, args...))
}
func Info(msg string, fields ...zap.Field) {
logger.Info(msg, fields...)
}
func Debug(msg string, fields ...zap.Field) {
logger.Debug(msg, fields...)
}
func DebugF(format string, args ...any) {
logger.Debug(fmt.Sprintf(format, args...))
}
func Warn(msg string, fields ...zap.Field) {
logger.Warn(msg, fields...)
}
func WarnF(format string, args ...any) {
logger.Info(fmt.Sprintf(format, args...))
}
func Error(msg string, fields ...zap.Field) {
logger.Error(msg, fields...)
}
func ErrorF(format string, args ...any) {
logger.Error(fmt.Sprintf(format, args...))
}
func Fatal(msg string, fields ...zap.Field) {
logger.Fatal(msg, fields...)
}
func FatalF(format string, args ...any) {
logger.Fatal(fmt.Sprintf(format, args...))
}
// 提供logger方便其它库打印正确的日志调用点
func GetLogger() *zap.Logger {
return logger
}

44
log/log_test.go Normal file
View File

@ -0,0 +1,44 @@
package log
import (
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"testing"
)
func testLogger(t *testing.T, level zapcore.Level) {
_ = t
Open("test.log", level)
defer Close()
Debug("debug. ", zap.String("name", "liu"))
Info("info. ", zap.Error(fmt.Errorf("this is an error")))
Warn("warn.", zap.Int64("int64", 111))
Error("error.", zap.Bool("bool", true))
Fatal("fatal. ", zap.String("name", "liu"))
DebugF("debugF: %.2f", 1.23)
InfoF("infoF: %.2f", 1.23)
WarnF("warnF: %.2f", 1.23)
ErrorF("errorF: %.2f", 1.23)
FatalF("fatalF: %.2f", 1.23)
}
func TestLogger(t *testing.T) {
testLogger(t, DebugL)
testLogger(t, InfoL)
testLogger(t, WarnL)
testLogger(t, ErrorL)
}
func testStackTrace(t *testing.T, level zapcore.Level) {
_ = t
Open("test.log", level)
defer Close()
stack := StackTrace()
Debug(stack)
}
func TestStackTrace(t *testing.T) {
testStackTrace(t, DebugL)
}

147
nat/nats.go Normal file
View File

@ -0,0 +1,147 @@
package nat
import (
"fmt"
"github.com/fox/fox/log"
"github.com/fox/fox/safeChan"
"github.com/nats-io/nats.go"
"sync"
"time"
)
type RpcHandler func(msg []byte) ([]byte, error)
type Nats struct {
address []string
nc *nats.Conn
sub []*nats.Subscription
name string
mt sync.Mutex
}
// []string{
// "nats://server1:4222",
// "nats://server2:4222",
// "nats://server3:4222",
// }
func NewNats(name string, address ...string) *Nats {
n := &Nats{
address: address,
nc: nil,
sub: nil,
name: name,
mt: sync.Mutex{},
}
return n
}
func (n *Nats) Connect() error {
opts := nats.GetDefaultOptions()
opts.Servers = n.address
opts.AllowReconnect = true
opts.MaxReconnect = 10
opts.ReconnectWait = 5 * time.Second
opts.Name = n.name
opts.ClosedCB = func(conn *nats.Conn) {
_ = conn
log.Info("nats 连接已关闭")
}
opts.DisconnectedErrCB = func(conn *nats.Conn, err error) {
if err != nil {
log.ErrorF("nats 连接断开, err:%v", err)
}
}
nc, err := opts.Connect()
if err != nil {
return err
}
n.nc = nc
log.InfoF("连接nats成功当前服务器:%v", nc.ConnectedUrl())
return nil
}
func (n *Nats) Close() {
if n.nc != nil {
n.nc.Close()
}
n.clearAllSub()
}
func (n *Nats) SubscribeRpc(topic string, rpcHandler RpcHandler) error {
if rpcHandler == nil {
return fmt.Errorf("rpc handler is nil")
}
rspErrF := func(m *nats.Msg) {
if err := m.Respond([]byte("error")); err != nil {
log.Error(err.Error())
}
}
sub, err := n.nc.Subscribe(topic, func(m *nats.Msg) {
rsp, err := rpcHandler(m.Data)
if err != nil {
log.Error(err.Error())
rspErrF(m)
return
}
if err = m.Respond(rsp); err != nil {
log.Error(err.Error())
}
})
if err != nil {
return err
}
n.addSub(sub)
return nil
}
func (n *Nats) Subscribe(topic string, msgChan *safeChan.ByteChan) error {
sub, err := n.nc.Subscribe(topic, func(m *nats.Msg) {
_ = msgChan.Write(m.Data)
})
if err != nil {
return err
}
n.addSub(sub)
return nil
}
func (n *Nats) Publish(topic string, msg []byte) error {
return n.nc.Publish(topic, msg)
}
func (n *Nats) Rpc(topic string, msg []byte) ([]byte, error) {
rsp, err := n.nc.Request(topic, msg, 30*time.Second)
if err != nil {
return nil, err
}
return rsp.Data, nil
}
// 队列订阅,队列中只会有一个消费者消费该消息
func (n *Nats) QueueSubscribe(topic string, queue string, msgChan *safeChan.ByteChan) error {
sub, err := n.nc.QueueSubscribe(topic, queue, func(m *nats.Msg) {
_ = msgChan.Write(m.Data)
})
if err != nil {
return err
}
n.addSub(sub)
return nil
}
func (n *Nats) addSub(sub *nats.Subscription) {
n.mt.Lock()
defer n.mt.Unlock()
n.sub = append(n.sub, sub)
}
func (n *Nats) clearAllSub() {
n.mt.Lock()
defer n.mt.Unlock()
for _, sub := range n.sub {
_ = sub.Unsubscribe()
}
n.sub = n.sub[:0]
}

117
nat/nats_test.go Normal file
View File

@ -0,0 +1,117 @@
package nat
import (
"fmt"
"github.com/fox/fox/ksync"
"github.com/fox/fox/log"
"github.com/fox/fox/safeChan"
"sync/atomic"
"testing"
"time"
)
const (
nats1 = "nats://192.168.232.128:4222"
nats2 = "nats://114.132.124.145:4222"
)
func initLog() {
_ = nats1
_ = nats2
log.Open("test.log", log.DebugL)
}
/*
docker pull nats:latest
docker run -d --name my-nats -p 4222:4222 -p 8222:8222 nats
*/
func TestNats(t *testing.T) {
initLog()
n := NewNats("test", nats2)
if err := n.Connect(); err != nil {
t.Log(err)
return
}
topic := "test.topic"
for i := 0; i < 2; i++ {
msgChan := safeChan.NewSafeChan[[]byte](128)
if err := n.Subscribe(topic, msgChan); err != nil {
t.Log(err)
return
}
ksync.GoSafe(func() {
for {
select {
case msg, ok := <-msgChan.Reader():
if !ok {
return
}
t.Log("consumer:", i, string(msg))
}
}
}, nil)
}
count := 0
for {
count++
_ = n.Publish(topic, []byte(fmt.Sprintf("hello nats:%v", count)))
if count > 4 {
break
}
}
time.Sleep(3 * time.Second)
n.Close()
}
func TestQueue(t *testing.T) {
initLog()
n := NewNats("test", "nats://192.168.232.128:4222")
if err := n.Connect(); err != nil {
t.Log(err)
return
}
topic := "test.group"
queue := "test.queue"
count2 := int32(0)
for i := 0; i < 3; i++ {
msgChan := safeChan.NewSafeChan[[]byte](128)
if err := n.QueueSubscribe(topic, queue, msgChan); err != nil {
t.Log(err)
return
}
ksync.GoSafe(func() {
for {
select {
case msg, ok := <-msgChan.Reader():
if !ok {
return
}
_ = atomic.AddInt32(&count2, 1)
_ = msg
// t.Log("consumer:", i, string(msg))
}
}
}, nil)
}
count := int32(0)
for {
count++
_ = n.Publish(topic, []byte(fmt.Sprintf("hello nats:%v", count)))
if count > 900000 {
break
}
}
time.Sleep(10 * time.Second)
c := atomic.LoadInt32(&count2)
if c == count {
t.Log("count==count2==", c)
} else {
t.Log("count:", count, " count2:", count2)
}
n.Close()
}

128
nsq/consumer.go Normal file
View File

@ -0,0 +1,128 @@
package nsq
import (
"fmt"
"github.com/fox/fox/log"
"github.com/nsqio/go-nsq"
"time"
)
const (
chanLen = 20 // 消息队列容量
)
type chanState int
const (
idle chanState = 0
busy chanState = 1
)
func (ch chanState) String() string {
switch ch {
case idle:
return "idle"
case busy:
return "busy"
default:
return "unknown"
}
}
type Consumer struct {
name string
addr string
topic string
channel string
msgChan chan *nsq.Message
consumer *nsq.Consumer
state chanState
}
func newConsumer(nsqAddr, topic, channel string) (*Consumer, error) {
var err error
config := nsq.NewConfig()
// 心跳设置
config.HeartbeatInterval = 30 * time.Second
// 关键配置项
// config.DialTimeout = 10 * time.Second // TCP 连接超时(默认系统级)
// config.ReadTimeout = 60 * time.Second // 读取数据超时
// config.WriteTimeout = 60 * time.Second // 写入数据超时
//
// // 如果是通过 nsqlookupd 发现服务,调整 HTTP 客户端超时
// config.LookupdPollInterval = 5 * time.Second // 轮询间隔
// config.LookupdPollTimeout = 10 * time.Second // 单个 HTTP 请求超时
c := &Consumer{}
c.addr = nsqAddr
c.state = idle
c.topic = topic
c.channel = channel
c.name = fmt.Sprintf("%v-%v", topic, channel)
c.msgChan = make(chan *nsq.Message, chanLen)
c.consumer, err = nsq.NewConsumer(topic, channel, config)
if err != nil {
return nil, err
}
c.consumer.AddHandler(c)
c.consumer.SetLogger(newNSQLogger(nsq.LogLevelError), nsq.LogLevelError)
return c, nil
}
// 创建消费者直连NSQd发现服务
func NewConsumerByNsqD(nsqDAddr, topic, channel string) (*Consumer, error) {
c, err := newConsumer(nsqDAddr, topic, channel)
if err != nil {
return nil, err
}
// 连接方式选择直连NSQd
if err = c.consumer.ConnectToNSQD(nsqDAddr); err != nil {
return nil, err
}
log.DebugF("consumer %v-%v 注册成功", topic, channel)
return c, err
}
// 创建消费者连接NSQLookupD发现服务
func NewConsumer(lookupAddr, topic, channel string) (*Consumer, error) {
c, err := newConsumer(lookupAddr, topic, channel)
if err != nil {
return nil, err
}
// 连接NSQLookupD发现服务
err = c.consumer.ConnectToNSQLookupd(c.addr)
if err != nil {
return nil, err
}
log.DebugF("consumer %v-%v 注册成功", topic, channel)
return c, err
}
func (c *Consumer) HandleMessage(msg *nsq.Message) error {
num := len(c.msgChan)
if num > chanLen/2 && c.state == idle {
c.state = busy
log.WarnF("%v-%v 通道已从闲时转为繁忙状态。当前积压消息数:%v", c.topic, c.channel, len(c.msgChan))
} else if c.state == busy && num < chanLen/4 {
c.state = idle
log.WarnF("%v-%v 通道已从繁忙转为闲时状态。当前积压消息数:%v", c.topic, c.channel, len(c.msgChan))
}
c.msgChan <- msg
msg.Finish()
return nil
}
func (c *Consumer) Name() string {
return c.name
}
func (c *Consumer) Read() <-chan *nsq.Message {
return c.msgChan
}
func (c *Consumer) Close() {
if c.consumer != nil {
c.consumer.Stop()
}
}

93
nsq/nsq_test.go Normal file
View File

@ -0,0 +1,93 @@
package nsq
import (
"fmt"
"github.com/fox/fox/log"
"github.com/nsqio/go-nsq"
"go.uber.org/zap"
"testing"
"time"
)
const (
testTopic = "test_topic"
testChannel = "test_channel"
testChannelD = "test_channelD"
testAddress = "192.168.232.128:4150"
testLookupAddress = "192.168.232.128:4161"
)
func initLog() {
log.Open("test.log", log.DebugL)
}
func toString(id nsq.MessageID) string {
sid := ""
for _, b := range id {
sid += string(b)
}
return sid
}
func testProducer() *Producer {
// 初始化一个生产者
producer, err := NewProducer(testAddress)
if err != nil {
log.Fatal(err.Error())
return nil
}
for n := 0; n <= 20; n++ {
err = producer.Publish(testTopic, []byte(fmt.Sprintf("hello nsq %d", n)))
if err != nil {
log.Fatal(err.Error())
return nil
}
// time.Sleep(1 * time.Second)
}
return producer
}
func testConsumers() {
consumer, err := NewConsumerByNsqD(testAddress, testTopic, testChannel)
if err != nil {
log.Fatal(err.Error())
return
}
defer consumer.Close()
time.Sleep(2 * time.Second)
for {
select {
case msg := <-consumer.Read():
log.Debug(consumer.Name(), zap.String("id", toString(msg.ID)), zap.String("body", string(msg.Body)))
}
}
}
func testConsumerByLookupD() {
consumer, err := NewConsumer(testLookupAddress, testTopic, testChannelD)
if err != nil {
log.Fatal(err.Error())
return
}
defer consumer.Close()
time.Sleep(2 * time.Second)
for {
select {
case msg := <-consumer.Read():
log.Debug(consumer.name, zap.String("id", toString(msg.ID)), zap.String("body", string(msg.Body)))
}
}
}
func TestNsq(t *testing.T) {
initLog()
// go testConsumers()
// go testConsumerByLookupD()
producer := testProducer()
if producer != nil {
defer producer.Close()
}
time.Sleep(5 * time.Second)
log.Debug("shutdown")
}

31
nsq/nsqlog.go Normal file
View File

@ -0,0 +1,31 @@
package nsq
import (
"github.com/fox/fox/log"
"github.com/nsqio/go-nsq"
)
type nsqLogger struct {
level nsq.LogLevel
}
func newNSQLogger(level nsq.LogLevel) *nsqLogger {
return &nsqLogger{level}
}
func (l *nsqLogger) Output(callDepth int, s string) error {
_ = callDepth
switch l.level {
case nsq.LogLevelDebug:
log.GetLogger().Debug(s)
case nsq.LogLevelInfo:
log.GetLogger().Error(s)
case nsq.LogLevelWarning:
log.GetLogger().Error(s)
case nsq.LogLevelError:
log.GetLogger().Error(s)
default:
log.GetLogger().Error(s)
}
return nil
}

76
nsq/producer.go Normal file
View File

@ -0,0 +1,76 @@
package nsq
import (
"errors"
"fmt"
"github.com/nsqio/go-nsq"
"time"
)
type Producer struct {
nsqAddr []string
config *nsq.Config
producer *nsq.Producer
size int
pos int
}
func initProducer(p *Producer, nsqAddr string) error {
var err error
p.producer, err = nsq.NewProducer(nsqAddr, p.config)
if err != nil {
return err
}
p.producer.SetLogger(newNSQLogger(nsq.LogLevelError), nsq.LogLevelError)
return nil
}
// 创建建生产者 nsqAddr:127.0.0.1:4150, 127.0.0.2:4150
func NewProducer(nsqAddr ...string) (*Producer, error) {
var err error
p := new(Producer)
p.config = nsq.NewConfig()
// 心跳设置
p.config.HeartbeatInterval = 30 * time.Second
p.nsqAddr = nsqAddr
p.size = len(nsqAddr)
p.pos = 0
if len(nsqAddr) == 0 {
return nil, errors.New("nsqAddr is empty")
}
err = initProducer(p, p.nsqAddr[0])
return p, err
}
// 向主题发布消息
func (p *Producer) Publish(topic string, data []byte) error {
var err error
for {
if p.producer != nil {
err = p.producer.Publish(topic, data)
if err == nil {
break
}
// 切换ip重发
p.producer.Stop()
}
p.pos = p.pos + 1
if p.pos < p.size {
err = initProducer(p, p.nsqAddr[p.pos])
} else {
p.pos = 0
return fmt.Errorf("连接nsq%v 失败", p.nsqAddr)
}
}
return nil
}
func (p *Producer) Close() {
if p.producer != nil {
p.producer.Stop()
p.producer = nil
}
}

111
rmq/rmq.go Normal file
View File

@ -0,0 +1,111 @@
package rmq
import (
"context"
"github.com/fox/fox/log"
"github.com/google/uuid"
"github.com/rabbitmq/amqp091-go"
"time"
)
const (
ExchangeDirect = "direct"
// ExchangeFanout = "fanout"
ExchangeTopic = "topic"
)
type Rmq struct {
conn *amqp091.Connection
ch *amqp091.Channel
consumerTag string
}
// url:amqp://guest:guest@localhost:5672/
func NewRmq(url string) (*Rmq, error) {
rmq := &Rmq{consumerTag: uuid.NewString()}
retries := 0
retryDelay := 1 * time.Second
var err error
for {
rmq.conn, err = amqp091.DialConfig(url, amqp091.Config{Heartbeat: 10 * time.Second})
if err == nil {
break
}
retries++
time.Sleep(retryDelay)
if retryDelay < 30*time.Second {
retryDelay *= 2
}
log.ErrorF("amqp connection failed after %v reconnect.err:%v url:%v", retryDelay, err, url)
}
rmq.ch, err = rmq.conn.Channel()
if err != nil {
return nil, err
}
return rmq, nil
}
func (r *Rmq) Close() {
if r.conn != nil {
_ = r.conn.Close()
}
if r.ch != nil {
_ = r.ch.Close()
}
}
func (r *Rmq) ExchangeDeclare(exchangeName, typ string) error {
return r.ch.ExchangeDeclare(exchangeName, typ, true, false, false, false, nil)
}
func (r *Rmq) QueueDeclare(queueName string) error {
_, err := r.ch.QueueDeclare(queueName, false, false, false, false, nil)
return err
}
func (r *Rmq) QueueDelete(queueName string) error {
_, err := r.ch.QueueDelete(queueName, false, false, false)
return err
}
func (r *Rmq) QueueBind(queueName, routerKey, exchangeName string) error {
return r.ch.QueueBind(queueName, routerKey, exchangeName, false, nil)
}
func (r *Rmq) QueueUnbind(queueName, routerKey, exchangeName string) error {
return r.ch.QueueUnbind(queueName, routerKey, exchangeName, nil)
}
// 发布消息到交换机,带有指定的路由键
func (r *Rmq) Publish(exchangeName, routerKey string, msg []byte) error {
return r.ch.Publish(exchangeName, routerKey, true, false, amqp091.Publishing{
ContentType: "text/plain",
Body: msg,
})
}
// 发布消息到交换机,带有指定的路由键
func (r *Rmq) PublishRpc(d *amqp091.Delivery, msg []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return r.ch.PublishWithContext(ctx, "", d.ReplyTo, false, false, amqp091.Publishing{
ContentType: "text/plain",
CorrelationId: d.CorrelationId,
Body: msg,
})
}
func (r *Rmq) PublishRaw(exchangeName, routerKey string, data amqp091.Publishing) error {
return r.ch.Publish(exchangeName, routerKey, true, false, data)
}
func (r *Rmq) Consume(queueName string) (<-chan amqp091.Delivery, error) {
return r.ch.Consume(queueName, r.consumerTag, true, false, false, false, nil)
}
func (r *Rmq) ConsumeDelete() error {
return r.ch.Cancel(r.consumerTag, true)
}

53
rmq/rmq_test.go Normal file
View File

@ -0,0 +1,53 @@
package rmq
import (
"testing"
"time"
)
const url = "amqp://samba:samba@testbuild.shoa.com:5672/vh_samba"
const exchangeName = "test_e"
const queueName = "test_q"
func testCreateMq(t *testing.T) *Rmq {
mq, err := NewRmq(url)
if err != nil {
t.Error(err)
}
err = mq.ExchangeDeclare(exchangeName, ExchangeDirect)
if err != nil {
t.Error(err)
}
err = mq.QueueDeclare(queueName)
if err != nil {
t.Error(err)
}
return mq
}
func testPublish(t *testing.T, mq *Rmq) {
err := mq.Publish(exchangeName, queueName, []byte("hello world"))
if err != nil {
t.Error(err)
}
return
}
func testConsume(t *testing.T, mq *Rmq) {
msgs, err := mq.Consume(queueName)
if err != nil {
t.Error(err)
}
for msg := range msgs {
t.Log(string(msg.Body))
}
return
}
func TestRabbitmq(t *testing.T) {
mq := testCreateMq(t)
defer mq.Close()
go testConsume(t, mq)
testPublish(t, mq)
time.Sleep(2 * time.Second)
}

54
safeChan/safeChan.go Normal file
View File

@ -0,0 +1,54 @@
package safeChan
import (
"context"
"fmt"
"sync"
)
type ByteChan = SafeChan[[]byte]
type SafeChan[T any] struct {
ch chan T
ctx context.Context
cancel context.CancelFunc
once sync.Once
}
func NewSafeChan[T any](size int) *SafeChan[T] {
ch := &SafeChan[T]{}
ch.ctx, ch.cancel = context.WithCancel(context.Background())
if size < 1 {
ch.ch = make(chan T)
} else {
ch.ch = make(chan T, size)
}
// ch.ch = make(chan T, size)
return ch
}
func (s *SafeChan[T]) Close() {
s.once.Do(func() {
s.cancel()
close(s.ch)
})
}
// 管道中剩余数量
func (s *SafeChan[T]) Size() int {
return len(s.ch)
}
func (s *SafeChan[T]) Reader() <-chan T {
return s.ch
}
func (s *SafeChan[T]) Write(d T) error {
select {
case <-s.ctx.Done():
return fmt.Errorf("chan was closed")
default:
s.ch <- d
return nil
}
}

43
safeChan/safeChan_test.go Normal file
View File

@ -0,0 +1,43 @@
package safeChan
import (
"testing"
)
func TestSafeChan(t *testing.T) {
ch := NewSafeChan[string](12)
// go func() {
_ = ch.Write("hello")
t.Log("write hello. 剩余数量:", ch.Size())
// }()
ch.Close()
if err := ch.Write("zzz"); err != nil {
t.Log("write zzz err.", err)
}
breakNum := 0
for {
select {
case <-ch.ctx.Done():
t.Log("done")
breakNum++
case v, ok := <-ch.Reader():
if ok {
t.Log("read", v, " 剩余数量:", ch.Size())
} else {
t.Log("break")
breakNum++
}
default:
t.Log("panic")
breakNum++
}
if breakNum > 10 {
break
}
}
}

182
service/baseService.go Normal file
View File

@ -0,0 +1,182 @@
package service
import (
"context"
"fmt"
"github.com/fox/fox/ksync"
"github.com/fox/fox/log"
"github.com/fox/fox/safeChan"
"github.com/fox/fox/timer"
"time"
)
type BaseService struct {
*timer.Timer
type_ string
name string
onFunc IOnFunc
sender ISender
msg *safeChan.SafeChan[[]byte]
job *safeChan.SafeChan[func()]
stop context.Context
stopFunc context.CancelFunc
waitStop context.Context
waitStopFunc context.CancelFunc
}
func NewBaseService(type_, name string, onFunc IOnFunc, sender ISender) *BaseService {
s := &BaseService{
type_: type_,
name: name,
Timer: timer.NewTimer(),
onFunc: onFunc,
sender: sender,
msg: safeChan.NewSafeChan[[]byte](128),
job: safeChan.NewSafeChan[func()](128),
}
s.stop, s.stopFunc = context.WithCancel(context.Background())
s.waitStop, s.waitStopFunc = context.WithCancel(context.Background())
s.Run()
return s
}
func (s *BaseService) Name() string {
return s.name
}
func (s *BaseService) Type() string {
return s.type_
}
func (s *BaseService) Write(msg []byte) error {
return s.msg.Write(msg)
}
func (s *BaseService) RunOnce(cb func()) {
select {
case <-s.stop.Done():
log.Error(s.Log("want stop, can not call RunOnce function"))
return
default:
_ = s.job.Write(cb)
}
}
func (s *BaseService) RunWait(cb func() (retValue any)) (retValue any, err error) {
select {
case <-s.stop.Done():
err = fmt.Errorf(s.Log("want stop, can not call RunOnce function"))
log.Error(err.Error())
return nil, err
default:
wait := make(chan any, 2)
err = s.job.Write(func() {
retValue = cb()
wait <- retValue
})
if err == nil {
select {
case retValue = <-wait:
return retValue, nil
case <-time.After(time.Second * time.Duration(30)):
return nil, fmt.Errorf("timeout fail")
}
}
return nil, err
}
}
func (s *BaseService) Send(topic string, msg []byte) error {
if s.sender != nil {
return s.sender.Send(topic, msg)
}
return s.Err("send is nil")
}
func (s *BaseService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) {
if s.sender != nil {
return s.sender.Call(topic, timeout, msg)
}
return nil, s.Err("call is nil")
}
func (s *BaseService) WaitStop() {
select {
case <-s.waitStop.Done():
return
}
}
func (s *BaseService) NotifyStop() {
s.stopFunc()
// log.Debug(fmt.Sprintf("notify %v service stop", s.name))
}
func (s *BaseService) allChanEmpty() bool {
if s.job.Size() == 0 && s.msg.Size() == 0 {
return true
}
return false
}
func (s *BaseService) canStop() bool {
select {
case <-s.stop.Done():
if s.allChanEmpty() {
return true
}
return false
default:
return false
}
}
func (s *BaseService) run() {
for {
if s.canStop() {
if s.onFunc != nil {
s.onFunc.OnStop()
s.waitStopFunc()
}
break
}
select {
case msg, ok := <-s.msg.Reader():
if ok && s.onFunc != nil {
_ = s.onFunc.OnMessage(msg)
}
case cb, ok := <-s.job.Reader():
if ok && cb != nil {
cb()
}
case t, ok := <-s.Timer.Reader():
if ok && t != nil && t.Func != nil {
t.Func()
}
case _ = <-s.stop.Done():
if s.onFunc != nil {
s.msg.Close()
s.job.Close()
s.Timer.CancelAllTimer()
s.Timer.Close()
}
}
}
}
func (s *BaseService) Run() {
ksync.GoSafe(s.run, s.Run)
}
func (s *BaseService) Log(format string, a ...any) string {
head := fmt.Sprintf("service:%v-%v ", s.type_, s.name)
return head + fmt.Sprintf(format, a...)
}
func (s *BaseService) Err(format string, a ...any) error {
head := fmt.Sprintf("service:%v-%v ", s.type_, s.name)
return fmt.Errorf(head + fmt.Sprintf(format, a...))
}

35
service/iservice.go Normal file
View File

@ -0,0 +1,35 @@
package service
import (
"time"
)
type IService interface {
Name() string
Type() string
RunOnce(cb func())
RunWait(cb func() (retValue any)) (retValue any, err error)
NewTimer(duration time.Duration, cb func(), needLog bool, desc ...string) uint32
CancelTimer(timerId uint32)
CancelAllTimer()
// 向服务内部消息管道写入消息
Write(msg []byte) error
Send(topic string, msg []byte) error
Call(topic string, timeout time.Duration, msg []byte) ([]byte, error)
WaitStop()
NotifyStop()
}
type ISender interface {
Send(topic string, msg []byte) error
Call(topic string, timeout time.Duration, msg []byte) ([]byte, error)
}
type IOnFunc interface {
OnStop()
OnInit()
OnMessage(msg []byte) error
}

48
service/natsService.go Normal file
View File

@ -0,0 +1,48 @@
package service
import (
"fmt"
"github.com/fox/fox/nat"
"time"
)
type NatsService struct {
*BaseService
nats *nat.Nats
}
func NewNatsService(type_, name string, onFunc IOnFunc, natsAddress ...string) (*NatsService, error) {
s := new(NatsService)
s.BaseService = NewBaseService(type_, name, onFunc, s)
s.nats = nat.NewNats(fmt.Sprintf("%v-%v", type_, name), natsAddress...)
if err := s.nats.Connect(); err != nil {
// log.Error(err.Error())
s.BaseService.NotifyStop()
return nil, err
}
return s, nil
}
func (n *NatsService) Subscribe(topic string) error {
return n.nats.Subscribe(topic, n.msg)
}
// 队列订阅,队列中只会有一个消费者消费该消息
func (n *NatsService) QueueSubscribe(topic string, queue string) error {
return n.nats.QueueSubscribe(topic, queue, n.msg)
}
func (s *NatsService) OnStop() {
s.nats.Close()
}
func (s *NatsService) Send(topic string, msg []byte) error {
return s.nats.Publish(topic, msg)
}
func (s *NatsService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) {
_ = topic
_ = timeout
_ = msg
return nil, nil
}

View File

@ -0,0 +1,97 @@
package service
import (
"github.com/fox/fox/etcd"
"github.com/fox/fox/log"
"testing"
"time"
)
const (
GameSrv = "game"
NatsAddress = "nats://192.168.232.128:4222"
EtcdAddress = "192.168.232.128:2379"
EtcdAddress2 = "114.132.124.145:2379"
NatsAddress2 = "nats://114.132.124.145:4222"
)
func newGameService() *gameService {
_ = NatsAddress2
_ = NatsAddress
_ = EtcdAddress2
_ = EtcdAddress
var err error
s := new(gameService)
if s.NatsService, err = NewNatsService(GameSrv, "1", s, NatsAddress2); err != nil {
log.Fatal(err.Error())
return nil
}
if s.etcdService, err = etcd.NewRegistry[etcd.ServiceNode]([]string{EtcdAddress2}, etcd.ServiceNode{}.EtcdRootKey(), "", ""); err != nil {
log.Error(err.Error())
s.NatsService.OnStop()
return nil
}
endpoint := &etcd.ServiceNode{
Name: s.Name(),
Type: s.Type(),
Address: "",
Port: 0,
Version: "",
ServiceType: etcd.Unique,
}
if err = s.etcdService.Register(endpoint); err != nil {
log.Error(err.Error())
s.NatsService.OnStop()
return nil
}
s.OnInit()
return s
}
type gameService struct {
*NatsService
etcdService *etcd.Registry[etcd.ServiceNode]
etcdTopic *etcd.Registry[etcd.TopicNode]
srvTopic string
}
func (s *gameService) OnInit() {
s.etcdService.WatchServices()
s.etcdTopic.WatchServices()
if err := s.NatsService.Subscribe(Topic(s)); err != nil {
log.Error(err.Error())
}
if err := s.NatsService.QueueSubscribe(GroupTopic(s), GroupQueue(s)); err != nil {
log.Error(err.Error())
}
log.Debug("onInit")
}
func (s *gameService) OnStop() {
s.etcdService.UnregisterService()
s.etcdService.UnregisterService()
s.NatsService.OnStop()
log.Debug("OnStop")
}
func (s *gameService) OnMessage(msg []byte) error {
log.Debug(s.Log("on message:%v", string(msg)))
return nil
}
func TestGameService(t *testing.T) {
log.Open("test.log", log.DebugL)
s := newGameService()
msg := "hello world"
if err := s.Send(Topic(s), []byte(msg)); err != nil {
log.Error(err.Error())
}
for _, srv := range s.etcdService.GetNodes() {
log.Debug(s.Log("发现有服务:%v", srv))
}
time.Sleep(1 * time.Second)
s.NotifyStop()
s.WaitStop()
log.Debug("exit")
}

61
service/service_test.go Normal file
View File

@ -0,0 +1,61 @@
package service
import (
"github.com/fox/fox/log"
"testing"
"time"
)
const (
TestSrv = "test"
)
type EchoService struct {
*BaseService
}
func (s *EchoService) OnInit() {
log.Debug("onInit")
}
func (s *EchoService) OnStop() {
log.Debug("OnStop")
}
func (s *EchoService) Send(topic string, msg []byte) error {
log.Debug(s.Log("send %v to topic:%v", string(msg), topic))
return nil
}
func (s *EchoService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) {
_ = timeout
_ = msg
log.Debug(s.Log("call topic:%v", topic))
return nil, nil
}
func (s *EchoService) OnMessage(msg []byte) error {
log.Debug(s.Log("on message:%v", string(msg)))
return nil
}
func NewTestService() *EchoService {
s := new(EchoService)
s.BaseService = NewBaseService(TestSrv, "1", s, s)
s.OnInit()
return s
}
func TestService(t *testing.T) {
log.Open("test.log", log.DebugL)
s := NewTestService()
msg := "hello world"
_ = s.Write([]byte(msg))
s.RunOnce(func() {
time.Sleep(2 * time.Second)
})
s.NotifyStop()
s.WaitStop()
log.Debug("exit")
}

18
service/topic.go Normal file
View File

@ -0,0 +1,18 @@
package service
import (
"fmt"
)
// 每个服务都有自己的服务topic
func Topic(s IService) string {
return fmt.Sprintf("%v-%v.topic", s.Type(), s.Name())
}
func GroupTopic(s IService) string {
return s.Type() + ".topic"
}
func GroupQueue(s IService) string {
return s.Type() + ".group"
}

108
timer/timer.go Normal file
View File

@ -0,0 +1,108 @@
package timer
import (
"github.com/fox/fox/log"
"github.com/fox/fox/safeChan"
cmap "github.com/orcaman/concurrent-map/v2"
"math"
"sync/atomic"
"time"
)
type timerData struct {
timer *time.Timer
id uint32
Func func()
desc string
needLog bool
}
type Timer struct {
chTimer *safeChan.SafeChan[*timerData]
timers cmap.ConcurrentMap[uint32, *timerData]
no uint32
stop int32
}
func NewTimer() *Timer {
rt := &Timer{}
rt.chTimer = safeChan.NewSafeChan[*timerData](16)
rt.timers = cmap.NewWithCustomShardingFunction[uint32, *timerData](func(key uint32) uint32 { return key })
rt.no = math.MaxUint32 - 2
rt.stop = 0
return rt
}
func (rt *Timer) NewTimer(duration time.Duration, cb func(), needLog bool, desc ...string) uint32 {
tData := &timerData{
timer: nil,
id: atomic.AddUint32(&rt.no, 1),
Func: cb,
needLog: needLog,
}
if len(desc) > 0 {
tData.desc = desc[0]
}
t := time.AfterFunc(duration, func() {
_ = rt.chTimer.Write(tData)
rt.timers.Remove(tData.id)
if needLog {
if tData.desc != "" {
log.DebugF("移除定时器:%v NewTimer desc:%v time:%v", tData.id, tData.desc, duration)
} else {
log.DebugF("移除定时器:%v NewTimer", tData.id)
}
}
})
tData.timer = t
rt.timers.Set(tData.id, tData)
if needLog {
if tData.desc != "" {
log.DebugF("设置定时器:%v NewTimer desc:%v", tData.id, tData.desc)
} else {
log.DebugF("设置定时器:%v NewTimer", tData.id)
}
}
return tData.id
}
func (rt *Timer) CancelTimer(timerId uint32) {
if t, _ := rt.timers.Get(timerId); t != nil {
if t.timer != nil {
_ = t.timer.Stop()
}
rt.timers.Remove(timerId)
if t.needLog {
if t.desc != "" {
log.DebugF("移除定时器:%v CancelTimer desc:%v", t.id, t.desc)
} else {
log.DebugF("移除定时器:%v CancelTimer", t.id)
}
}
}
}
func (rt *Timer) CancelAllTimer() {
rt.timers.IterCb(func(_ uint32, t *timerData) {
if t.timer != nil {
t.timer.Stop()
if t.needLog {
if t.desc != "" {
log.DebugF("移除定时器:%v CancelAllTimer desc:%v", t.id, t.desc)
} else {
log.DebugF("移除定时器:%v CancelAllTimer", t.id)
}
}
}
})
rt.timers.Clear()
}
func (rt *Timer) Reader() <-chan *timerData {
return rt.chTimer.Reader()
}
func (rt *Timer) Close() {
rt.chTimer.Close()
}

51
timer/timer_test.go Normal file
View File

@ -0,0 +1,51 @@
package timer
import (
"github.com/fox/fox/ksync"
"testing"
"time"
)
func runTimerEvent(tm *Timer) {
ksync.GoSafe(func() {
for {
select {
case t, ok := <-tm.chTimer.Reader():
if ok && t != nil && t.Func != nil {
t.Func()
}
}
}
}, nil)
}
func TestTimer(t *testing.T) {
timer := NewTimer()
runTimerEvent(timer)
id := timer.NewTimer(1*time.Second, func() {
t.Log("this is timer1")
}, false)
t.Log("new timer:", id)
id = timer.NewTimer(2*time.Second, func() {
t.Log("this is timer2")
}, false)
t.Log("new timer:", id)
id = timer.NewTimer(3*time.Second, func() {
t.Log("this is timer3")
}, false)
t.Log("new timer:", id)
id = timer.NewTimer(1*time.Second, func() {
t.Log("this is timer4")
}, false)
t.Log("new timer:", id)
timer.CancelTimer(id)
t.Log("cancel timer:", id)
// fmt.Println("rrr id:", id)
time.Sleep(5 * time.Second)
t.Log("rest timer:", timer.timers.Count(), " count")
}

10
ws/iconn.go Normal file
View File

@ -0,0 +1,10 @@
package ws
type IConn interface {
Close()
SendMsg(data []byte) error
Name() string
Id() uint32
UserId() int64
Log(format string, v ...interface{}) string
}

41
ws/userMgr.go Normal file
View File

@ -0,0 +1,41 @@
package ws
import cmap "github.com/orcaman/concurrent-map/v2"
var userMgr = newUserManager()
type userManager struct {
users cmap.ConcurrentMap[int64, uint32]
}
func newUserManager() *userManager {
return &userManager{
users: cmap.NewWithCustomShardingFunction[int64, uint32](func(key int64) uint32 {
return uint32(key)
}),
}
}
func (m *userManager) Add(connId uint32, userId int64) bool {
if userId < 1 {
return false
}
if conn, ok := wsMgr.Get(connId); ok {
conn.setUserId(userId)
m.users.Set(userId, connId)
return true
}
return false
}
func (m *userManager) GetConnId(userId int64) uint32 {
connId, _ := m.users.Get(userId)
return connId
}
func (m *userManager) Remove(userId int64) {
if userId < 1 {
return
}
m.users.Remove(userId)
}

125
ws/wsClient.go Normal file
View File

@ -0,0 +1,125 @@
package ws
import (
"context"
"github.com/fox/fox/ksync"
"github.com/fox/fox/log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
type Client struct {
conn *websocket.Conn
sendChan chan *wsMessage
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
func NewClient(url string) (*Client, error) {
dialer := websocket.DefaultDialer
dialer.HandshakeTimeout = 30 * time.Second
conn, _, err := dialer.Dial(url, http.Header{"User-Agent": {"MyClient/1.0"}})
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
return &Client{
conn: conn,
sendChan: make(chan *wsMessage, 100),
ctx: ctx,
cancel: cancel,
}, nil
}
func (c *Client) Start() {
c.wg.Add(3)
ksync.GoSafe(c.readLoop, nil)
ksync.GoSafe(c.writeLoop, nil)
ksync.GoSafe(c.heartbeatLoop, nil)
}
func (c *Client) readLoop() {
defer c.wg.Done()
for {
select {
case <-c.ctx.Done():
return
default:
messageType, message, err := c.conn.ReadMessage()
if err != nil {
// log.Error(fmt.Sprintf("读取错误:%v", err))
c.Stop()
return
}
switch messageType {
case websocket.PingMessage:
c.sendChan <- &wsMessage{messageType: websocket.PongMessage, data: []byte("pong")}
case websocket.PongMessage:
case websocket.TextMessage, websocket.BinaryMessage:
log.DebugF("收到消息,类型:%v 内容:%v", messageType, string(message))
case websocket.CloseMessage:
log.Debug("收到关闭帧")
c.Stop()
return
}
}
}
}
func (c *Client) SendMsg(data []byte) {
c.sendChan <- &wsMessage{messageType: websocket.BinaryMessage, data: data}
}
func (c *Client) writeLoop() {
defer c.wg.Done()
for {
select {
case msg := <-c.sendChan:
switch msg.messageType {
case websocket.PingMessage:
_ = c.conn.WriteMessage(websocket.PingMessage, []byte("ping"))
case websocket.PongMessage:
_ = c.conn.WriteMessage(websocket.PongMessage, []byte("pong"))
default:
_ = c.conn.WriteMessage(msg.messageType, msg.data)
}
case <-c.ctx.Done():
// 发送关闭帧
_ = c.conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
time.Now().Add(10*time.Second),
)
return
}
}
}
func (c *Client) heartbeatLoop() {
defer c.wg.Done()
ticker := time.NewTicker(25 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.sendChan <- &wsMessage{messageType: websocket.PingMessage, data: []byte("ping")}
case <-c.ctx.Done():
return
}
}
}
func (c *Client) Stop() {
c.cancel()
_ = c.conn.Close()
c.wg.Wait()
}

194
ws/wsConn.go Normal file
View File

@ -0,0 +1,194 @@
package ws
import (
"encoding/binary"
"fmt"
"github.com/fox/fox/log"
"github.com/gorilla/websocket"
"sync"
"time"
)
// 连接id
var (
wsMsgType = websocket.BinaryMessage
nextConnId uint32
)
// 客户端读写消息
type wsMessage struct {
messageType int
data []byte
}
// 客户端连接
type wsConnect struct {
wsConn *websocket.Conn // 底层websocket
inChan chan *wsMessage // 读队列
outChan chan *wsMessage // 写队列
mutex sync.Mutex // 避免重复关闭管道,加锁处理
isClosed bool
closeCh chan struct{} // 关闭通知
id uint32
userId int64
onDisconnect func(IConn)
}
func newWsConnect(wsConn *websocket.Conn, onDisconnect func(IConn)) *wsConnect {
return &wsConnect{
wsConn: wsConn,
inChan: make(chan *wsMessage, 1000),
outChan: make(chan *wsMessage, 1000),
closeCh: make(chan struct{}),
isClosed: false,
id: nextConnId,
userId: 0,
onDisconnect: onDisconnect,
}
}
// 从读队列读取消息
func (c *wsConnect) readFromChan() (*wsMessage, error) {
select {
case msg := <-c.inChan:
return msg, nil
case <-c.closeCh:
return nil, fmt.Errorf("连接已关闭")
}
}
// 把消息放进写队列
func (c *wsConnect) sendMsg(msgType int, data []byte) error {
select {
case c.outChan <- &wsMessage{messageType: msgType, data: data}:
case <-c.closeCh:
return fmt.Errorf("连接已关闭")
}
return nil
}
// 把消息放进写队列
func (c *wsConnect) SendMsg(data []byte) error {
return c.sendMsg(wsMsgType, data)
}
// 关闭链接
func (c *wsConnect) Close() {
log.Debug(c.Log("关闭链接"))
c.mutex.Lock()
defer c.mutex.Unlock()
if c.isClosed == false {
c.isClosed = true
wsMgr.Remove(c)
close(c.closeCh)
if c.onDisconnect != nil {
c.onDisconnect(c)
}
}
}
// 循环从websocket中读取消息放入到读队列中
func (c *wsConnect) readWsLoop() {
c.wsConn.SetReadLimit(maxMessageSize)
_ = c.wsConn.SetReadDeadline(time.Now().Add(pongWait))
for {
// 读一个message
msgType, data, err := c.wsConn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) {
log.Error(c.Log("消息读取出现错误:%v", err))
}
c.Close()
return
}
switch msgType {
case websocket.PingMessage:
_ = c.sendMsg(websocket.PongMessage, []byte("pong"))
case websocket.PongMessage:
// _ = c.sendMsg(websocket.PingMessage, []byte("ping"))
case websocket.CloseMessage:
code := websocket.CloseNormalClosure
reason := ""
if len(data) >= 2 {
code = int(binary.BigEndian.Uint16(data))
reason = string(data[2:])
}
log.Debug(c.Log("关闭原因码:%d 描述:%s", code, reason))
// 发送响应关闭帧(必须回传相同状态码)
rspMsg := websocket.FormatCloseMessage(code, reason)
_ = c.wsConn.WriteControl(websocket.CloseMessage, rspMsg, time.Now().Add(5*time.Second))
c.Close()
default:
if msgType != wsMsgType {
continue
}
msg := &wsMessage{messageType: msgType, data: data}
select {
case c.inChan <- msg:
case <-c.closeCh:
return
}
}
}
}
func (c *wsConnect) writeWsLoop() {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
// 取一个消息发送给客户端
case msg := <-c.outChan:
if err := c.wsConn.WriteMessage(msg.messageType, msg.data); err != nil {
log.Error(c.Log("发送消息错误:%v", err))
// 关闭连接
c.Close()
return
}
case <-c.closeCh:
// 收到关闭通知
return
case <-ticker.C:
_ = c.wsConn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.wsConn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}
func (c *wsConnect) handle(process func(IConn, []byte)) {
for {
msg, err := c.readFromChan()
if err != nil {
// log.Error(c.Log("获取消息错误:%v", err))
break
}
// Log.Debug(c.Log("接收消息:%v", msg.data))
process(c, msg.data)
}
}
// 设置用户id
func (c *wsConnect) setUserId(uid int64) {
c.userId = uid
}
// 获取连接id
func (c *wsConnect) Id() uint32 {
return c.id
}
// 获取用户id
func (c *wsConnect) UserId() int64 {
return c.userId
}
func (c *wsConnect) Name() string {
return fmt.Sprintf("用户:%v, 地址:%v", c.userId, c.wsConn.RemoteAddr())
}
func (c *wsConnect) Log(format string, v ...interface{}) string {
s := fmt.Sprintf("连接:%v, id:%v ", c.wsConn.RemoteAddr().String(), c.id)
return s + fmt.Sprintf(format, v...)
}

45
ws/wsMgr.go Normal file
View File

@ -0,0 +1,45 @@
package ws
import cmap "github.com/orcaman/concurrent-map/v2"
var wsMgr = newManager()
type wsManager struct {
wsConnAll cmap.ConcurrentMap[uint32, *wsConnect]
}
func newManager() *wsManager {
return &wsManager{
wsConnAll: cmap.NewWithCustomShardingFunction[uint32, *wsConnect](func(key uint32) uint32 {
return key
}),
}
}
func (m *wsManager) Add(conn *wsConnect) {
m.wsConnAll.Set(conn.id, conn)
}
func (m *wsManager) SetUserId(connId uint32, userId int64) {
userMgr.Add(connId, userId)
}
func (m *wsManager) Remove(conn *wsConnect) {
if conn.UserId() > 0 {
userMgr.Remove(conn.UserId())
}
m.wsConnAll.Remove(conn.id)
}
func (m *wsManager) Get(connId uint32) (*wsConnect, bool) {
return m.wsConnAll.Get(connId)
}
func (m *wsManager) FindByUserId(userId int64) (*wsConnect, bool) {
connId := userMgr.GetConnId(userId)
return m.wsConnAll.Get(connId)
}
func (m *wsManager) Count() int {
return m.wsConnAll.Count()
}

81
ws/wsServer.go Normal file
View File

@ -0,0 +1,81 @@
package ws
import (
"github.com/fox/fox/ksync"
"github.com/fox/fox/log"
"github.com/gorilla/websocket"
"net/http"
"time"
)
const (
// 允许等待的写入时间
writeWait = 10 * time.Second
// pong间隔时间
pongWait = 60 * time.Second
// ping间隔时间
pingPeriod = (pongWait * 9) / 10
// 最大消息长度
maxMessageSize = 512
)
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 允许所有跨域请求(生产环境应限制)
// 跨域限制
// allowedOrigins := map[string]bool{
// "https://yourdomain.com": true,
// "https://api.yourdomain.com": true,
// }
// return allowedOrigins[r.Header.Get("Origin")]
},
HandshakeTimeout: 10 * time.Second, // 握手超时
ReadBufferSize: 4096, // 读缓冲区
WriteBufferSize: 4096, // 写缓冲区
// MaxMessageSize: 1024 * 1024 * 2, // 最大消息2MB
}
type WsServer struct {
addr string // 0.0.0.0:8888
onMessage func(IConn, []byte)
onDisconnect func(IConn)
}
func NewWsServer(addr string, onMessage func(IConn, []byte), onDisconnect func(IConn)) *WsServer {
return &WsServer{addr: addr, onMessage: onMessage, onDisconnect: onDisconnect}
}
func (s *WsServer) wsHandle(w http.ResponseWriter, r *http.Request) {
conn, err := upGrader.Upgrade(w, r, nil)
if err != nil {
log.ErrorF("升级到WebSocket失败:%v", err)
return
}
// defer func() { _ = conn.Close() }()
nextConnId++
wsConn := newWsConnect(conn, s.onDisconnect)
wsMgr.Add(wsConn)
log.DebugF("当前在线人数:%v", wsMgr.Count())
ksync.GoSafe(func() { wsConn.handle(s.onMessage) }, nil)
ksync.GoSafe(wsConn.readWsLoop, nil)
ksync.GoSafe(wsConn.writeWsLoop, nil)
}
func (s *WsServer) Run() {
http.HandleFunc("/", s.wsHandle)
log.DebugF("websocket server listening on :%v", s.addr)
ksync.GoSafe(func() {
err := http.ListenAndServe(s.addr, nil)
if err != nil {
log.Error(err.Error())
}
}, nil)
}
func (s *WsServer) SetUserId(connId uint32, userId int64) {
wsMgr.SetUserId(connId, userId)
}
func (s *WsServer) FindConnByUserId(userId int64) (IConn, bool) {
return wsMgr.FindByUserId(userId)
}

66
ws/ws_test.go Normal file
View File

@ -0,0 +1,66 @@
package ws
import (
"fmt"
"github.com/fox/fox/log"
"testing"
"time"
)
var (
serverAddr = ":8080"
addr = "localhost:8080"
)
func initLog() {
log.Open("test.Log", log.DebugL)
}
func wsServer() {
s := NewWsServer(serverAddr, func(conn IConn, data []byte) {
log.DebugF("服务端收到消息:%v", string(data))
_ = conn.SendMsg(data)
}, func(conn IConn) {
log.Debug(conn.Log("退出"))
})
s.Run()
}
func wsClient() {
client, err := NewClient(fmt.Sprintf("ws://%v", addr))
if err != nil {
log.Fatal(err.Error())
return
}
defer client.Stop()
count := 1
client.Start()
msg := fmt.Sprintf("hell world %v", count)
// Log.Debug(fmt.Sprintf("%v", []byte(msg)))
client.SendMsg([]byte(msg))
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
count++
client.SendMsg([]byte(fmt.Sprintf("hell world %v", count)))
if count > 30 {
return
}
}
break
}
}
func TestWebsocket(t *testing.T) {
initLog()
wsServer()
wsClient()
// time.Sleep(60 * time.Second)
log.Debug("shutdown")
}

67
xrand/rand.go Normal file
View File

@ -0,0 +1,67 @@
package xrand
import (
"math/rand"
"time"
)
func init() {
rand.New(rand.NewSource(time.Now().UnixNano()))
}
var Int = rand.Int
func Intn(n int) int {
return rand.Intn(n)
}
func Int31() int32 {
return rand.Int31()
}
func Int31n(n int32) int32 {
return rand.Int31n(n)
}
func Int63() int64 {
return rand.Int63()
}
func Int63n(n int64) int64 {
return rand.Int63n(n)
}
func Uint32() uint32 {
return rand.Uint32()
}
func Uint64() uint64 {
return rand.Uint64()
}
func Float32() float32 {
return rand.Float32()
}
func Float64() float64 {
return rand.Float64()
}
func Perm(n int) []int {
return rand.Perm(n)
}
func Read(p []byte) (n int, err error) {
return rand.Read(p)
}
func Shuffle[T any](slice []T) {
rand.Shuffle(len(slice), func(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
})
}
// RandomInt64 随机生成一个[min, max]之间的整数
func RandomInt64(min, max int64) int64 {
return Int63n(max-min) + min
}

9
xrand/rand_test.go Normal file
View File

@ -0,0 +1,9 @@
package xrand
import "testing"
func TestShuffle(t *testing.T) {
a := []int{1, 2, 3}
Shuffle(a)
t.Log(a)
}

42
xrand/weight.go Normal file
View File

@ -0,0 +1,42 @@
package xrand
import "errors"
var ErrWeightRandomBadParam = errors.New("WeightRandom: bad param")
type randomItem[T any] struct {
Weight uint
Data T
}
// WeightRandom 加权随机
func WeightRandom[W int | uint | int32 | uint32 | int64 | uint64, T any](m map[W]T) (result T, err error) {
sum := uint(0)
var items []randomItem[T]
for weight, data := range m {
sum += uint(weight)
items = append(items, randomItem[T]{
Weight: uint(weight),
Data: data,
})
}
if len(items) == 0 || sum == 0 {
err = ErrWeightRandomBadParam
return
}
if len(items) == 1 {
return items[0].Data, nil
}
r := Intn(int(sum))
for _, item := range items {
r -= int(item.Weight)
if r < 0 {
return item.Data, nil
}
}
err = ErrWeightRandomBadParam
return
}

25
xrand/weight_test.go Normal file
View File

@ -0,0 +1,25 @@
package xrand
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestWeightRandom(t *testing.T) {
stat := make(map[any]int)
for i := 1; i <= 10000; i++ {
items := map[uint]string{
5: "a",
15: "b",
30: "c",
50: "d",
}
item, err := WeightRandom(items)
assert.Nil(t, err)
if _, ok := stat[item]; !ok {
stat[item] = 0
}
stat[item]++
}
t.Log(stat)
}

42
xtime/constant.go Normal file
View File

@ -0,0 +1,42 @@
package xtime
import (
"github.com/golang-module/carbon/v2"
"time"
)
type TimezoneOffset = int
// 系统时区偏移量,也就是默认时区偏移量
var sysTimezoneOffset = SaoPauloTimezoneOffset
var nowOffset int
const (
// SaoPauloTimezoneOffset 南美/圣保罗 西三区时间UTC-3
SaoPauloTimezoneOffset TimezoneOffset = -3 * 3600
// ShangHaiTimezoneOffset 亚洲/上海 东八区时间UTC+3
ShangHaiTimezoneOffset TimezoneOffset = 8 * 3600
)
const (
// 每周起始日
WeekStartsAt = carbon.Monday
// 时间间隔
NoDuration = time.Duration(0)
Day = time.Hour * 24
Week = 7 * Day
Month = 30 * Day
)
// SetSysTimezoneOffset 设置系统时区偏移量,影响所有默认行为
func SetSysTimezoneOffset(offset int) {
sysTimezoneOffset = offset
}
// SetNowTimeOffset 设置当前时间偏移影响Now的结果
func SetNowTimeOffset(offset int) {
nowOffset = offset
}

83
xtime/duration.go Normal file
View File

@ -0,0 +1,83 @@
package xtime
import (
"time"
)
// 刷新点:时间边界
func GetDuration(t time.Time) (d time.Duration) {
return t.Sub(time.Now())
}
// 现在到明日刷新点的剩余时间间隔
func GetDurationToNextHour() (d time.Duration) {
return GetDurationToNextNHour(1)
}
// 现在到第N天刷新点的剩余时间间隔 (n为1是明天)
func GetDurationToNextNHour(n int) (d time.Duration) {
now := Now().StdTime()
next := Now().AddHours(n).StartOfHour().StdTime()
return next.Sub(now)
}
// 现在到明日刷新点的剩余时间间隔
func GetDurationToTomorrow() (d time.Duration) {
return GetDurationToNextNDay(1)
}
// 现在到第N天刷新点的剩余时间间隔 (n为1是明天)
func GetDurationToNextNDay(n int) (d time.Duration) {
now := Now().StdTime()
next := Now().AddDays(n).StartOfDay().StdTime()
return next.Sub(now)
}
// 现在到下周刷新点的剩余时间间隔
func GetDurationToNextWeek() (expiration time.Duration) {
return GetDurationToNextNWeek(1)
}
// 现在到下N周刷新点的剩余时间间隔 (n为1是下周)
func GetDurationToNextNWeek(n int) (expiration time.Duration) {
now := Now().StdTime()
nextN := Now().AddWeeks(n).StartOfWeek().StdTime()
return nextN.Sub(now)
}
// 现在到下月刷新点的剩余时间间隔
func GetDurationToNextMonth() (expiration time.Duration) {
return GetDurationToNextNMonth(1)
}
// 现在到下N月刷新点的剩余时间间隔 (n为1是下月)
func GetDurationToNextNMonth(n int) (expiration time.Duration) {
now := Now().StdTime()
nextN := Now().AddMonths(n).StartOfMonth().StdTime()
return nextN.Sub(now)
}
// 现在到下季度刷新点的剩余时间间隔
func GetDurationToNextQuarter() (expiration time.Duration) {
return GetDurationToNextNQuarter(1)
}
// 现在到下N季度刷新点的剩余时间间隔 (n为1是下年)
func GetDurationToNextNQuarter(n int) (expiration time.Duration) {
now := Now().StdTime()
nextN := Now().AddQuarters(n).StartOfQuarter().StdTime()
return nextN.Sub(now)
}
// 现在到下年刷新点的剩余时间间隔
func GetDurationToNextYear() (expiration time.Duration) {
return GetDurationToNextNYear(1)
}
// 现在到下N年刷新点的剩余时间间隔 (n为1是下年)
func GetDurationToNextNYear(n int) (expiration time.Duration) {
now := Now().StdTime()
nextN := Now().AddYears(n).StartOfYear().StdTime()
return nextN.Sub(now)
}

114
xtime/duration_test.go Normal file
View File

@ -0,0 +1,114 @@
package xtime
import (
"fmt"
"testing"
)
func TestGetDurationToNextHour(t *testing.T) {
now := Now()
target := Tomorrow()
d := GetDurationToNextHour()
fmt.Printf("now[%v] %s to next hour[%s] \n",
now,
d,
target,
)
}
func TestGetDurationToNextNHour(t *testing.T) {
n := 3
now := Now()
target := Tomorrow()
d := GetDurationToNextNHour(n)
fmt.Printf("now[%v] %s to next %d hour[%s] \n",
now,
d,
n,
target,
)
}
func TestGetDurationToTomorrow(t *testing.T) {
now := Now()
target := Tomorrow()
d := GetDurationToTomorrow()
fmt.Printf("now[%v](%s) %s to tomorrow[%s](%s) \n",
FormatDate(now),
now,
d,
FormatDate(target),
target,
)
}
func TestGetDurationToNextNDay(t *testing.T) {
n := 3
now := Now()
target := NextNDay(n)
d := GetDurationToNextNDay(n)
fmt.Printf("now[%v](%s) %s to next %v day[%s](%s) \n",
FormatDate(now),
now,
d,
n,
FormatDate(target),
target,
)
}
func TestGetDurationToNextWeek(t *testing.T) {
now := Now()
target := NextWeek()
d := GetDurationToNextWeek()
fmt.Printf("now[%v](%s) %s to next week[%s](%s) \n",
FormatWeek(now),
now,
d,
FormatWeek(target),
target,
)
}
func TestGetDurationToNextNWeek(t *testing.T) {
n := 3
now := Now()
target := NextNWeek(n)
d := GetDurationToNextNWeek(n)
fmt.Printf("now[%v](%s) %s to next %v week[%s](%s) \n",
FormatWeek(now),
now,
d,
n,
FormatWeek(target),
target,
)
}
func TestGetDurationToNextMonth(t *testing.T) {
now := Now()
target := NextMonth()
d := GetDurationToNextMonth()
fmt.Printf("now[%v](%s) %s to next month[%s](%s) \n",
FormatMonth(now),
now,
d,
FormatMonth(target),
target,
)
}
func TestGetDurationToNextNMonth(t *testing.T) {
n := 3
now := Now()
target := NextNMonth(n)
d := GetDurationToNextNMonth(n)
fmt.Printf("now[%v](%s) %s to next %v month[%s](%s) \n",
FormatMonth(now),
now,
d,
n,
FormatMonth(target),
target,
)
}

46
xtime/format.go Normal file
View File

@ -0,0 +1,46 @@
package xtime
import "github.com/golang-module/carbon/v2"
const (
WeekLayout = "Y-W"
MonthLayout = "Y-m"
QuarterLayout = "Y-Q"
YearLayout = "Y"
)
// 2006-01-02
func FormatDate(c carbon.Carbon) string {
return c.ToDateString()
}
// 2006-55 (55周)
func FormatWeek(c carbon.Carbon) string {
return c.Format(WeekLayout)
}
// 2006-12
func FormatMonth(c carbon.Carbon) string {
return c.Format(MonthLayout)
}
// 2006-04 (4季度)
func FormatQuarter(c carbon.Carbon) string {
return c.Format(QuarterLayout)
}
// 2006
func FormatYear(c carbon.Carbon) string {
return c.Format(YearLayout)
}
// 20060102
func FormatDateNum(c carbon.Carbon) int {
return c.Year()*10000 + c.Month()*100 + c.Day()
}

32
xtime/format_test.go Normal file
View File

@ -0,0 +1,32 @@
package xtime
import (
"fmt"
"testing"
)
func TestFormat(t *testing.T) {
fmt.Printf(
"FormatDate(Yesterday()): %v\nFormatDate(Today()): %v\nFormatDate(Tomorrow()): %v\nFormatDate(NextNDay(3)): %v\nFormatWeek(ThisWeek()): %v\nFormatWeek(LastWeek()): %v\nFormatWeek(NextWeek()): %v\nFormatWeek(NextNWeek(3)): %v\nFormatMonth(ThisMonth()): %v\nFormatMonth(LastMonth()): %v\nFormatMonth(NextMonth()): %v\nFormatMonth(NextNMonth(3)): %v\nFormatQuarter(ThisQuarter()): %v\nFormatQuarter(LastQuarter()): %v\nFormatQuarter(NextQuarter()): %v\nFormatQuarter(NextNQuarter(3)): %v\nFormatYear(ThisYear()): %v\nFormatYear(LastYear()): %v\nFormatYear(NextYear()): %v\nFormatYear(NextNYear(3)): %v\n",
FormatDate(Yesterday()),
FormatDate(Today()),
FormatDate(Tomorrow()),
FormatDate(NextNDay(3)),
FormatWeek(ThisWeek()),
FormatWeek(LastWeek()),
FormatWeek(NextWeek()),
FormatWeek(NextNWeek(3)),
FormatMonth(ThisMonth()),
FormatMonth(LastMonth()),
FormatMonth(NextMonth()),
FormatMonth(NextNMonth(3)),
FormatQuarter(ThisQuarter()),
FormatQuarter(LastQuarter()),
FormatQuarter(NextQuarter()),
FormatQuarter(NextNQuarter(3)),
FormatYear(ThisYear()),
FormatYear(LastYear()),
FormatYear(NextYear()),
FormatYear(NextNYear(3)),
)
}

11
xtime/os.go Normal file
View File

@ -0,0 +1,11 @@
package xtime
import (
"time"
)
// 设置系统时区
func SetTimeOffset(offset int) {
time.Local = time.FixedZone("CST", offset)
}

41
xtime/parser.go Normal file
View File

@ -0,0 +1,41 @@
package xtime
import (
"github.com/golang-module/carbon/v2"
)
// 解析系统时区输出的字符串、时间戳,转为偏移时区
// 注意如果你的时间戳和字符串已经是偏移时区输出的,就不应使用下列解析函数!
func Parse(value string, offsets ...int) (c carbon.Carbon) {
c = carbon.Parse(value)
c = set(c, offsets...)
return c
}
//goland:noinspection GoUnusedExportedFunction
func FromUnix(sec int64, offsets ...int) carbon.Carbon {
c := carbon.CreateFromTimestamp(sec)
c = set(c, offsets...)
return c
}
func FromUnixMilli(msec int64, offsets ...int) carbon.Carbon {
c := carbon.CreateFromTimestampMilli(msec)
c = set(c, offsets...)
return c
}
//goland:noinspection GoUnusedExportedFunction
func FromUnixMicro(msec int64, offsets ...int) carbon.Carbon {
c := carbon.CreateFromTimestampMicro(msec)
c = set(c, offsets...)
return c
}
//goland:noinspection GoUnusedExportedFunction
func FromUnixNano(msec int64, offsets ...int) carbon.Carbon {
c := carbon.CreateFromTimestampNano(msec)
c = set(c, offsets...)
return c
}

16
xtime/parser_test.go Normal file
View File

@ -0,0 +1,16 @@
package xtime
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParse(t *testing.T) {
assert.Equal(t, Parse("2023-01-01 00:00:00").ToDateTimeString(), "2022-12-31 20:00:00")
}
func TestFromMilli(t *testing.T) {
ts := int64(1672502400000) // "2023-01-01 00:00:00"
assert.Equal(t, FromUnixMilli(ts).ToDateTimeString(), "2022-12-31 20:00:00")
}

209
xtime/timex.go Normal file
View File

@ -0,0 +1,209 @@
package xtime
import (
"fmt"
"time"
"github.com/golang-module/carbon/v2"
)
// 基于Carbon而不是Time
// 默认将 SaoPauloTimezoneOffset 作为时区偏移
// 默认将 WeekStartsAt 作为每周起始
// 注意新加的函数都要基于Now()
// 设置时区
// reverse给Now()设置时传false解析系统时区输出的字符串/时间戳时传true
func setLocation(c carbon.Carbon, offsets ...int) carbon.Carbon {
var loc *time.Location
if len(offsets) > 0 {
loc = time.FixedZone("CUS", offsets[len(offsets)-1])
} else {
loc = time.FixedZone("CST", sysTimezoneOffset)
}
return c.SetLocation(loc)
}
func set(c carbon.Carbon, offsets ...int) carbon.Carbon {
// 设置周起始
c = c.SetWeekStartsAt(WeekStartsAt)
// 设置时区
c = setLocation(c, offsets...)
return c
}
func Now(offsets ...int) (c carbon.Carbon) {
c = carbon.Now().AddSeconds(nowOffset)
c = set(c, offsets...)
return
}
func ConvertToCarbon[T time.Time | carbon.Carbon | int64](t T, offsets ...int) carbon.Carbon {
var c carbon.Carbon
switch v := any(t).(type) {
case time.Time:
c = set(carbon.CreateFromStdTime(v), offsets...)
case carbon.Carbon:
c = set(v, offsets...)
case int64:
c = set(carbon.CreateFromTimestamp(v), offsets...)
}
return c
}
// IsTodayTimestamp 判断该时间戳是否为今天
func IsTodayTimestamp[T int | int64](timestamp T, offsets ...int) bool {
return IsToday(int64(timestamp), offsets...)
}
// IsToday 判断时间是否为今天
func IsToday[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool {
c := ConvertToCarbon(t, offsets...)
return c.ToDateString() == Now().ToDateString()
}
// IsBeforeNow 判断时间t是否在现在之前
// example:
//
// now := time.Now()
// before := time.Now().Add(-time.Hour)
// IsBeforeNow(after) // true
func IsBeforeNow[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool {
return !IsAfterNow(t, offsets...)
}
// IsAfterNow 判断时间t是否在现在之后
// example:
//
// now := time.Now()
// after := time.Now().Add(time.Hour)
// IsAfterNow(after) // true
func IsAfterNow[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool {
switch v := any(t).(type) {
case time.Time:
return IsAfter(v, time.Now(), offsets...)
case carbon.Carbon:
return IsAfter(v, Now(), offsets...)
case int64:
return IsAfter(v, Now().Timestamp())
default:
return false
}
}
// IsBefore 判断时间t1是否在t2之前
// example:
// t1 := time.Now().Add(-time.Hour)
// t2 := time.Now()
// IsBefore(t1,t2) // true
func IsBefore[T time.Time | carbon.Carbon | int64](t1, t2 T, offsets ...int) bool {
return !IsAfter(t1, t2, offsets...)
}
// IsAfter 判断时间t1是否在t2之后
// example:
// t1 := time.Now().Add(time.Hour)
// t2 := time.Now()
// IsAfter(t1,t2) // true
func IsAfter[T time.Time | carbon.Carbon | int64](t1, t2 T, offsets ...int) bool {
c1, c2 := ConvertToCarbon(t1, offsets...), ConvertToCarbon(t2, offsets...)
return c1.Gt(c2)
}
// TimestampToTime 将时间戳转为 carbon.Carbon 类型
func TimestampToTime[T int | int64](timestamp T, offsets ...int) carbon.Carbon {
return set(carbon.CreateFromTimestamp(int64(timestamp)), offsets...)
}
// IsInDuration 判断某个时间在某个时间段内
func IsInDuration[T time.Time | carbon.Carbon | int64](t, start, end T, offsets ...int) bool {
return IsAfter(t, start, offsets...) && IsBefore(t, end, offsets...)
}
// FormatDuration 将起止时间,格式化为 format - format
func FormatDuration[T time.Time | carbon.Carbon | int64](start, end T, layout string, offsets ...int) string {
startC, endC := ConvertToCarbon(start, offsets...), ConvertToCarbon(end, offsets...)
return fmt.Sprintf("%s - %s", startC.StdTime().Format(layout), endC.StdTime().Format(layout))
}
func Yesterday(offsets ...int) carbon.Carbon {
return Now(offsets...).SubDay().StartOfDay()
}
func Today(offsets ...int) carbon.Carbon {
return Now(offsets...).StartOfDay()
}
func Tomorrow(offsets ...int) carbon.Carbon {
return Now(offsets...).AddDay().StartOfDay()
}
func NextNDay(n int, offsets ...int) carbon.Carbon {
return Now(offsets...).AddDays(n).StartOfDay()
}
func LastWeek(offsets ...int) carbon.Carbon {
return Now(offsets...).SubWeek().StartOfWeek()
}
func ThisWeek(offsets ...int) carbon.Carbon {
return Now(offsets...).StartOfWeek()
}
func NextWeek(offsets ...int) carbon.Carbon {
return Now(offsets...).AddWeek().StartOfWeek()
}
func NextNWeek(n int, offsets ...int) carbon.Carbon {
return Now(offsets...).AddWeeks(n).StartOfWeek()
}
func LastMonth(offsets ...int) carbon.Carbon {
return Now(offsets...).SubMonth().StartOfMonth()
}
func ThisMonth(offsets ...int) carbon.Carbon {
return Now(offsets...).StartOfMonth()
}
func NextMonth(offsets ...int) carbon.Carbon {
return Now(offsets...).AddMonth().StartOfMonth()
}
func NextNMonth(n int, offsets ...int) carbon.Carbon {
return Now(offsets...).AddMonths(n).StartOfMonth()
}
func LastQuarter(offsets ...int) carbon.Carbon {
return Now(offsets...).SubQuarter().StartOfQuarter()
}
func ThisQuarter(offsets ...int) carbon.Carbon {
return Now(offsets...).StartOfQuarter()
}
func NextQuarter(offsets ...int) carbon.Carbon {
return Now(offsets...).AddQuarter().StartOfQuarter()
}
func NextNQuarter(n int, offsets ...int) carbon.Carbon {
return Now(offsets...).AddQuarters(n).StartOfQuarter()
}
func LastYear(offsets ...int) carbon.Carbon {
return Now(offsets...).SubYear().StartOfYear()
}
func ThisYear(offsets ...int) carbon.Carbon {
return Now(offsets...).StartOfYear()
}
func NextYear(offsets ...int) carbon.Carbon {
return Now(offsets...).AddYear().StartOfYear()
}
func NextNYear(n int, offsets ...int) carbon.Carbon {
return Now(offsets...).AddYears(n).StartOfYear()
}

146
xtime/timex_test.go Normal file
View File

@ -0,0 +1,146 @@
package xtime
import (
"github.com/golang-module/carbon/v2"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestSetTimeOffset(t *testing.T) {
loc, err := time.LoadLocation("Asia/Shanghai")
assert.NoError(t, err)
loc2, err := time.LoadLocation("America/New_York")
assert.NoError(t, err)
a := time.Now().In(loc)
b := time.Now().In(loc2)
t.Log(a.String())
t.Log(b.String())
Now()
t.Log(a.Sub(b))
}
func TestFormatDayNum(t *testing.T) {
t1 := Parse("2023-01-01 00:00:00", 0)
assert.Equal(t, FormatDateNum(t1), 20221231)
t2 := Parse("2023-01-01 04:00:00", 0)
assert.Equal(t, FormatDateNum(t2), 20230101)
}
func TestNow(t *testing.T) {
now := Now()
sc := carbon.Now(carbon.SaoPaulo)
b := func(args ...int) []int { return args }
assert.Equal(t, b(now.Time()), b(sc.Time()))
}
func TestIsToday(t *testing.T) {
now := time.Now()
assert.True(t, IsToday(now))
assert.False(t, IsToday(now.Add(24*time.Hour)))
assert.False(t, IsToday(now.Add(-24*time.Hour)))
}
func TestIsTodayTimestamp(t *testing.T) {
now := Now()
st := now.Timestamp()
assert.True(t, IsTodayTimestamp(st))
assert.False(t, IsTodayTimestamp(now.Yesterday().Timestamp()))
assert.False(t, IsTodayTimestamp(now.AddDay().Timestamp()))
}
func TestIsAfterNow(t *testing.T) {
t.Run("std Time", func(t *testing.T) {
now := time.Now()
after := now.Add(time.Hour)
assert.True(t, IsAfterNow(after))
now = time.Now()
before := now.Add(-time.Hour)
assert.False(t, IsAfterNow(before))
})
t.Run("carbon", func(t *testing.T) {
now := Now()
after := now.AddHour()
assert.True(t, IsAfterNow(after))
now = Now()
before := now.SubHour()
assert.False(t, IsAfterNow(before))
})
t.Run("timestamp", func(t *testing.T) {
now := time.Now().Unix()
after := now + 3600
assert.True(t, IsAfterNow(after))
now = time.Now().Unix()
before := now - 3600
assert.False(t, IsAfterNow(before))
})
}
func TestIsBeforeNow(t *testing.T) {
t.Run("std Time", func(t *testing.T) {
now := time.Now()
before := now.Add(-time.Hour)
assert.True(t, IsBeforeNow(before))
now = time.Now()
after := now.Add(time.Hour)
assert.False(t, IsBeforeNow(after))
})
t.Run("carbon", func(t *testing.T) {
now := Now()
before := now.SubHour()
assert.True(t, IsBeforeNow(before))
now = Now()
after := now.AddHour()
assert.False(t, IsBeforeNow(after))
})
t.Run("timestamp", func(t *testing.T) {
now := time.Now().Unix()
before := now - 3600
assert.True(t, IsBeforeNow(before))
now = time.Now().Unix()
after := now + 3600
assert.False(t, IsBeforeNow(after))
})
}
func TestIsInDuration(t *testing.T) {
t.Run("std Time", func(t *testing.T) {
now := time.Now()
start := now.Add(-time.Hour)
end := now.Add(time.Hour)
assert.True(t, IsInDuration(now, start, end))
assert.False(t, IsInDuration(now.Add(10*time.Hour), start, end))
assert.False(t, IsInDuration(now.Add(-10*time.Hour), start, end))
})
t.Run("carbon", func(t *testing.T) {
now := Now()
start := now.SubHour()
end := now.AddHour()
assert.True(t, IsInDuration(now, start, end))
assert.False(t, IsInDuration(now.AddHours(10), start, end))
assert.False(t, IsInDuration(now.SubHours(10), start, end))
})
t.Run("timestamp", func(t *testing.T) {
now := time.Now().Unix()
start := now - 3600
end := now + 3600
assert.True(t, IsInDuration(now, start, end))
assert.False(t, IsInDuration(now+3600*10, start, end))
assert.False(t, IsInDuration(now-3600*10, start, end))
})
}