Compare commits

...

89 Commits

Author SHA1 Message Date
Bobo
99c8f6e4c1 feat: id utils 2025-06-04 15:52:46 +08:00
Bobo
3f713e489b feat: time util 2025-05-29 00:15:37 +08:00
Bobo
ace6bd4237 feat: time util 2025-05-29 00:07:27 +08:00
Bobo
3ec9bbbd27 feat: time util 2025-05-28 23:58:08 +08:00
Bobo
faa5817857 feat: time util 2025-05-28 23:55:43 +08:00
Bobo
89a69a9d4d feat: entgo 2025-05-28 23:29:21 +08:00
Bobo
63789d090d feat: entgo 2025-05-28 23:08:18 +08:00
Bobo
06c29c57d2 feat: copier util 2025-05-28 23:06:30 +08:00
Bobo
5faa4e06f7 feat: time util 2025-05-28 22:38:29 +08:00
Bobo
5abe88a0d9 feat: jwt util 2025-05-23 14:31:59 +08:00
Bobo
028dd8dd19 feat: trans util 2025-05-23 12:58:21 +08:00
Bobo
50161f8c8a feat: trans util 2025-05-22 14:15:57 +08:00
Bobo
e686e7189c feat: copier util 2025-05-22 10:53:04 +08:00
Bobo
d1aa8e36ad feat: copier util 2025-05-22 10:33:02 +08:00
Bobo
2fb73a0a1f feat: copier util 2025-05-21 22:09:32 +08:00
Bobo
c03ac708f0 feat: copier util 2025-05-21 21:58:26 +08:00
Bobo
9035e79520 feat: time trans utils 2025-05-18 21:41:00 +08:00
Bobo
3b2678de10 feat: translator 2025-05-14 20:43:37 +08:00
Bobo
982e18a991 feat: string utils 2025-05-13 14:35:47 +08:00
Bobo
f2f5388906 feat: time trans 2025-05-11 23:43:07 +08:00
Bobo
9eca340c7e feat: time trans 2025-05-11 23:42:50 +08:00
Bobo
376746f4db feat: time trans 2025-05-11 23:41:53 +08:00
Bobo
efc24c452f feat: crypto 2025-05-07 16:00:40 +08:00
Bobo
78cef077e5 feat: crypto 2025-05-07 15:57:19 +08:00
Bobo
0420e35a30 feat: crypto 2025-05-07 15:05:18 +08:00
Bobo
0b18560901 feat: crypto 2025-05-07 14:42:12 +08:00
Bobo
7b29f09e37 feat: entgo 2025-05-07 14:07:13 +08:00
Bobo
363a18b1c8 feat: crypto 2025-05-07 13:52:07 +08:00
Bobo
e90588b9ca feat: random string 2025-04-22 18:46:41 +08:00
Bobo
713975e7f1 feat: crypto 2025-04-10 15:15:46 +08:00
Bobo
2ab920982a feat: Randomizer 2025-04-08 20:31:32 +08:00
Bobo
3153ff149f feat: Seeder 2025-04-08 19:58:05 +08:00
Bobo
578cf26ee8 feat: Randomizer 2025-04-08 19:55:49 +08:00
Bobo
ef08927a50 feat: time trans 2025-04-02 15:18:08 +08:00
Bobo
6d209d3612 feat: time trans 2025-04-02 15:08:14 +08:00
Bobo
8f957a7d29 feat: time trans 2025-04-02 14:21:38 +08:00
Bobo
c83b10ca22 feat: entgo 2025-02-16 10:55:51 +08:00
Bobo
15b6b012c2 feat: entgo 2025-02-08 16:09:02 +08:00
Bobo
fdcb900700 feat: entgo 2025-02-08 16:00:54 +08:00
Bobo
ce04468424 feat: entgo. 2025-02-08 15:31:39 +08:00
Bobo
d1418caf84 feat: entgo. 2025-02-08 14:44:21 +08:00
Bobo
f73f016ec5 feat: entgo. 2025-02-08 00:08:10 +08:00
Bobo
d549d305ae feat: entgo. 2025-02-08 00:00:50 +08:00
Bobo
23dcad60a3 feat: time utils. 2025-02-07 22:42:19 +08:00
Bobo
fa7ae4f876 feat: time trans. 2025-01-11 13:22:23 +08:00
tx7do
9f6a4eba80 feat: ent support update json field. 2024-12-14 14:41:44 +08:00
tx7do
801deb98cd feat: ent support update json field. 2024-12-14 13:07:01 +08:00
tx7do
c9a0909d46 feat: upgrade go.mod. 2024-11-14 11:20:41 +08:00
tx7do
9be498db69 feat: upgrade go.mod. 2024-11-14 11:18:05 +08:00
tx7do
71cb2d28e1 feat: upgrade go.mod. 2024-10-18 12:55:04 +08:00
tx7do
e8e61fc7a1 feat: upgrade go.mod. 2024-10-18 12:52:55 +08:00
tx7do
f7abc4e941 feat: math. 2024-09-23 21:31:56 +08:00
tx7do
8d39ab89d9 feat: ent, gorm support otel trace and metrics. 2024-04-28 20:41:10 +08:00
tx7do
902a0ce860 feat: add gorm utils. 2024-04-28 19:02:42 +08:00
tx7do
576d1bfe2b feat: add math utils. 2024-04-28 09:42:53 +08:00
tx7do
4e084cc4b5 feat: TimestamppbToTime & TimeToTimestamppb. 2024-04-21 22:20:59 +08:00
tx7do
ce853c6b87 feat: entgo interface. 2024-04-16 18:43:49 +08:00
tx7do
d5819c6ef4 feat: upgrade go.mod. 2024-04-07 13:31:34 +08:00
tx7do
4d6b87fae6 feat: upgrade go.mod. 2024-04-07 13:29:42 +08:00
tx7do
df9f3f4430 feat: upgrade go.mod. 2024-04-07 13:26:43 +08:00
tx7do
871b028e34 feat: returns a pointer to the provided value use template function. 2024-04-07 13:19:53 +08:00
tx7do
dcbdf4b7a6 feat: ent filter support 3 params and json filed. 2023-11-29 20:36:41 +08:00
tx7do
c04968803a fix: #1 document error. 2023-11-24 13:10:52 +08:00
tx7do
093172ebca feat: ent update fields. 2023-11-08 18:17:47 +08:00
tx7do
d65e7bb928 feat: ent update fields. 2023-11-08 18:15:48 +08:00
tx7do
78452b1abf feat: field mask util. 2023-11-08 15:16:20 +08:00
tx7do
95ce578ddf feat: field mask util. 2023-11-06 20:15:07 +08:00
tx7do
413e14ac78 feat: ent select fields. 2023-11-06 18:40:47 +08:00
tx7do
50a2e139eb feat: ent select fields. 2023-11-06 17:57:40 +08:00
tx7do
18755155ba feat: ent select fields. 2023-11-06 17:02:55 +08:00
tx7do
da82d442cc feat: add. 2023-11-06 14:11:20 +08:00
tx7do
83bb818ade feat: dateutils, maputils, sliceutils. 2023-11-06 13:54:50 +08:00
tx7do
c37d726b4c feat: ent query filter. 2023-11-04 21:35:54 +08:00
tx7do
b27c96f932 feat: ent mixin. 2023-11-01 18:23:12 +08:00
tx7do
836f4e2461 feat: slug generate. 2023-10-31 07:05:48 +08:00
tx7do
5717d4aefe feat: entgo mixin. 2023-10-31 04:46:44 +08:00
tx7do
62142a6836 feat: entgo mixin. 2023-10-31 04:46:17 +08:00
tx7do
9990fa43e0 fix: pagination bug. 2023-10-28 20:14:31 +08:00
tx7do
82fbdc15d9 feat: byte utils 2023-10-28 20:06:50 +08:00
tx7do
b4188ca4d8 feat: io utils 2023-10-27 17:01:35 +08:00
tx7do
e9ea8fa536 feat: io utils 2023-10-27 16:58:06 +08:00
tx7do
6ed25e948c feat: entgo. 2023-10-27 08:15:00 +08:00
tx7do
bfea578907 feat: upgraded. 2023-10-26 19:57:40 +08:00
tx7do
b8a6f7f7d8 feat: change project name. 2023-10-26 19:52:59 +08:00
tx7do
149ded5d4e feat: change project name. 2023-10-26 19:51:23 +08:00
tx7do
60c0477999 feat: tag 2023-10-25 16:59:40 +08:00
tx7do
ef3aa38cb2 feat: update. 2023-10-25 16:58:26 +08:00
tx7do
498bc3ea18 feat: update. 2023-10-25 16:55:53 +08:00
tx7do
1c225465de feat: scripts. 2023-10-25 15:50:55 +08:00
124 changed files with 9994 additions and 789 deletions

26
.gitignore vendored
View File

@@ -1,6 +1,4 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Reference https://github.com/github/gitignore/blob/master/Go.gitignore
# Binaries for programs and plugins
*.exe
*.exe~
@@ -15,7 +13,23 @@
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
vendor/
# Go workspace file
go.work
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
# OS General
Thumbs.db
.DS_Store
# project
*.cert
*.key
*.log
bin/
# Develop tools
.vscode/
.idea/
*.swp

View File

@@ -1 +1 @@
# kratos-utils
# go-utils

View File

@@ -10,7 +10,7 @@ import (
"github.com/mattn/go-sqlite3"
_ "github.com/mattn/go-sqlite3"
"github.com/tx7do/kratos-utils/bank_card/assets"
"github.com/tx7do/go-utils/bank_card/assets"
)
type Database struct {

View File

@@ -1,10 +1,10 @@
module github.com/tx7do/kratos-utils/bank_card
module github.com/tx7do/go-utils/bank_card
go 1.20
require (
github.com/mattn/go-sqlite3 v1.14.17
github.com/stretchr/testify v1.8.4
github.com/mattn/go-sqlite3 v1.14.28
github.com/stretchr/testify v1.10.0
)
require (
@@ -12,3 +12,5 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

View File

@@ -1,11 +1,11 @@
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/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

44
byteutil/util.go Normal file
View File

@@ -0,0 +1,44 @@
package byteutil
import (
"bytes"
"encoding/binary"
)
// IntToBytes 将int转换为[]byte
func IntToBytes(n int) []byte {
data := int64(n)
byteBuf := bytes.NewBuffer([]byte{})
_ = binary.Write(byteBuf, binary.BigEndian, data)
return byteBuf.Bytes()
}
// BytesToInt 将[]byte转换为int
func BytesToInt(bys []byte) int {
byteBuf := bytes.NewBuffer(bys)
var data int64
_ = binary.Read(byteBuf, binary.BigEndian, &data)
return int(data)
}
// ByteToLower lowers a byte
func ByteToLower(b byte) byte {
if b <= '\u007F' {
if 'A' <= b && b <= 'Z' {
b += 'a' - 'A'
}
return b
}
return b
}
// ByteToUpper upper a byte
func ByteToUpper(b byte) byte {
if b <= '\u007F' {
if 'a' <= b && b <= 'z' {
b -= 'a' - 'A'
}
return b
}
return b
}

11
byteutil/util_test.go Normal file
View File

@@ -0,0 +1,11 @@
package byteutil
import (
"fmt"
"testing"
)
func TestIntToBytes(t *testing.T) {
fmt.Println(IntToBytes(1))
fmt.Println(BytesToInt(IntToBytes(1)))
}

128
copierutil/converters.go Normal file
View File

@@ -0,0 +1,128 @@
package copierutil
import (
"time"
"github.com/jinzhu/copier"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/tx7do/go-utils/timeutil"
"github.com/tx7do/go-utils/trans"
)
var TimeToStringConverter = copier.TypeConverter{
SrcType: &time.Time{}, // 源类型
DstType: trans.Ptr(""), // 目标类型
Fn: func(src interface{}) (interface{}, error) {
return timeutil.TimeToTimeString(src.(*time.Time)), nil
},
}
var StringToTimeConverter = copier.TypeConverter{
SrcType: trans.Ptr(""),
DstType: &time.Time{},
Fn: func(src interface{}) (interface{}, error) {
return timeutil.StringTimeToTime(src.(*string)), nil
},
}
var TimeToTimestamppbConverter = copier.TypeConverter{
SrcType: &time.Time{},
DstType: &timestamppb.Timestamp{},
Fn: func(src interface{}) (interface{}, error) {
return timeutil.TimeToTimestamppb(src.(*time.Time)), nil
},
}
var TimestamppbToTimeConverter = copier.TypeConverter{
SrcType: &timestamppb.Timestamp{},
DstType: &time.Time{},
Fn: func(src interface{}) (interface{}, error) {
return timeutil.TimestamppbToTime(src.(*timestamppb.Timestamp)), nil
},
}
func TimeToString(tm *time.Time) *string {
return timeutil.TimeToString(tm, timeutil.ISO8601)
}
func NewTimeStringConverterPair() []copier.TypeConverter {
srcType := &time.Time{}
dstType := trans.Ptr("")
fromFn := TimeToString
toFn := timeutil.StringTimeToTime
return NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
}
func NewTimeTimestamppbConverterPair() []copier.TypeConverter {
srcType := &time.Time{}
dstType := &timestamppb.Timestamp{}
fromFn := timeutil.TimeToTimestamppb
toFn := timeutil.TimestamppbToTime
return NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
}
func NewTypeConverter(srcType, dstType interface{}, fn func(src interface{}) (interface{}, error)) copier.TypeConverter {
return copier.TypeConverter{
SrcType: srcType,
DstType: dstType,
Fn: fn,
}
}
func NewTypeConverterPair(srcType, dstType interface{}, fromFn, toFn func(src interface{}) (interface{}, error)) []copier.TypeConverter {
return []copier.TypeConverter{
{
SrcType: srcType,
DstType: dstType,
Fn: fromFn,
},
{
SrcType: dstType,
DstType: srcType,
Fn: toFn,
},
}
}
func NewGenericTypeConverterPair[A interface{}, B interface{}](srcType A, dstType B, fromFn func(src A) B, toFn func(src B) A) []copier.TypeConverter {
return []copier.TypeConverter{
{
SrcType: srcType,
DstType: dstType,
Fn: func(src interface{}) (interface{}, error) {
return fromFn(src.(A)), nil
},
},
{
SrcType: dstType,
DstType: srcType,
Fn: func(src interface{}) (interface{}, error) {
return toFn(src.(B)), nil
},
},
}
}
func NewErrorHandlingGenericTypeConverterPair[A interface{}, B interface{}](srcType A, dstType B, fromFn func(src A) (B, error), toFn func(src B) (A, error)) []copier.TypeConverter {
return []copier.TypeConverter{
{
SrcType: srcType,
DstType: dstType,
Fn: func(src interface{}) (interface{}, error) {
return fromFn(src.(A))
},
},
{
SrcType: dstType,
DstType: srcType,
Fn: func(src interface{}) (interface{}, error) {
return toFn(src.(B))
},
},
}
}

View File

@@ -0,0 +1,153 @@
package copierutil
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/tx7do/go-utils/timeutil"
"github.com/tx7do/go-utils/trans"
)
func TestNewTypeConverter(t *testing.T) {
srcType := &time.Time{}
dstType := trans.Ptr("")
fn := func(src interface{}) (interface{}, error) {
return timeutil.TimeToTimeString(src.(*time.Time)), nil
}
converter := NewTypeConverter(srcType, dstType, fn)
// 验证转换器的类型
assert.IsType(t, srcType, converter.SrcType)
assert.IsType(t, dstType, converter.DstType)
// 验证转换器的功能
result, err := converter.Fn(&time.Time{})
assert.NoError(t, err)
assert.IsType(t, dstType, result)
}
func TestNewTypeConverterPair(t *testing.T) {
srcType := &time.Time{}
dstType := trans.Ptr("")
fromFn := func(src interface{}) (interface{}, error) {
return timeutil.TimeToTimeString(src.(*time.Time)), nil
}
toFn := func(src interface{}) (interface{}, error) {
return timeutil.StringTimeToTime(src.(*string)), nil
}
converters := NewTypeConverterPair(srcType, dstType, fromFn, toFn)
assert.Len(t, converters, 2, "expected 2 converters")
// 验证第一个转换器
assert.IsType(t, srcType, converters[0].SrcType)
assert.IsType(t, dstType, converters[0].DstType)
result, err := converters[0].Fn(&time.Time{})
assert.NoError(t, err)
assert.IsType(t, dstType, result)
// 验证第二个转换器
assert.IsType(t, dstType, converters[1].SrcType)
assert.IsType(t, srcType, converters[1].DstType)
result, err = converters[1].Fn(trans.Ptr(""))
assert.NoError(t, err)
assert.IsType(t, srcType, result)
}
func TestNewGenericTypeConverterPair(t *testing.T) {
srcType := &time.Time{}
dstType := trans.Ptr("")
fromFn := timeutil.TimeToTimeString
toFn := timeutil.StringTimeToTime
converters := NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
assert.Len(t, converters, 2, "expected 2 converters")
// 验证第一个转换器
assert.IsType(t, srcType, converters[0].SrcType)
assert.IsType(t, dstType, converters[0].DstType)
result, err := converters[0].Fn(&time.Time{})
assert.NoError(t, err)
assert.IsType(t, dstType, result)
// 验证第二个转换器
assert.IsType(t, dstType, converters[1].SrcType)
assert.IsType(t, srcType, converters[1].DstType)
result, err = converters[1].Fn(trans.Ptr(""))
assert.NoError(t, err)
assert.IsType(t, srcType, result)
}
func TestNewErrorHandlingGenericTypeConverterPair(t *testing.T) {
srcType := &time.Time{}
dstType := trans.Ptr("")
fromFn := func(src *time.Time) (*string, error) {
return timeutil.TimeToTimeString(src), nil
}
toFn := func(src *string) (*time.Time, error) {
return timeutil.StringTimeToTime(src), nil
}
converters := NewErrorHandlingGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
assert.Len(t, converters, 2, "expected 2 converters")
// 验证第一个转换器
assert.IsType(t, srcType, converters[0].SrcType)
assert.IsType(t, dstType, converters[0].DstType)
result, err := converters[0].Fn(&time.Time{})
assert.NoError(t, err)
assert.IsType(t, dstType, result)
// 验证第二个转换器
assert.IsType(t, dstType, converters[1].SrcType)
assert.IsType(t, srcType, converters[1].DstType)
result, err = converters[1].Fn(trans.Ptr(""))
assert.NoError(t, err)
assert.IsType(t, srcType, result)
}
func TestNewTimeStringConverterPair(t *testing.T) {
converters := NewTimeStringConverterPair()
assert.Len(t, converters, 2, "expected 2 converters")
// 验证第一个转换器
srcType := &time.Time{}
dstType := trans.Ptr("")
assert.IsType(t, srcType, converters[0].SrcType)
assert.IsType(t, dstType, converters[0].DstType)
result, err := converters[0].Fn(&time.Time{})
assert.NoError(t, err)
assert.IsType(t, dstType, result)
// 验证第二个转换器
assert.IsType(t, dstType, converters[1].SrcType)
assert.IsType(t, srcType, converters[1].DstType)
result, err = converters[1].Fn(trans.Ptr(""))
assert.NoError(t, err)
assert.IsType(t, srcType, result)
}
func TestNewTimeTimestamppbConverterPair(t *testing.T) {
converters := NewTimeTimestamppbConverterPair()
assert.Len(t, converters, 2, "expected 2 converters")
// 验证第一个转换器
srcType := &time.Time{}
dstType := &timestamppb.Timestamp{}
assert.IsType(t, srcType, converters[0].SrcType)
assert.IsType(t, dstType, converters[0].DstType)
result, err := converters[0].Fn(&time.Time{})
assert.NoError(t, err)
assert.IsType(t, dstType, result)
// 验证第二个转换器
assert.IsType(t, dstType, converters[1].SrcType)
assert.IsType(t, srcType, converters[1].DstType)
result, err = converters[1].Fn(&timestamppb.Timestamp{})
assert.NoError(t, err)
assert.IsType(t, srcType, result)
}

24
copierutil/go.mod Normal file
View File

@@ -0,0 +1,24 @@
module github.com/tx7do/go-utils/copierutil
go 1.23.0
toolchain go1.24.3
require (
github.com/jinzhu/copier v0.4.0
github.com/tx7do/go-utils v1.1.27
)
require (
github.com/stretchr/testify v1.10.0
google.golang.org/protobuf v1.36.6
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

26
copierutil/go.sum Normal file
View File

@@ -0,0 +1,26 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

21
crypto/README.md Normal file
View File

@@ -0,0 +1,21 @@
# 加解密算法
## bcrypt
bcrypt是一个由美国计算机科学家尼尔斯·普罗沃斯Niels Provos以及大卫·马齐耶David Mazières根据Blowfish加密算法所设计的密码散列函数于1999年在USENIX中展示[1]。实现中bcrypt会使用一个加盐的流程以防御彩虹表攻击同时bcrypt还是适应性函数它可以借由增加迭代之次数来抵御日益增进的电脑运算能力透过暴力法破解。
由bcrypt加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符并将在内部被转化为448位的密钥。然而所提供的所有字符都具有十分重要的意义。密码越强大数据就越安全。
除了对数据进行加密默认情况下bcrypt在删除数据之前将使用随机数据三次覆盖原始输入文件以阻挠可能会获得计算机数据的人恢复数据的尝试。如果您不想使用此功能可设置禁用此功能。
具体来说bcrypt使用美国密码学家保罗·柯切尔的算法实现。随bcrypt一起发布的源代码对原始版本作了略微改动。
bcrypt哈希由多个部分组成。这些部分用于确定创建哈希的设置从而可以在不需要任何其他信息的情况下对其进行验证。
```text
$2a$10$ygWrRwHCzg2GUpz0UK40kuWAGva121VkScpcdMNsDCih2U/bL2qYy
```
- $2a$Prefix 表示使用bcrypt的算法版本。
- 10$Cost factor 表示加密的复杂度,值越大,计算时间越长。
- ygWrRwHCzg2GUpz0UK40kuWAGva121VkScpcdMNsDCih2U/bL2qYySalt 和 Hash前22个字符是盐值后面的字符是哈希值。

100
crypto/aes.go Normal file
View File

@@ -0,0 +1,100 @@
package crypto
import (
"bytes"
"fmt"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
// DefaultAESKey 默认AES密钥(16字节)
var DefaultAESKey = []byte("f51d66a73d8a0927")
// GenerateAESKey 生成AES密钥
func GenerateAESKey(length int) ([]byte, error) {
if length != 16 && length != 24 && length != 32 {
return nil, fmt.Errorf("invalid key length: %d, must be 16, 24, or 32 bytes", length)
}
key := make([]byte, length)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
return key, nil
}
// PKCS5Padding 填充明文
func PKCS5Padding(plaintext []byte, blockSize int) []byte {
padding := blockSize - len(plaintext)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(plaintext, padText...)
}
// PKCS5UnPadding 去除填充数据
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
// AesEncrypt AES加密
func AesEncrypt(plainText, key, iv []byte) ([]byte, error) {
if plainText == nil {
return nil, fmt.Errorf("plain text is nil")
}
if key == nil {
return nil, fmt.Errorf("key is nil")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// AES分组长度为128位所以blockSize=16单位字节
blockSize := block.BlockSize()
if iv == nil {
// 初始向量的长度必须等于块block的长度16字节
iv = key[:blockSize]
}
plainText = PKCS5Padding(plainText, blockSize)
blockMode := cipher.NewCBCEncrypter(block, iv)
cryptedText := make([]byte, len(plainText))
blockMode.CryptBlocks(cryptedText, plainText)
return cryptedText, nil
}
// AesDecrypt AES解密
func AesDecrypt(cryptedText, key, iv []byte) ([]byte, error) {
if cryptedText == nil {
return nil, fmt.Errorf("crypted text is nil")
}
if key == nil {
return nil, fmt.Errorf("key is nil")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
//AES分组长度为128位所以blockSize=16单位字节
blockSize := block.BlockSize()
if iv == nil {
// 初始向量的长度必须等于块block的长度16字节
iv = key[:blockSize]
}
blockMode := cipher.NewCBCDecrypter(block, iv)
plainText := make([]byte, len(cryptedText))
blockMode.CryptBlocks(plainText, cryptedText)
plainText = PKCS5UnPadding(plainText)
return plainText, nil
}

50
crypto/aes_test.go Normal file
View File

@@ -0,0 +1,50 @@
package crypto
import (
"fmt"
"testing"
"encoding/base64"
"github.com/stretchr/testify/assert"
)
func TestDecryptAES(t *testing.T) {
//key的长度必须是16、24或者32字节分别用于选择AES-128, AES-192, or AES-256
aesKey, _ := GenerateAESKey(16)
aesKey = DefaultAESKey
plainText := []byte("cloud123456")
encryptText, err := AesEncrypt(plainText, aesKey, nil)
if err != nil {
fmt.Println(err)
return
}
pass64 := base64.StdEncoding.EncodeToString(encryptText)
fmt.Printf("加密后:%v\n", pass64)
bytesPass, err := base64.StdEncoding.DecodeString(pass64)
if err != nil {
fmt.Println(err)
return
}
decryptText, err := AesDecrypt(bytesPass, aesKey, nil)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("解密后:%s\n", decryptText)
assert.Equal(t, plainText, decryptText)
}
func TestGenerateAESKey_ValidLengths(t *testing.T) {
lengths := []int{16, 24, 32}
for _, length := range lengths {
key, err := GenerateAESKey(length)
assert.NoError(t, err)
assert.Equal(t, length, len(key))
t.Logf("%d : %x\n", length, string(key))
}
}

View File

@@ -1,17 +1,56 @@
package crypto
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
// DefaultCost 最小值=4 最大值=31 默认值=10
var DefaultCost = 10
// HashPassword 加密密码
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
// Prefix + Cost + Salt + Hashed Text
bytes, err := bcrypt.GenerateFromPassword([]byte(password), DefaultCost)
return string(bytes), err
}
// CheckPasswordHash 校验密码
func CheckPasswordHash(password, hash string) bool {
// HashPasswordWithSalt 对密码进行加盐哈希处理
func HashPasswordWithSalt(password, salt string) (string, error) {
// 将密码和盐组合
combined := []byte(password + salt)
// 计算哈希值
hash := sha256.Sum256(combined)
// 将哈希值转换为十六进制字符串
return hex.EncodeToString(hash[:]), nil
}
// VerifyPassword 验证密码是否正确
func VerifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// VerifyPasswordWithSalt 验证密码是否正确
func VerifyPasswordWithSalt(password, salt, hashedPassword string) bool {
// 对输入的密码和盐进行哈希处理
newHash, _ := HashPasswordWithSalt(password, salt)
// 比较哈希值是否相同
return newHash == hashedPassword
}
// GenerateSalt 生成指定长度的盐
func GenerateSalt(length int) (string, error) {
salt := make([]byte, length)
_, err := rand.Read(salt)
if err != nil {
return "", err
}
return hex.EncodeToString(salt), nil
}

View File

@@ -2,9 +2,10 @@ package crypto
import (
"fmt"
"github.com/stretchr/testify/assert"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHashPassword(t *testing.T) {
@@ -13,16 +14,27 @@ func TestHashPassword(t *testing.T) {
fmt.Println(hash)
}
func TestCheckPasswordHash(t *testing.T) {
func TestVerifyPassword(t *testing.T) {
text := "123456"
// Prefix + Cost + Salt + Hashed Text
hash3 := "$2a$10$ygWrRwHCzg2GUpz0UK40kuWAGva121VkScpcdMNsDCih2U/bL2qYy"
bMatched := CheckPasswordHash(text, hash3)
bMatched := VerifyPassword(text, hash3)
assert.True(t, bMatched)
bMatched = CheckPasswordHash(text, hash3)
bMatched = VerifyPassword(text, hash3)
assert.True(t, bMatched)
}
func TestVerifyPasswordWithSalt_CorrectPassword(t *testing.T) {
password := "securePassword"
salt, _ := GenerateSalt(16)
hashedPassword, _ := HashPasswordWithSalt(password, salt)
result := VerifyPasswordWithSalt(password, salt, hashedPassword)
assert.True(t, result, "Password verification should succeed with correct password and salt")
}
func TestJwtToken(t *testing.T) {
const bearerWord string = "Bearer"
token := "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjowfQ.XgcKAAjHbA6o4sxxbEaMi05ingWvKdCNnyW9wowbJvs"

32
dateutil/dateutils.go Normal file
View File

@@ -0,0 +1,32 @@
package dateutil
import "time"
// Floor - takes a datetime and return a datetime from the same day at 00:00:00 (UTC).
func Floor(t time.Time) time.Time {
return t.UTC().Truncate(time.Hour * 24)
}
// Ceil - takes a datetime and return a datetime from the same day at 23:59:59 (UTC).
func Ceil(t time.Time) time.Time {
// add 24 hours so that we are dealing with tomorrow's datetime
// Floor
// Substract one second and we have today at 23:59:59
return Floor(t.Add(time.Hour * 24)).Add(time.Second * -1)
}
func BeforeOrEqual(milestone time.Time, date time.Time) bool {
return date.UTC().Before(milestone) || date.UTC().Equal(milestone)
}
func AfterOrEqual(milestone time.Time, date time.Time) bool {
return date.UTC().After(milestone) || date.UTC().Equal(milestone)
}
// Overlap - returns true if two date intervals overlap.
func Overlap(start1 time.Time, end1 time.Time, start2 time.Time, end2 time.Time) bool {
return (AfterOrEqual(start2, start1) && BeforeOrEqual(end2, start1)) ||
(AfterOrEqual(start2, end1) && BeforeOrEqual(end2, end1)) ||
(AfterOrEqual(start1, start2) && BeforeOrEqual(end1, start2)) ||
(AfterOrEqual(start1, end2) && BeforeOrEqual(end1, end2))
}

View File

@@ -0,0 +1,66 @@
package dateutil_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tx7do/go-utils/dateutil"
)
func TestFloor(t *testing.T) {
now := time.Now()
assert.Equal(t, "00:00:00", dateutil.Floor(now).Format("15:04:05"))
}
func TestCeil(t *testing.T) {
now := time.Now()
assert.Equal(t, "23:59:59", dateutil.Ceil(now).Format("15:04:05"))
}
func TestBeforeOrEqual(t *testing.T) {
milestone, _ := time.Parse("2006-01-02", "2023-01-01")
dBefore, _ := time.Parse("2006-01-02", "2022-12-31")
dEqual, _ := time.Parse("2006-01-02", "2023-01-01")
dAfter, _ := time.Parse("2006-01-02", "2023-01-31")
assert.Equal(t, true, dateutil.BeforeOrEqual(milestone, dBefore))
assert.Equal(t, true, dateutil.BeforeOrEqual(milestone, dEqual))
assert.Equal(t, false, dateutil.BeforeOrEqual(milestone, dAfter))
}
func TestAfterOrEqual(t *testing.T) {
milestone, _ := time.Parse("2006-01-02", "2023-01-01")
dBefore, _ := time.Parse("2006-01-02", "2022-12-31")
dEqual, _ := time.Parse("2006-01-02", "2023-01-01")
dAfter, _ := time.Parse("2006-01-02", "2023-01-31")
assert.Equal(t, false, dateutil.AfterOrEqual(milestone, dBefore))
assert.Equal(t, true, dateutil.AfterOrEqual(milestone, dEqual))
assert.Equal(t, true, dateutil.AfterOrEqual(milestone, dAfter))
}
func TestOverlap(t *testing.T) {
s1, _ := time.Parse("2006-01-02", "2022-12-28")
e1, _ := time.Parse("2006-01-02", "2022-12-31")
s2, _ := time.Parse("2006-01-02", "2022-12-30")
e2, _ := time.Parse("2006-01-02", "2023-01-01")
s3, _ := time.Parse("2006-01-02", "2023-01-02")
e3, _ := time.Parse("2006-01-02", "2023-01-04")
assert.Equal(t, true, dateutil.Overlap(s1, e1, s2, e2))
assert.Equal(t, false, dateutil.Overlap(s1, e1, s3, e3))
s4, _ := time.Parse("2006-01-02", "2023-07-13")
e4, _ := time.Parse("2006-01-02", "2023-07-14")
s5, _ := time.Parse("2006-01-02", "2023-07-10")
e5, _ := time.Parse("2006-01-02", "2023-07-17")
assert.Equal(t, true, dateutil.Overlap(s4, e4, s5, e5))
assert.Equal(t, true, dateutil.Overlap(s5, e5, s4, e4))
}

View File

@@ -1,23 +1,30 @@
package entgo
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"entgo.io/ent/dialect/sql"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
"github.com/XSAM/otelsql"
entSql "entgo.io/ent/dialect/sql"
)
type entClientInterface interface {
type EntClientInterface interface {
Close() error
}
type EntClient[T entClientInterface] struct {
type EntClient[T EntClientInterface] struct {
db T
drv *sql.Driver
drv *entSql.Driver
}
func NewEntClient[T entClientInterface](db T, drv *sql.Driver) *EntClient[T] {
func NewEntClient[T EntClientInterface](db T, drv *entSql.Driver) *EntClient[T] {
return &EntClient[T]{
db: db,
drv: drv,
@@ -28,28 +35,85 @@ func (c *EntClient[T]) Client() T {
return c.db
}
func (c *EntClient[T]) Driver() *sql.Driver {
func (c *EntClient[T]) Driver() *entSql.Driver {
return c.drv
}
func (c *EntClient[T]) Close() {
_ = c.db.Close()
func (c *EntClient[T]) DB() *sql.DB {
return c.drv.DB()
}
// Close 关闭数据库连接
func (c *EntClient[T]) Close() error {
return c.db.Close()
}
// Query 查询数据
func (c *EntClient[T]) Query(ctx context.Context, query string, args, v any) error {
return c.Driver().Query(ctx, query, args, v)
}
func (c *EntClient[T]) Exec(ctx context.Context, query string, args, v any) error {
return c.Driver().Exec(ctx, query, args, v)
}
// SetConnectionOption 设置连接配置
func (c *EntClient[T]) SetConnectionOption(maxIdleConnections, maxOpenConnections int, connMaxLifetime time.Duration) {
// 连接池中最多保留的空闲连接数量
c.DB().SetMaxIdleConns(maxIdleConnections)
// 连接池在同一时间打开连接的最大数量
c.DB().SetMaxOpenConns(maxOpenConnections)
// 连接可重用的最大时间长度
c.DB().SetConnMaxLifetime(connMaxLifetime)
}
func driverNameToSemConvKeyValue(driverName string) attribute.KeyValue {
switch driverName {
case "mariadb":
return semconv.DBSystemMariaDB
case "mysql":
return semconv.DBSystemMySQL
case "postgresql":
return semconv.DBSystemPostgreSQL
case "sqlite":
return semconv.DBSystemSqlite
default:
return semconv.DBSystemKey.String(driverName)
}
}
// CreateDriver 创建数据库驱动
func CreateDriver(driverName, dsn string, maxIdleConnections, maxOpenConnections int, connMaxLifetime time.Duration) (*sql.Driver, error) {
drv, err := sql.Open(driverName, dsn)
if err != nil {
func CreateDriver(driverName, dsn string, enableTrace, enableMetrics bool) (*entSql.Driver, error) {
var db *sql.DB
var drv *entSql.Driver
var err error
if enableTrace {
// Connect to database
if db, err = otelsql.Open(driverName, dsn, otelsql.WithAttributes(
driverNameToSemConvKeyValue(driverName),
)); err != nil {
return nil, errors.New(fmt.Sprintf("failed opening connection to db: %v", err))
}
db := drv.DB()
// 连接池中最多保留的空闲连接数量
db.SetMaxIdleConns(maxIdleConnections)
// 连接池在同一时间打开连接的最大数量
db.SetMaxOpenConns(maxOpenConnections)
// 连接可重用的最大时间长度
db.SetConnMaxLifetime(connMaxLifetime)
drv = entSql.OpenDB(driverName, db)
} else {
if drv, err = entSql.Open(driverName, dsn); err != nil {
return nil, errors.New(fmt.Sprintf("failed opening connection to db: %v", err))
}
db = drv.DB()
}
// Register DB stats to meter
if enableMetrics {
err = otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes(
driverNameToSemConvKeyValue(driverName),
))
if err != nil {
return nil, errors.New(fmt.Sprintf("failed register otel meter: %v", err))
}
}
return drv, nil
}

View File

@@ -1,36 +1,50 @@
module github.com/tx7do/kratos-utils/entgo
module github.com/tx7do/go-utils/entgo
go 1.20
go 1.23.0
toolchain go1.23.2
require (
entgo.io/contrib v0.4.5
entgo.io/ent v0.12.4
github.com/go-kratos/kratos/v2 v2.7.1
github.com/google/uuid v1.3.1
github.com/stretchr/testify v1.8.4
github.com/tx7do/kratos-utils v0.0.0-20231025044346-36a096df76d8
entgo.io/contrib v0.6.0
entgo.io/ent v0.14.4
github.com/XSAM/otelsql v0.38.0
github.com/go-kratos/kratos/v2 v2.8.4
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.10.0
github.com/tx7do/go-utils v1.1.27
go.opentelemetry.io/otel v1.36.0
google.golang.org/protobuf v1.36.6
)
require (
ariga.io/atlas v0.15.0 // indirect
ariga.io/atlas v0.33.1 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/bufbuild/protocompile v0.6.0 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/bufbuild/protocompile v0.14.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/hashicorp/hcl/v2 v2.19.1 // indirect
github.com/jhump/protoreflect v1.15.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/inflect v0.21.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/hcl/v2 v2.23.0 // indirect
github.com/jhump/protoreflect v1.17.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/zclconf/go-cty v1.14.1 // indirect
github.com/sony/sonyflake v1.2.1 // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

View File

@@ -1,74 +1,105 @@
ariga.io/atlas v0.15.0 h1:9lwSVcO/D3WgaCzstSGqR1hEDtsGibu6JqUofEI/0sY=
ariga.io/atlas v0.15.0/go.mod h1:isZrlzJ5cpoCoKFoY9knZug7Lq4pP1cm8g3XciLZ0Pw=
entgo.io/contrib v0.4.5 h1:BFaOHwFLE8WZjVJadP0XHCIaxgcC1BAtUvAyw7M/GHk=
entgo.io/contrib v0.4.5/go.mod h1:wpZyq2DJgthugFvDBlaqMXj9mV4/9ebyGEn7xlTVQqE=
entgo.io/ent v0.12.4 h1:LddPnAyxls/O7DTXZvUGDj0NZIdGSu317+aoNLJWbD8=
entgo.io/ent v0.12.4/go.mod h1:Y3JVAjtlIk8xVZYSn3t3mf8xlZIn5SAOXZQxD6kKI+Q=
ariga.io/atlas v0.33.1 h1:m3rn+m2jkfOZDqdZrgTDvDWh9G3Ldr77IElFDGEwDPk=
ariga.io/atlas v0.33.1/go.mod h1:WJesu2UCpGQvgUh3oVP94EiRT61nNy1W/VN5g+vqP1I=
entgo.io/contrib v0.6.0 h1:xfo4TbJE7sJZWx7BV7YrpSz7IPFvS8MzL3fnfzZjKvQ=
entgo.io/contrib v0.6.0/go.mod h1:3qWIseJ/9Wx2Hu5zVh15FDzv7d/UvKNcYKdViywWCQg=
entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=
entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/XSAM/otelsql v0.38.0 h1:zWU0/YM9cJhPE71zJcQ2EBHwQDp+G4AX2tPpljslaB8=
github.com/XSAM/otelsql v0.38.0/go.mod h1:5ePOgcLEkWvZtN9H3GV4BUlPeM3p3pzLDCnRG73X8h8=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY=
github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
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/go-kratos/kratos/v2 v2.7.1 h1:PNMUaWxS5ZGDp1EyID5ZosJb1OA/YiHnBxB0yUmocnc=
github.com/go-kratos/kratos/v2 v2.7.1/go.mod h1:CPn82O93OLHjtnbuyOKhAG5TkSvw+mFnL32c4lZFDwU=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs=
github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/inflect v0.21.2 h1:0gClGlGcxifcJR56zwvhaOulnNgnhc4qTAkob5ObnSM=
github.com/go-openapi/inflect v0.21.2/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI=
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls=
github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tx7do/kratos-utils v0.0.0-20231025044346-36a096df76d8 h1:G+s0DQ6zI0dGcvd9tRpXcYzelO0xRnp2jpFtQ+697Ko=
github.com/tx7do/kratos-utils v0.0.0-20231025044346-36a096df76d8/go.mod h1:rTeeqeABjK9HamIwKZ+uLozVIk/hGiEbNsi6rVr4l0w=
github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA=
github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sony/sonyflake v1.2.1 h1:Jzo4abS84qVNbYamXZdrZF1/6TzNJjEogRfXv7TsG48=
github.com/sony/sonyflake v1.2.1/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y=
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/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230629202037-9506855d4529 h1:9JucMWR7sPvCxUFd6UsOUNmA5kCcWOfORaT3tpAsKQs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 h1:DEH99RbiLZhMxrpEJCZ0A+wdTe0EOgou/poSLx9vWf4=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

21
entgo/mixin/creator_id.go Normal file
View File

@@ -0,0 +1,21 @@
package mixin
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
_mixin "entgo.io/ent/schema/mixin"
)
type CreatorId struct {
_mixin.Schema
}
func (CreatorId) Fields() []ent.Field {
return []ent.Field{
field.Uint64("creator_id").
Comment("创建者用户ID").
Immutable().
Optional().
Nillable(),
}
}

View File

@@ -0,0 +1,37 @@
package mixin
import (
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"entgo.io/ent/schema/mixin"
"github.com/tx7do/go-utils/sonyflake"
)
type SnowflackId struct {
mixin.Schema
}
func (SnowflackId) Fields() []ent.Field {
return []ent.Field{
field.Uint64("id").
Comment("id").
DefaultFunc(sonyflake.GenerateSonyflake).
Positive().
Immutable().
StructTag(`json:"id,omitempty"`).
SchemaType(map[string]string{
dialect.MySQL: "bigint",
dialect.Postgres: "bigint",
}),
}
}
// Indexes of the SnowflackId.
func (SnowflackId) Indexes() []ent.Index {
return []ent.Index{
index.Fields("id"),
}
}

33
entgo/mixin/string_id.go Normal file
View File

@@ -0,0 +1,33 @@
package mixin
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"entgo.io/ent/schema/mixin"
"regexp"
)
type StringId struct {
mixin.Schema
}
func (StringId) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Comment("id").
MaxLen(25).
NotEmpty().
Unique().
Immutable().
Match(regexp.MustCompile("^[0-9a-zA-Z_\\-]+$")).
StructTag(`json:"id,omitempty"`),
}
}
// Indexes of the StringId.
func (StringId) Indexes() []ent.Index {
return []ent.Index{
index.Fields("id"),
}
}

View File

@@ -29,6 +29,7 @@ func (SwitchStatus) Fields() []ent.Field {
// dialect.MySQL: "switch_status",
// dialect.Postgres: "switch_status",
//}).
Default("ON").
Values(
"OFF",
"ON",

View File

@@ -69,11 +69,11 @@ func (TimeAt) Fields() []ent.Field {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var _ ent.Mixin = (*CreatedTime)(nil)
var _ ent.Mixin = (*CreateTime)(nil)
type CreatedTime struct{ mixin.Schema }
type CreateTime struct{ mixin.Schema }
func (CreatedTime) Fields() []ent.Field {
func (CreateTime) Fields() []ent.Field {
return []ent.Field{
// 创建时间
field.Time("create_time").
@@ -86,11 +86,11 @@ func (CreatedTime) Fields() []ent.Field {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var _ ent.Mixin = (*UpdatedTime)(nil)
var _ ent.Mixin = (*UpdateTime)(nil)
type UpdatedTime struct{ mixin.Schema }
type UpdateTime struct{ mixin.Schema }
func (UpdatedTime) Fields() []ent.Field {
func (UpdateTime) Fields() []ent.Field {
return []ent.Field{
// 更新时间
field.Time("update_time").
@@ -102,11 +102,11 @@ func (UpdatedTime) Fields() []ent.Field {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var _ ent.Mixin = (*DeletedTime)(nil)
var _ ent.Mixin = (*DeleteTime)(nil)
type DeletedTime struct{ mixin.Schema }
type DeleteTime struct{ mixin.Schema }
func (DeletedTime) Fields() []ent.Field {
func (DeleteTime) Fields() []ent.Field {
return []ent.Field{
// 删除时间
field.Time("delete_time").
@@ -122,9 +122,9 @@ type Time struct{ mixin.Schema }
func (Time) Fields() []ent.Field {
var fields []ent.Field
fields = append(fields, CreatedTime{}.Fields()...)
fields = append(fields, UpdatedTime{}.Fields()...)
fields = append(fields, DeletedTime{}.Fields()...)
fields = append(fields, CreateTime{}.Fields()...)
fields = append(fields, UpdateTime{}.Fields()...)
fields = append(fields, DeleteTime{}.Fields()...)
return fields
}

View File

@@ -1,10 +1,11 @@
package mixin
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"
"time"
)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -74,3 +75,71 @@ func (Timestamp) Fields() []ent.Field {
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var _ ent.Mixin = (*CreatedAtTimestamp)(nil)
type CreatedAtTimestamp struct{ mixin.Schema }
func (CreatedAtTimestamp) Fields() []ent.Field {
return []ent.Field{
// 创建时间,毫秒
field.Int64("created_at").
Comment("创建时间").
Immutable().
Optional().
Nillable().
DefaultFunc(time.Now().UnixMilli),
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var _ ent.Mixin = (*UpdatedAtTimestamp)(nil)
type UpdatedAtTimestamp struct{ mixin.Schema }
func (UpdatedAtTimestamp) Fields() []ent.Field {
return []ent.Field{
// 更新时间,毫秒
// 需要注意的是,如果不是程序自动更新,那么这个字段不会被更新,除非在数据库里面下触发器更新。
field.Int64("updated_at").
Comment("更新时间").
Optional().
Nillable().
UpdateDefault(time.Now().UnixMilli),
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var _ ent.Mixin = (*DeletedAtTimestamp)(nil)
type DeletedAtTimestamp struct{ mixin.Schema }
func (DeletedAtTimestamp) Fields() []ent.Field {
return []ent.Field{
// 删除时间,毫秒
field.Int64("deleted_at").
Comment("删除时间").
Optional().
Nillable(),
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var _ ent.Mixin = (*TimestampAt)(nil)
type TimestampAt struct{ mixin.Schema }
func (TimestampAt) Fields() []ent.Field {
var fields []ent.Field
fields = append(fields, CreatedAtTimestamp{}.Fields()...)
fields = append(fields, UpdatedAtTimestamp{}.Fields()...)
fields = append(fields, DeletedAtTimestamp{}.Fields()...)
return fields
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@@ -3,13 +3,14 @@
## 通用列表查询请求
| 字段名 | 类型 | 格式 | 字段描述 | 示例 | 备注 |
|----------|-----------|-------------------------------------|---------|----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
|-----------|-----------|-------------------------------------|---------|----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| page | `number` | | 当前页码 | | 默认为`1`,最小值为`1`。 |
| pageSize | `number` | | 每页的行数 | | 默认为`10`,最小值为`1`。 |
| query | `string` | `json object``json object array` | AND过滤条件 | json字符串: `{"field1":"val1","field2":"val2"}` 或者`[{"field1":"val1"},{"field1":"val2"},{"field2":"val2"}]` | `map``array`都支持,当需要同字段名,不同值的情况下,请使用`array`。具体规则请见:[过滤规则](#过滤规则) |
| or | `string` | `json object``json object array` | OR过滤条件 | 同 AND过滤条件 | |
| orderBy | `string` | `json string array` | 排序条件 | json字符串`["-create_time", "type"]` | json的`string array`,字段名前加`-`是为降序,不加为升序。具体规则请见:[排序规则](#排序规则) |
| nopaging | `boolean` | | 是否不分页 | | 此字段为`true`时,`page``pageSize`字段的传入将无效用。 |
| noPaging | `boolean` | | 是否不分页 | | 此字段为`true`时,`page``pageSize`字段的传入将无效用。 |
| fieldMask | `string` | 其语法为使用逗号分隔字段名 | 字段掩码 | 例如id,realName,userName。 | 此字段是`SELECT`条件,为空的时候是为`*`。 |
## 排序规则
@@ -35,18 +36,19 @@
```text
{字段名}__{查找类型} : {值}
{字段名}.{JSON字段名}__{查找类型} : {值}
```
| 查找类型 | 示例 | SQL | 备注 |
|-------------|---------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
|-------------|---------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| not | `{"name__not" : "tom"}` | `WHERE NOT ("name" = "tom")` | |
| in | `{"name__in" : ["tom", "jimmy"]}` | `WHERE name IN ("tom", "jimmy")` | |
| not_in | `{"name__not_in" : ["tom", "jimmy"]}` | `WHERE name NOT IN ("tom", "jimmy")` | |
| in | `{"name__in" : "[\"tom\", \"jimmy\"]"}` | `WHERE name IN ("tom", "jimmy")` | |
| not_in | `{"name__not_in" : "[\"tom\", \"jimmy\"]"}` | `WHERE name NOT IN ("tom", "jimmy")` | |
| gte | `{"create_time__gte" : "2023-10-25"}` | `WHERE "create_time" >= "2023-10-25"` | |
| gt | `{"create_time__gt" : "2023-10-25"}` | `WHERE "create_time" > "2023-10-25"` | |
| lte | `{"create_time__lte" : "2023-10-25"}` | `WHERE "create_time" <= "2023-10-25"` | |
| lt | `{"create_time__lt" : "2023-10-25"}` | `WHERE "create_time" < "2023-10-25"` | |
| range | `{"create_time__range" : ["2023-10-25", "2024-10-25"]}` | `WHERE "create_time" BETWEEN "2023-10-25" AND "2024-10-25"` <br><br> `WHERE "create_time" >= "2023-10-25" AND "create_time" <= "2024-10-25"` | 需要注意的是: <br>1. 有些数据库的BETWEEN实现的开闭区间可能不一样。<br>2. 日期`2005-01-01`会被隐式转换为:`2005-01-01 00:00:00`,两个日期一致就会导致查询不到数据。 |
| range | `{"create_time__range" : "[\"2023-10-25\", \"2024-10-25\"]"}` | `WHERE "create_time" BETWEEN "2023-10-25" AND "2024-10-25"` <br><br> `WHERE "create_time" >= "2023-10-25" AND "create_time" <= "2024-10-25"` | 需要注意的是: <br>1. 有些数据库的BETWEEN实现的开闭区间可能不一样。<br>2. 日期`2005-01-01`会被隐式转换为:`2005-01-01 00:00:00`,两个日期一致就会导致查询不到数据。 |
| isnull | `{"name__isnull" : "True"}` | `WHERE name IS NULL` | |
| not_isnull | `{"name__not_isnull" : "False"}` | `WHERE name IS NOT NULL` | |
| contains | `{"name__contains" : "L"}` | `WHERE name LIKE '%L%';` | |

View File

@@ -1,56 +1,173 @@
package entgo
import (
"encoding/json"
"fmt"
"strings"
"encoding/json"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"github.com/go-kratos/kratos/v2/encoding"
"github.com/tx7do/kratos-utils/stringcase"
"github.com/tx7do/go-utils/stringcase"
)
const (
FilterNot = "not" // 不等于
FilterIn = "in" // 检查值是否在列表中
FilterNotIn = "not_in" // 不在列表中
FilterGTE = "gte" // 大于或等于传递的值
FilterGT = "gt" // 大于传递值
FilterLTE = "lte" // 小于或等于传递值
FilterLT = "lt" // 小于传递值
FilterRange = "range" // 是否介于和给定的两个值之间
FilterIsNull = "isnull" // 是否为空
FilterNotIsNull = "not_isnull" // 是否不为空
FilterContains = "contains" // 是否包含指定的子字符串
FilterInsensitiveContains = "icontains" // 不区分大小写,是否包含指定的子字符串
FilterStartsWith = "startswith" // 以值开头
FilterInsensitiveStartsWith = "istartswith" // 不区分大小写,以值开头
FilterEndsWith = "endswith" // 以值结尾
FilterInsensitiveEndsWith = "iendswith" // 不区分大小写,以值结尾
FilterExact = "exact" // 精确匹配
FilterInsensitiveExact = "iexact" // 不区分大小写,精确匹配
FilterRegex = "regex" // 正则表达式
FilterInsensitiveRegex = "iregex" // 不区分大小写,正则表达式
FilterSearch = "search" // 全文搜索
)
type FilterOp int
const (
FilterDatePartDate = "date" // 日期
FilterDatePartYear = "year" //
FilterDatePartISOYear = "iso_year" // ISO 8601 一年中的周数
FilterDatePartQuarter = "quarter" // 季度
FilterDatePartMonth = "month" // 月
FilterDatePartWeek = "week" // ISO 8601 周编号 一年中的周数
FilterDatePartWeekDay = "week_day" // 星期几
FilterDatePartISOWeekDay = "iso_week_day" // 星期几
FilterDatePartDay = "day" // 日
FilterDatePartTime = "time" // 小时:分钟:秒
FilterDatePartHour = "hour" // 小时
FilterDatePartMinute = "minute" // 分钟
FilterDatePartSecond = "second" //
FilterDatePartMicrosecond = "microsecond" // 微秒
FilterNot DatePart = iota // 不等于
FilterIn // 检查值是否在列表中
FilterNotIn // 不在列表中
FilterGTE // 大于或等于传递的值
FilterGT // 大于传递值
FilterLTE // 小于或等于传递值
FilterLT // 小于传递值
FilterRange // 是否介于和给定的两个值之间
FilterIsNull // 是否为空
FilterNotIsNull // 是否不为空
FilterContains // 是否包含指定的子字符串
FilterInsensitiveContains // 不区分大小写,是否包含指定的子字符串
FilterStartsWith // 以值开头
FilterInsensitiveStartsWith // 不区分大小写,以值开头
FilterEndsWith // 以值结尾
FilterInsensitiveEndsWith // 不区分大小写,以值结尾
FilterExact // 精确匹配
FilterInsensitiveExact // 不区分大小写,精确匹配
FilterRegex // 正则表达式
FilterInsensitiveRegex // 不区分大小写,正则表达式
FilterSearch // 全文搜索
)
var ops = [...]string{
FilterNot: "not",
FilterIn: "in",
FilterNotIn: "not_in",
FilterGTE: "gte",
FilterGT: "gt",
FilterLTE: "lte",
FilterLT: "lt",
FilterRange: "range",
FilterIsNull: "isnull",
FilterNotIsNull: "not_isnull",
FilterContains: "contains",
FilterInsensitiveContains: "icontains",
FilterStartsWith: "startswith",
FilterInsensitiveStartsWith: "istartswith",
FilterEndsWith: "endswith",
FilterInsensitiveEndsWith: "iendswith",
FilterExact: "exact",
FilterInsensitiveExact: "iexact",
FilterRegex: "regex",
FilterInsensitiveRegex: "iregex",
FilterSearch: "search",
}
type DatePart int
const (
DatePartDate DatePart = iota // 日期
DatePartYear // 年
DatePartISOYear // ISO 8601 一年中的周数
DatePartQuarter // 季度
DatePartMonth // 月
DatePartWeek // ISO 8601 周编号 一年中的周数
DatePartWeekDay // 星期几
DatePartISOWeekDay // 星期几
DatePartDay // 日
DatePartTime // 小时:分钟:秒
DatePartHour // 小时
DatePartMinute // 分钟
DatePartSecond // 秒
DatePartMicrosecond // 微秒
)
var dateParts = [...]string{
DatePartDate: "date",
DatePartYear: "year",
DatePartISOYear: "iso_year",
DatePartQuarter: "quarter",
DatePartMonth: "month",
DatePartWeek: "week",
DatePartWeekDay: "week_day",
DatePartISOWeekDay: "iso_week_day",
DatePartDay: "day",
DatePartTime: "time",
DatePartHour: "hour",
DatePartMinute: "minute",
DatePartSecond: "second",
DatePartMicrosecond: "microsecond",
}
const (
QueryDelimiter = "__" // 分隔符
JsonFieldDelimiter = "." // JSONB字段分隔符
)
// splitQueryKey 分割查询键
func splitQueryKey(key string) []string {
return strings.Split(key, QueryDelimiter)
}
// splitJsonFieldKey 分割JSON字段键
func splitJsonFieldKey(key string) []string {
return strings.Split(key, JsonFieldDelimiter)
}
// isJsonFieldKey 是否为JSON字段键
func isJsonFieldKey(key string) bool {
return strings.Contains(key, JsonFieldDelimiter)
}
// hasOperations 是否有操作
func hasOperations(str string) bool {
str = strings.ToLower(str)
for _, item := range ops {
if str == item {
return true
}
}
return false
}
// hasDatePart 是否有日期部分
func hasDatePart(str string) bool {
str = strings.ToLower(str)
for _, item := range dateParts {
if str == item {
return true
}
}
return false
}
// BuildFilterSelector 构建过滤选择器
func BuildFilterSelector(andFilterJsonString, orFilterJsonString string) (error, []func(s *sql.Selector)) {
var err error
var queryConditions []func(s *sql.Selector)
var andSelector func(s *sql.Selector)
err, andSelector = QueryCommandToWhereConditions(andFilterJsonString, false)
if err != nil {
return err, nil
}
if andSelector != nil {
queryConditions = append(queryConditions, andSelector)
}
var orSelector func(s *sql.Selector)
err, orSelector = QueryCommandToWhereConditions(orFilterJsonString, true)
if err != nil {
return err, nil
}
if orSelector != nil {
queryConditions = append(queryConditions, orSelector)
}
return nil, queryConditions
}
// QueryCommandToWhereConditions 查询命令转换为选择条件
func QueryCommandToWhereConditions(strJson string, isOr bool) (error, func(s *sql.Selector)) {
if len(strJson) == 0 {
@@ -82,14 +199,13 @@ func QueryCommandToWhereConditions(strJson string, isOr bool) (error, func(s *sq
}
}
// processQueryMap 处理查询映射表
func processQueryMap(s *sql.Selector, queryMap map[string]string) []*sql.Predicate {
var ps []*sql.Predicate
for k, v := range queryMap {
key := stringcase.ToSnakeCase(k)
keys := splitQueryKey(k)
keys := strings.Split(key, "__")
if cond := oneFieldFilter(s, keys, v); cond != nil {
if cond := makeFieldFilter(s, keys, v); cond != nil {
ps = append(ps, cond)
}
}
@@ -97,188 +213,243 @@ func processQueryMap(s *sql.Selector, queryMap map[string]string) []*sql.Predica
return ps
}
func BuildFilterSelector(andFilterJsonString, orFilterJsonString string) (error, []func(s *sql.Selector)) {
var err error
var queryConditions []func(s *sql.Selector)
var andSelector func(s *sql.Selector)
err, andSelector = QueryCommandToWhereConditions(andFilterJsonString, false)
if err != nil {
return err, nil
}
if andSelector != nil {
queryConditions = append(queryConditions, andSelector)
}
var orSelector func(s *sql.Selector)
err, orSelector = QueryCommandToWhereConditions(orFilterJsonString, true)
if err != nil {
return err, nil
}
if orSelector != nil {
queryConditions = append(queryConditions, orSelector)
}
return nil, queryConditions
}
func oneFieldFilter(s *sql.Selector, keys []string, value string) *sql.Predicate {
var cond *sql.Predicate
if len(keys) == 1 {
field := keys[0]
cond = filterEqual(s, field, value)
} else if len(keys) == 2 {
if len(keys[0]) == 0 {
// makeFieldFilter 构建一个字段过滤器
func makeFieldFilter(s *sql.Selector, keys []string, value string) *sql.Predicate {
if len(keys) == 0 {
return nil
}
if len(value) == 0 {
return nil
}
field := keys[0]
op := strings.ToLower(keys[1])
switch op {
case FilterNot:
cond = filterNot(s, field, value)
case FilterIn:
cond = filterIn(s, field, value)
case FilterNotIn:
cond = filterNotIn(s, field, value)
case FilterGTE:
cond = filterGTE(s, field, value)
case FilterGT:
cond = filterGT(s, field, value)
case FilterLTE:
cond = filterLTE(s, field, value)
case FilterLT:
cond = filterLT(s, field, value)
case FilterRange:
cond = filterRange(s, field, value)
case FilterIsNull:
cond = filterIsNull(s, field, value)
case FilterNotIsNull:
cond = filterNotIsNull(s, field, value)
case FilterContains:
cond = filterContains(s, field, value)
case FilterInsensitiveContains:
cond = filterInsensitiveContains(s, field, value)
case FilterStartsWith:
cond = filterStartsWith(s, field, value)
case FilterInsensitiveStartsWith:
cond = filterInsensitiveStartsWith(s, field, value)
case FilterEndsWith:
cond = filterEndsWith(s, field, value)
case FilterInsensitiveEndsWith:
cond = filterInsensitiveEndsWith(s, field, value)
case FilterExact:
cond = filterExact(s, field, value)
case FilterInsensitiveExact:
cond = filterInsensitiveExact(s, field, value)
case FilterRegex:
cond = filterRegex(s, field, value)
case FilterInsensitiveRegex:
cond = filterInsensitiveRegex(s, field, value)
case FilterSearch:
cond = filterSearch(s, field, value)
if len(field) == 0 {
return nil
}
p := sql.P()
switch len(keys) {
case 1:
if isJsonFieldKey(field) {
jsonFields := splitJsonFieldKey(field)
if len(jsonFields) != 2 {
field = stringcase.ToSnakeCase(field)
return filterEqual(s, p, field, value)
}
//value = "'" + value + "'"
return filterJsonb(
s, p,
stringcase.ToSnakeCase(jsonFields[1]),
stringcase.ToSnakeCase(jsonFields[0]),
).
EQ("", value)
}
field = stringcase.ToSnakeCase(field)
return filterEqual(s, p, field, value)
case 2:
op := keys[1]
if len(op) == 0 {
return nil
}
if isJsonFieldKey(field) {
jsonFields := splitJsonFieldKey(field)
if len(jsonFields) == 2 {
field = filterJsonbField(s,
stringcase.ToSnakeCase(jsonFields[1]),
stringcase.ToSnakeCase(jsonFields[0]),
)
//value = "'" + value + "'"
}
} else {
field = stringcase.ToSnakeCase(field)
}
var cond *sql.Predicate
if hasOperations(op) {
return processOp(s, p, op, field, value)
} else if hasDatePart(op) {
cond = filterDatePart(s, p, op, field).EQ("", value)
} else {
cond = filterJsonb(s, p, op, field).EQ("", value)
}
return cond
case 3:
op1 := keys[1]
if len(op1) == 0 {
return nil
}
op2 := keys[2]
if len(op2) == 0 {
return nil
}
// 第二个参数要么是提取日期要么是json字段。
//var cond *sql.Predicate
if hasDatePart(op1) {
if isJsonFieldKey(field) {
jsonFields := splitJsonFieldKey(field)
if len(jsonFields) == 2 {
field = filterJsonbField(s, jsonFields[1], jsonFields[0])
//value = "'" + value + "'"
}
} else {
field = stringcase.ToSnakeCase(field)
}
str := filterDatePartField(s, op1, field)
if hasOperations(op2) {
return processOp(s, p, op2, str, value)
}
return nil
} else {
str := filterJsonbField(s, op1, field)
if hasOperations(op2) {
return processOp(s, p, op2, str, value)
} else if hasDatePart(op2) {
return filterDatePart(s, p, op2, str)
}
return nil
}
default:
cond = filterDatePart(s, op, field, value)
return nil
}
}
func processOp(s *sql.Selector, p *sql.Predicate, op, field, value string) *sql.Predicate {
var cond *sql.Predicate
switch op {
case ops[FilterNot]:
cond = filterNot(s, p, field, value)
case ops[FilterIn]:
cond = filterIn(s, p, field, value)
case ops[FilterNotIn]:
cond = filterNotIn(s, p, field, value)
case ops[FilterGTE]:
cond = filterGTE(s, p, field, value)
case ops[FilterGT]:
cond = filterGT(s, p, field, value)
case ops[FilterLTE]:
cond = filterLTE(s, p, field, value)
case ops[FilterLT]:
cond = filterLT(s, p, field, value)
case ops[FilterRange]:
cond = filterRange(s, p, field, value)
case ops[FilterIsNull]:
cond = filterIsNull(s, p, field, value)
case ops[FilterNotIsNull]:
cond = filterIsNotNull(s, p, field, value)
case ops[FilterContains]:
cond = filterContains(s, p, field, value)
case ops[FilterInsensitiveContains]:
cond = filterInsensitiveContains(s, p, field, value)
case ops[FilterStartsWith]:
cond = filterStartsWith(s, p, field, value)
case ops[FilterInsensitiveStartsWith]:
cond = filterInsensitiveStartsWith(s, p, field, value)
case ops[FilterEndsWith]:
cond = filterEndsWith(s, p, field, value)
case ops[FilterInsensitiveEndsWith]:
cond = filterInsensitiveEndsWith(s, p, field, value)
case ops[FilterExact]:
cond = filterExact(s, p, field, value)
case ops[FilterInsensitiveExact]:
cond = filterInsensitiveExact(s, p, field, value)
case ops[FilterRegex]:
cond = filterRegex(s, p, field, value)
case ops[FilterInsensitiveRegex]:
cond = filterInsensitiveRegex(s, p, field, value)
case ops[FilterSearch]:
cond = filterSearch(s, p, field, value)
default:
return nil
}
return cond
}
// filterEqual = 相等操作
// SQL: WHERE "name" = "tom"
func filterEqual(s *sql.Selector, field, value string) *sql.Predicate {
return sql.EQ(s.C(field), value)
func filterEqual(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.EQ(s.C(field), value)
}
// filterNot NOT 不相等操作
// SQL: WHERE NOT ("name" = "tom")
// 或者: WHERE "name" <> "tom"
// 用NOT可以过滤出NULL而用<>、!=则不能。
func filterNot(s *sql.Selector, field, value string) *sql.Predicate {
return sql.Not(sql.EQ(s.C(field), value))
func filterNot(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.Not().EQ(s.C(field), value)
}
// filterIn IN操作
// SQL: WHERE name IN ("tom", "jimmy")
func filterIn(s *sql.Selector, field, value string) *sql.Predicate {
var strs []string
if err := json.Unmarshal([]byte(value), &strs); err == nil {
return sql.In(s.C(field), strs)
func filterIn(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
var values []any
if err := json.Unmarshal([]byte(value), &values); err == nil {
return p.In(s.C(field), values...)
}
var float64s []float64
if err := json.Unmarshal([]byte(value), &float64s); err == nil {
return sql.In(s.C(field), strs)
}
return nil
}
// filterNotIn NOT IN操作
// SQL: WHERE name NOT IN ("tom", "jimmy")`
func filterNotIn(s *sql.Selector, field, value string) *sql.Predicate {
var strs []string
if err := json.Unmarshal([]byte(value), &strs); err == nil {
return sql.NotIn(s.C(field), strs)
func filterNotIn(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
var values []any
if err := json.Unmarshal([]byte(value), &values); err == nil {
return p.NotIn(s.C(field), values...)
}
var float64s []float64
if err := json.Unmarshal([]byte(value), &float64s); err == nil {
return sql.NotIn(s.C(field), strs)
}
return nil
}
// filterGTE GTE (Greater Than or Equal) 大于等于 >=操作
// SQL: WHERE "create_time" >= "2023-10-25"
func filterGTE(s *sql.Selector, field, value string) *sql.Predicate {
return sql.GTE(s.C(field), value)
func filterGTE(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.GTE(s.C(field), value)
}
// filterGT GT (Greater than) 大于 >操作
// SQL: WHERE "create_time" > "2023-10-25"
func filterGT(s *sql.Selector, field, value string) *sql.Predicate {
return sql.GT(s.C(field), value)
func filterGT(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.GT(s.C(field), value)
}
// filterLTE LTE (Less Than or Equal) 小于等于 <=操作
// SQL: WHERE "create_time" <= "2023-10-25"
func filterLTE(s *sql.Selector, field, value string) *sql.Predicate {
return sql.LTE(s.C(field), value)
func filterLTE(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.LTE(s.C(field), value)
}
// filterLT LT (Less than) 小于 <操作
// SQL: WHERE "create_time" < "2023-10-25"
func filterLT(s *sql.Selector, field, value string) *sql.Predicate {
return sql.LT(s.C(field), value)
func filterLT(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.LT(s.C(field), value)
}
// filterRange 在值域之中 BETWEEN操作
// SQL: WHERE "create_time" BETWEEN "2023-10-25" AND "2024-10-25"
// 或者: WHERE "create_time" >= "2023-10-25" AND "create_time" <= "2024-10-25"
func filterRange(s *sql.Selector, field, value string) *sql.Predicate {
var strs []string
if err := json.Unmarshal([]byte(value), &strs); err == nil {
if len(strs) != 2 {
func filterRange(s *sql.Selector, _ *sql.Predicate, field, value string) *sql.Predicate {
var values []any
if err := json.Unmarshal([]byte(value), &values); err == nil {
if len(values) != 2 {
return nil
}
return sql.And(
sql.GTE(s.C(field), strs[0]),
sql.LTE(s.C(field), strs[1]),
)
}
var float64s []float64
if err := json.Unmarshal([]byte(value), &float64s); err == nil {
if len(float64s) != 2 {
return nil
}
return sql.And(
sql.GTE(s.C(field), float64s[0]),
sql.LTE(s.C(field), float64s[1]),
sql.GTE(s.C(field), values[0]),
sql.LTE(s.C(field), values[1]),
)
}
@@ -287,90 +458,216 @@ func filterRange(s *sql.Selector, field, value string) *sql.Predicate {
// filterIsNull 为空 IS NULL操作
// SQL: WHERE name IS NULL
func filterIsNull(s *sql.Selector, field, _ string) *sql.Predicate {
return sql.IsNull(s.C(field))
func filterIsNull(s *sql.Selector, p *sql.Predicate, field, _ string) *sql.Predicate {
return p.IsNull(s.C(field))
}
// filterNotIsNull 不为空 IS NOT NULL操作
// filterIsNotNull 不为空 IS NOT NULL操作
// SQL: WHERE name IS NOT NULL
func filterNotIsNull(s *sql.Selector, field, _ string) *sql.Predicate {
return sql.Not(sql.IsNull(s.C(field)))
func filterIsNotNull(s *sql.Selector, p *sql.Predicate, field, _ string) *sql.Predicate {
return p.Not().IsNull(s.C(field))
}
// filterContains LIKE 前后模糊查询
// SQL: WHERE name LIKE '%L%';
func filterContains(s *sql.Selector, field, value string) *sql.Predicate {
return sql.Contains(s.C(field), value)
func filterContains(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.Contains(s.C(field), value)
}
// filterInsensitiveContains ILIKE 前后模糊查询
// SQL: WHERE name ILIKE '%L%';
func filterInsensitiveContains(s *sql.Selector, field, value string) *sql.Predicate {
return sql.ContainsFold(s.C(field), value)
func filterInsensitiveContains(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.ContainsFold(s.C(field), value)
}
// filterStartsWith LIKE 前缀+模糊查询
// SQL: WHERE name LIKE 'La%';
func filterStartsWith(s *sql.Selector, field, value string) *sql.Predicate {
return sql.HasPrefix(s.C(field), value)
func filterStartsWith(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.HasPrefix(s.C(field), value)
}
// filterInsensitiveStartsWith ILIKE 前缀+模糊查询
// SQL: WHERE name ILIKE 'La%';
func filterInsensitiveStartsWith(s *sql.Selector, field, value string) *sql.Predicate {
return sql.EqualFold(s.C(field), value+"%")
func filterInsensitiveStartsWith(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.EqualFold(s.C(field), value+"%")
}
// filterEndsWith LIKE 后缀+模糊查询
// SQL: WHERE name LIKE '%a';
func filterEndsWith(s *sql.Selector, field, value string) *sql.Predicate {
return sql.HasSuffix(s.C(field), value)
func filterEndsWith(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.HasSuffix(s.C(field), value)
}
// filterInsensitiveEndsWith ILIKE 后缀+模糊查询
// SQL: WHERE name ILIKE '%a';
func filterInsensitiveEndsWith(s *sql.Selector, field, value string) *sql.Predicate {
return sql.EqualFold(s.C(field), "%"+value)
func filterInsensitiveEndsWith(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.EqualFold(s.C(field), "%"+value)
}
// filterExact LIKE 操作 精确比对
// SQL: WHERE name LIKE 'a';
func filterExact(s *sql.Selector, field, value string) *sql.Predicate {
return sql.Like(s.C(field), value)
func filterExact(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.Like(s.C(field), value)
}
// filterInsensitiveExact ILIKE 操作 不区分大小写,精确比对
// SQL: WHERE name ILIKE 'a';
func filterInsensitiveExact(s *sql.Selector, field, value string) *sql.Predicate {
return sql.EqualFold(s.C(field), value)
func filterInsensitiveExact(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
return p.EqualFold(s.C(field), value)
}
// filterRegex LIKE 操作 精确比对
// filterRegex 正则查找
// MySQL: WHERE title REGEXP BINARY '^(An?|The) +'
// Oracle: WHERE REGEXP_LIKE(title, '^(An?|The) +', 'c');
// PostgreSQL: WHERE title ~ '^(An?|The) +';
// SQLite: WHERE title REGEXP '^(An?|The) +';
func filterRegex(s *sql.Selector, field, value string) *sql.Predicate {
return nil
func filterRegex(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
p.Append(func(b *sql.Builder) {
switch s.Builder.Dialect() {
case dialect.Postgres:
b.Ident(s.C(field)).WriteString(" ~ ")
b.Arg(value)
break
case dialect.MySQL:
b.Ident(s.C(field)).WriteString(" REGEXP BINARY ")
b.Arg(value)
break
case dialect.SQLite:
b.Ident(s.C(field)).WriteString(" REGEXP ")
b.Arg(value)
break
case dialect.Gremlin:
break
}
})
return p
}
// filterInsensitiveRegex ILIKE 操作 不区分大小写,精确比对
// filterInsensitiveRegex 正则查找 不区分大小写
// MySQL: WHERE title REGEXP '^(an?|the) +'
// Oracle: WHERE REGEXP_LIKE(title, '^(an?|the) +', 'i');
// PostgreSQL: WHERE title ~* '^(an?|the) +';
// SQLite: WHERE title REGEXP '(?i)^(an?|the) +';
func filterInsensitiveRegex(s *sql.Selector, field, value string) *sql.Predicate {
return nil
func filterInsensitiveRegex(s *sql.Selector, p *sql.Predicate, field, value string) *sql.Predicate {
p.Append(func(b *sql.Builder) {
switch s.Builder.Dialect() {
case dialect.Postgres:
b.Ident(s.C(field)).WriteString(" ~* ")
b.Arg(strings.ToLower(value))
break
case dialect.MySQL:
b.Ident(s.C(field)).WriteString(" REGEXP ")
b.Arg(strings.ToLower(value))
break
case dialect.SQLite:
b.Ident(s.C(field)).WriteString(" REGEXP ")
if !strings.HasPrefix(value, "(?i)") {
value = "(?i)" + value
}
b.Arg(strings.ToLower(value))
break
case dialect.Gremlin:
break
}
})
return p
}
// filterSearch 全文搜索
// SQL:
func filterSearch(s *sql.Selector, _, _ string) *sql.Predicate {
return nil
func filterSearch(s *sql.Selector, p *sql.Predicate, _, _ string) *sql.Predicate {
p.Append(func(b *sql.Builder) {
switch s.Builder.Dialect() {
}
})
return p
}
// filterDatePart 时间戳提取日期 select extract(quarter from timestamp '2018-08-15 12:10:10');
// SQL:
func filterDatePart(s *sql.Selector, datePart, field, value string) *sql.Predicate {
return nil
// filterDatePart 时间戳提取日期
// SQL: select extract(quarter from timestamp '2018-08-15 12:10:10');
func filterDatePart(s *sql.Selector, p *sql.Predicate, datePart, field string) *sql.Predicate {
p.Append(func(b *sql.Builder) {
switch s.Builder.Dialect() {
case dialect.Postgres:
str := fmt.Sprintf("EXTRACT('%s' FROM %s)", strings.ToUpper(datePart), s.C(field))
b.WriteString(str)
//b.Arg(strings.ToLower(value))
break
case dialect.MySQL:
str := fmt.Sprintf("%s(%s)", strings.ToUpper(datePart), s.C(field))
b.WriteString(str)
//b.Arg(strings.ToLower(value))
break
}
})
return p
}
// filterDatePartField 日期
func filterDatePartField(s *sql.Selector, datePart, field string) string {
p := sql.P()
switch s.Builder.Dialect() {
case dialect.Postgres:
str := fmt.Sprintf("EXTRACT('%s' FROM %s)", strings.ToUpper(datePart), s.C(field))
p.WriteString(str)
break
case dialect.MySQL:
str := fmt.Sprintf("%s(%s)", strings.ToUpper(datePart), s.C(field))
p.WriteString(str)
break
}
return p.String()
}
// filterJsonb 提取JSONB字段
// Postgresql: WHERE ("app_profile"."preferences" ->> 'daily_email') = 'true'
func filterJsonb(s *sql.Selector, p *sql.Predicate, jsonbField, field string) *sql.Predicate {
field = stringcase.ToSnakeCase(field)
p.Append(func(b *sql.Builder) {
switch s.Builder.Dialect() {
case dialect.Postgres:
b.Ident(s.C(field)).WriteString(" ->> ").WriteString("'" + jsonbField + "'")
//b.Arg(strings.ToLower(value))
break
case dialect.MySQL:
str := fmt.Sprintf("JSON_EXTRACT(%s, '$.%s')", s.C(field), jsonbField)
b.WriteString(str)
//b.Arg(strings.ToLower(value))
break
}
})
return p
}
// filterJsonbField JSONB字段
func filterJsonbField(s *sql.Selector, jsonbField, field string) string {
field = stringcase.ToSnakeCase(field)
p := sql.P()
switch s.Builder.Dialect() {
case dialect.Postgres:
p.Ident(s.C(field)).WriteString(" ->> ").WriteString("'" + jsonbField + "'")
//b.Arg(strings.ToLower(value))
break
case dialect.MySQL:
str := fmt.Sprintf("JSON_EXTRACT(%s, '$.%s')", s.C(field), jsonbField)
p.WriteString(str)
//b.Arg(strings.ToLower(value))
break
}
return p.String()
}

784
entgo/query/filter_test.go Normal file
View File

@@ -0,0 +1,784 @@
package entgo
import (
"fmt"
"testing"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"github.com/stretchr/testify/require"
)
func TestFilter(t *testing.T) {
t.Run("MySQL_FilterEqual", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterEqual(s, p, "name", "tom")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "tom")
})
t.Run("PostgreSQL_FilterEqual", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterEqual(s, p, "name", "tom")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" = $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "tom")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterNot", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterNot(s, p, "name", "tom")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE NOT `users`.`name` = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "tom")
})
t.Run("PostgreSQL_FilterNot", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterNot(s, p, "name", "tom")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE NOT \"users\".\"name\" = $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "tom")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterIn", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterIn(s, p, "name", "[\"tom\", \"jimmy\", 123]")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` IN (?, ?, ?)", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "tom")
require.Equal(t, args[1], "jimmy")
require.Equal(t, args[2], float64(123))
})
t.Run("PostgreSQL_FilterIn", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterIn(s, p, "name", "[\"tom\", \"jimmy\", 123]")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" IN ($1, $2, $3)", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "tom")
require.Equal(t, args[1], "jimmy")
require.Equal(t, args[2], float64(123))
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterNotIn", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterNotIn(s, p, "name", "[\"tom\", \"jimmy\", 123]")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` NOT IN (?, ?, ?)", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "tom")
require.Equal(t, args[1], "jimmy")
require.Equal(t, args[2], float64(123))
})
t.Run("PostgreSQL_FilterNotIn", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterNotIn(s, p, "name", "[\"tom\", \"jimmy\", 123]")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" NOT IN ($1, $2, $3)", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "tom")
require.Equal(t, args[1], "jimmy")
require.Equal(t, args[2], float64(123))
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterGTE", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterGTE(s, p, "create_time", "2023-10-25")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`create_time` >= ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
})
t.Run("PostgreSQL_FilterGTE", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterGTE(s, p, "create_time", "2023-10-25")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"create_time\" >= $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterGT", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterGT(s, p, "create_time", "2023-10-25")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`create_time` > ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
})
t.Run("PostgreSQL_FilterGT", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterGT(s, p, "create_time", "2023-10-25")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"create_time\" > $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterLTE", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterLTE(s, p, "create_time", "2023-10-25")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`create_time` <= ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
})
t.Run("PostgreSQL_FilterLTE", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterLTE(s, p, "create_time", "2023-10-25")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"create_time\" <= $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterLT", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterLT(s, p, "create_time", "2023-10-25")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`create_time` < ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
})
t.Run("PostgreSQL_FilterLT", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterLT(s, p, "create_time", "2023-10-25")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"create_time\" < $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterRange", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterRange(s, p, "create_time", "[\"2023-10-25\", \"2024-10-25\"]")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`create_time` >= ? AND `users`.`create_time` <= ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
require.Equal(t, args[1], "2024-10-25")
})
t.Run("PostgreSQL_FilterRange", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterRange(s, p, "create_time", "[\"2023-10-25\", \"2024-10-25\"]")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"create_time\" >= $1 AND \"users\".\"create_time\" <= $2", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-10-25")
require.Equal(t, args[1], "2024-10-25")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterIsNull", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterIsNull(s, p, "name", "true")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` IS NULL", query)
require.Empty(t, args)
})
t.Run("PostgreSQL_FilterIsNull", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterIsNull(s, p, "name", "true")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" IS NULL", query)
require.Empty(t, args)
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterIsNotNull", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterIsNotNull(s, p, "name", "true")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE NOT `users`.`name` IS NULL", query)
require.Empty(t, args)
})
t.Run("PostgreSQL_FilterIsNotNull", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterIsNotNull(s, p, "name", "true")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE NOT \"users\".\"name\" IS NULL", query)
require.Empty(t, args)
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterContains", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterContains(s, p, "name", "L")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` LIKE ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "%L%")
})
t.Run("PostgreSQL_FilterContains", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterContains(s, p, "name", "L")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" LIKE $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "%L%")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterInsensitiveContains", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveContains(s, p, "name", "L")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci LIKE ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "%l%")
})
t.Run("PostgreSQL_FilterInsensitiveContains", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveContains(s, p, "name", "L")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" ILIKE $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "%l%")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterStartsWith", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterStartsWith(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` LIKE ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "La%")
})
t.Run("PostgreSQL_FilterStartsWith", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterStartsWith(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" LIKE $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "La%")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterInsensitiveStartsWith", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveStartsWith(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "la%")
})
t.Run("PostgreSQL_FilterInsensitiveStartsWith", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveStartsWith(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" ILIKE $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "la\\%")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterEndsWith", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterEndsWith(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` LIKE ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "%La")
})
t.Run("PostgreSQL_FilterEndsWith", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterEndsWith(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" LIKE $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "%La")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterInsensitiveEndsWith", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveEndsWith(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "%la")
})
t.Run("PostgreSQL_FilterInsensitiveEndsWith", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveEndsWith(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" ILIKE $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "\\%la")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterExact", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterExact(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` LIKE ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "La")
})
t.Run("PostgreSQL_FilterExact", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterExact(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" LIKE $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "La")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterInsensitiveExact", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveExact(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "la")
})
t.Run("PostgreSQL_FilterInsensitiveExact", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveExact(s, p, "name", "La")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" ILIKE $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "la")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterRegex", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterRegex(s, p, "name", "^(An?|The) +")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` REGEXP BINARY ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "^(An?|The) +")
})
t.Run("PostgreSQL_FilterRegex", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterRegex(s, p, "name", "^(An?|The) +")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" ~ $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "^(An?|The) +")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterInsensitiveRegex", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveRegex(s, p, "name", "^(An?|The) +")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` REGEXP ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "^(an?|the) +")
})
t.Run("PostgreSQL_FilterInsensitiveRegex", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = filterInsensitiveRegex(s, p, "name", "^(An?|The) +")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE \"users\".\"name\" ~* $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "^(an?|the) +")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterDatePart", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("publishes"))
p := sql.P()
p = filterDatePart(s, p, "date", "pub_date")
p.EQ("", "2023-01-01")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `publishes` WHERE DATE(`publishes`.`pub_date`) = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-01-01")
})
t.Run("PostgreSQL_FilterDatePart", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("publishes"))
p := sql.P()
p = filterDatePart(s, p, "date", "pub_date")
p.EQ("", "2023-01-01")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"publishes\" WHERE EXTRACT('DATE' FROM \"publishes\".\"pub_date\") = $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "2023-01-01")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterJsonb", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("app_profile"))
p := sql.P()
p = filterJsonb(s, p, "daily_email", "preferences")
p.EQ("", "true")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `app_profile` WHERE JSON_EXTRACT(`app_profile`.`preferences`, '$.daily_email') = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "true")
})
t.Run("PostgreSQL_FilterJsonb", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("app_profile"))
p := sql.P()
p = filterJsonb(s, p, "daily_email", "preferences")
p.EQ("", "true")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"app_profile\" WHERE \"app_profile\".\"preferences\" ->> 'daily_email' = $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "true")
})
}
func TestFilterJsonbField(t *testing.T) {
t.Run("filterJsonbField", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("app_profile"))
str := filterJsonbField(s, "daily_email", "preferences")
fmt.Println(str)
require.Equal(t, str, "\"app_profile\".\"preferences\" ->> 'daily_email'")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterEqual", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("menus"))
p := sql.P()
p = makeFieldFilter(s, []string{"meta.title"}, "tom")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `menus` WHERE JSON_EXTRACT(`menus`.`meta`, '$.title') = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "'tom'")
})
t.Run("PostgreSQL_FilterEqual", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("menus"))
p := sql.P()
p = makeFieldFilter(s, []string{"meta.title"}, "tom")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"menus\" WHERE \"menus\".\"meta\" ->> 'title' = $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "'tom'")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterNot", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = makeFieldFilter(s, []string{"meta.title", "not"}, "tom")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE NOT JSON_EXTRACT(`users`.`meta`, '$.title') = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "'tom'")
})
t.Run("PostgreSQL_FilterNot", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = makeFieldFilter(s, []string{"meta.title", "not"}, "tom")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE NOT \"users\".\"meta\" ->> 'title' = $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "'tom'")
})
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
t.Run("MySQL_FilterNot_Date", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
p := sql.P()
p = makeFieldFilter(s, []string{"meta.title", "date", "not"}, "2023-01-01")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE NOT DATE(JSON_EXTRACT(`users`.`meta`, '$.title')) = ?", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "'2023-01-01'")
})
t.Run("PostgreSQL_FilterNot_Date", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
p := sql.P()
p = makeFieldFilter(s, []string{"meta.title", "date", "not"}, "2023-01-01")
s.Where(p)
query, args := s.Query()
require.Equal(t, "SELECT * FROM \"users\" WHERE NOT EXTRACT('DATE' FROM \"users\".\"meta\" ->> 'title') = $1", query)
require.NotEmpty(t, args)
require.Equal(t, args[0], "'2023-01-01'")
})
}

View File

@@ -21,23 +21,31 @@ func QueryCommandToOrderConditions(orderBys []string) (error, func(s *sql.Select
continue
}
s.OrderBy(sql.Desc(s.C(key)))
BuildOrderSelect(s, key, true)
} else {
// 升序
if len(v) == 0 {
continue
}
s.OrderBy(sql.Asc(s.C(v)))
BuildOrderSelect(s, v, false)
}
}
}
}
func BuildOrderSelect(s *sql.Selector, field string, desc bool) {
if desc {
s.OrderBy(sql.Desc(s.C(field)))
} else {
s.OrderBy(sql.Asc(s.C(field)))
}
}
func BuildOrderSelector(orderBys []string, defaultOrderField string) (error, func(s *sql.Selector)) {
if len(orderBys) == 0 {
return nil, func(s *sql.Selector) {
s.OrderBy(sql.Desc(s.C(defaultOrderField)))
BuildOrderSelect(s, defaultOrderField, true)
}
} else {
return QueryCommandToOrderConditions(orderBys)

View File

@@ -3,24 +3,27 @@ package entgo
import (
"entgo.io/ent/dialect/sql"
paging "github.com/tx7do/kratos-utils/pagination"
paging "github.com/tx7do/go-utils/pagination"
)
func BuildPaginationSelector(page, pageSize int32, noPaging bool) func(*sql.Selector) {
if noPaging {
return nil
}
if page == 0 {
page = DefaultPage
}
if pageSize == 0 {
pageSize = DefaultPageSize
}
} else {
return func(s *sql.Selector) {
s.Offset(paging.GetPageOffset(page, pageSize)).
Limit(int(pageSize))
BuildPaginationSelect(s, page, pageSize)
}
}
}
func BuildPaginationSelect(s *sql.Selector, page, pageSize int32) {
if page < 1 {
page = paging.DefaultPage
}
if pageSize < 1 {
pageSize = paging.DefaultPageSize
}
offset := paging.GetPageOffset(page, pageSize)
s.Offset(offset).Limit(int(pageSize))
}

View File

@@ -2,20 +2,16 @@ package entgo
import (
"entgo.io/ent/dialect/sql"
_ "github.com/go-kratos/kratos/v2/encoding/json"
)
const (
DefaultPage = 1
DefaultPageSize = 10
)
// BuildQuerySelector 构建分页查询选择器
// BuildQuerySelector 构建分页过滤查询器
func BuildQuerySelector(
dbDriverName string,
andFilterJsonString, orFilterJsonString string,
page, pageSize int32, noPaging bool,
orderBys []string, defaultOrderField string,
selectFields []string,
) (err error, whereSelectors []func(s *sql.Selector), querySelectors []func(s *sql.Selector)) {
err, whereSelectors = BuildFilterSelector(andFilterJsonString, orFilterJsonString)
if err != nil {
@@ -30,6 +26,9 @@ func BuildQuerySelector(
pageSelector := BuildPaginationSelector(page, pageSize, noPaging)
var fieldSelector func(s *sql.Selector)
err, fieldSelector = BuildFieldSelector(selectFields)
if len(whereSelectors) > 0 {
querySelectors = append(querySelectors, whereSelectors...)
}
@@ -40,6 +39,9 @@ func BuildQuerySelector(
if pageSelector != nil {
querySelectors = append(querySelectors, pageSelector)
}
if fieldSelector != nil {
querySelectors = append(querySelectors, fieldSelector)
}
return
}

View File

@@ -3,13 +3,15 @@ package entgo
import (
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"github.com/go-kratos/kratos/v2/encoding"
_ "github.com/go-kratos/kratos/v2/encoding/json"
"github.com/stretchr/testify/assert"
)
func TestKratosJsonCodec(t *testing.T) {
@@ -83,12 +85,70 @@ func TestJsonCodec(t *testing.T) {
func TestSplitQuery(t *testing.T) {
var keys []string
keys = strings.Split("id", "__")
keys = splitQueryKey("id")
assert.Equal(t, len(keys), 1)
assert.Equal(t, keys[0], "id")
keys = strings.Split("id__not", "__")
keys = splitQueryKey("id__not")
assert.Equal(t, len(keys), 2)
assert.Equal(t, keys[0], "id")
assert.Equal(t, keys[1], "not")
}
func TestBuildQuerySelectorDefault(t *testing.T) {
testcases := []struct {
name string
dialect string
and string
or string
noPaging bool
actualSql string
}{
{"MySQL_Pagination", dialect.MySQL, "", "", false, "SELECT * FROM `users` ORDER BY `users`.`created_at` DESC LIMIT 10 OFFSET 0"},
{"PostgreSQL_Pagination", dialect.Postgres, "", "", false, "SELECT * FROM \"users\" ORDER BY \"users\".\"created_at\" DESC LIMIT 10 OFFSET 0"},
{"MySQL_NoPagination", dialect.MySQL, "", "", true, "SELECT * FROM `users` ORDER BY `users`.`created_at` DESC"},
{"PostgreSQL_NoPagination", dialect.Postgres, "", "", true, "SELECT * FROM \"users\" ORDER BY \"users\".\"created_at\" DESC"},
{"MySQL_JsonbQuery", dialect.MySQL, "{\"preferences__daily_email\" : \"true\"}", "", true, "SELECT * FROM `users` WHERE JSON_EXTRACT(`users`.`preferences`, '$.daily_email') = ? ORDER BY `users`.`created_at` DESC"},
{"PostgreSQL_JsonbQuery", dialect.Postgres, "{\"preferences__daily_email\" : \"true\"}", "", true, "SELECT * FROM \"users\" WHERE \"users\".\"preferences\" ->> 'daily_email' = $1 ORDER BY \"users\".\"created_at\" DESC"},
{"MySQL_DatePartQuery", dialect.MySQL, "{\"created_at__date\" : \"2023-01-01\"}", "", true, "SELECT * FROM `users` WHERE DATE(`users`.`created_at`) = ? ORDER BY `users`.`created_at` DESC"},
{"PostgreSQL_DatePartQuery", dialect.Postgres, "{\"created_at__date\" : \"2023-01-01\"}", "", true, "SELECT * FROM \"users\" WHERE EXTRACT('DATE' FROM \"users\".\"created_at\") = $1 ORDER BY \"users\".\"created_at\" DESC"},
{"MySQL_JsonbCombineQuery", dialect.MySQL, "{\"preferences__pub_date__not\" : \"true\"}", "", true, "SELECT * FROM `users` WHERE NOT JSON_EXTRACT(`users`.`preferences`, '$.pub_date') = ? ORDER BY `users`.`created_at` DESC"},
{"PostgreSQL_JsonbCombineQuery", dialect.Postgres, "{\"preferences__pub_date__not\" : \"true\"}", "", true, "SELECT * FROM \"users\" WHERE NOT \"users\".\"preferences\" ->> 'pub_date' = $1 ORDER BY \"users\".\"created_at\" DESC"},
{"MySQL_DatePartCombineQuery", dialect.MySQL, "{\"pub_date__date__not\" : \"true\"}", "", true, "SELECT * FROM `users` WHERE NOT DATE(`users`.`pub_date`) = ? ORDER BY `users`.`created_at` DESC"},
{"PostgreSQL_DatePartCombineQuery", dialect.Postgres, "{\"pub_date__date__not\" : \"true\"}", "", true, "SELECT * FROM \"users\" WHERE NOT EXTRACT('DATE' FROM \"users\".\"pub_date\") = $1 ORDER BY \"users\".\"created_at\" DESC"},
{"MySQL_DatePartRangeQuery", dialect.MySQL, "{\"pub_date__date__range\" : \"[\\\"2023-10-25\\\", \\\"2024-10-25\\\"]\"}", "", true, "SELECT * FROM `users` WHERE DATE(`users`.`pub_date`) >= ? AND DATE(`users`.`pub_date`) <= ? ORDER BY `users`.`created_at` DESC"},
{"PostgreSQL_DatePartRangeQuery", dialect.Postgres, "{\"pub_date__date__range\" : \"[\\\"2023-10-25\\\", \\\"2024-10-25\\\"]\"}", "", true, "SELECT * FROM \"users\" WHERE EXTRACT('DATE' FROM \"users\".\"pub_date\") >= $1 AND EXTRACT('DATE' FROM \"users\".\"pub_date\") <= $2 ORDER BY \"users\".\"created_at\" DESC"},
{"MySQL_JsonQuery", dialect.MySQL, "{\"meta.title\" : \"preferences__daily_email\"}", "", true, "SELECT * FROM `users` WHERE JSON_EXTRACT(`users`.`meta`, '$.title') = ? ORDER BY `users`.`created_at` DESC"},
{"PostgreSQL_JsonQuery", dialect.Postgres, "{\"meta.title\" : \"preferences__daily_email\"}", "", true, "SELECT * FROM \"users\" WHERE \"users\".\"meta\" ->> 'title' = $1 ORDER BY \"users\".\"created_at\" DESC"},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
checker := assert.New(t)
s := sql.Dialect(tc.dialect).Select("*").From(sql.Table("users"))
err, _, querySelectors := BuildQuerySelector(tc.and, tc.or,
1, 10, tc.noPaging,
[]string{}, "created_at",
[]string{},
)
checker.Nil(err)
//checker.NotNil(whereSelectors)
checker.NotNil(querySelectors)
for _, fnc := range querySelectors {
fnc(s)
}
query, _ := s.Query()
checker.Equal(tc.actualSql, query)
//checker.Empty(t, args)
})
}
}

29
entgo/query/select.go Normal file
View File

@@ -0,0 +1,29 @@
package entgo
import (
"entgo.io/ent/dialect/sql"
"github.com/tx7do/go-utils/stringcase"
)
func BuildFieldSelect(s *sql.Selector, fields []string) {
if len(fields) > 0 {
for i, field := range fields {
switch {
case field == "id_" || field == "_id":
field = "id"
}
fields[i] = stringcase.ToSnakeCase(field)
}
s.Select(fields...)
}
}
func BuildFieldSelector(fields []string) (error, func(s *sql.Selector)) {
if len(fields) > 0 {
return nil, func(s *sql.Selector) {
BuildFieldSelect(s, fields)
}
} else {
return nil, nil
}
}

View File

@@ -0,0 +1,48 @@
package entgo
import (
"testing"
"github.com/stretchr/testify/require"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
)
func TestBuildFieldSelect(t *testing.T) {
t.Run("MySQL_2Fields", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
BuildFieldSelect(s, []string{"id", "username"})
query, args := s.Query()
require.Equal(t, "SELECT `id`, `username` FROM `users`", query)
require.Empty(t, args)
})
t.Run("PostgreSQL_2Fields", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
BuildFieldSelect(s, []string{"id", "username"})
query, args := s.Query()
require.Equal(t, `SELECT "id", "username" FROM "users"`, query)
require.Empty(t, args)
})
t.Run("MySQL_AllFields", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Select("*").From(sql.Table("users"))
BuildFieldSelect(s, []string{})
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users`", query)
require.Empty(t, args)
})
t.Run("PostgreSQL_AllFields", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Select("*").From(sql.Table("users"))
BuildFieldSelect(s, []string{})
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users"`, query)
require.Empty(t, args)
})
}

100
entgo/update/update.go Normal file
View File

@@ -0,0 +1,100 @@
package entgo
import (
"fmt"
"strings"
"entgo.io/ent/dialect/sql"
"github.com/tx7do/go-utils/fieldmaskutil"
"github.com/tx7do/go-utils/stringcase"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)
func BuildSetNullUpdate(u *sql.UpdateBuilder, fields []string) {
if len(fields) > 0 {
for _, field := range fields {
field = stringcase.ToSnakeCase(field)
u.SetNull(field)
}
}
}
// BuildSetNullUpdater 构建一个UpdateBuilder用于清空字段的值
func BuildSetNullUpdater(fields []string) func(u *sql.UpdateBuilder) {
if len(fields) == 0 {
return nil
}
return func(u *sql.UpdateBuilder) {
BuildSetNullUpdate(u, fields)
}
}
// ExtractJsonFieldKeyValues 提取json字段的键值对
func ExtractJsonFieldKeyValues(msg proto.Message, paths []string, needToSnakeCase bool) []string {
var keyValues []string
rft := msg.ProtoReflect()
for _, path := range paths {
fd := rft.Descriptor().Fields().ByName(protoreflect.Name(path))
if fd == nil {
continue
}
if !rft.Has(fd) {
continue
}
var k string
if needToSnakeCase {
k = stringcase.ToSnakeCase(path)
} else {
k = path
}
keyValues = append(keyValues, fmt.Sprintf("'%s'", k))
v := rft.Get(fd)
switch v.Interface().(type) {
case int32, int64, uint32, uint64, float32, float64, bool:
keyValues = append(keyValues, fmt.Sprintf("%d", v.Interface()))
case string:
keyValues = append(keyValues, fmt.Sprintf("'%s'", v.Interface()))
}
}
return keyValues
}
// SetJsonNullFieldUpdateBuilder 设置json字段的空值
func SetJsonNullFieldUpdateBuilder(fieldName string, msg proto.Message, paths []string) func(u *sql.UpdateBuilder) {
nilPaths := fieldmaskutil.NilValuePaths(msg, paths)
if len(nilPaths) == 0 {
return nil
}
return func(u *sql.UpdateBuilder) {
u.Set(fieldName,
sql.Expr(
fmt.Sprintf("\"%s\" - '{%s}'::text[]", fieldName, strings.Join(nilPaths, ",")),
),
)
}
}
// SetJsonFieldValueUpdateBuilder 设置json字段的值
func SetJsonFieldValueUpdateBuilder(fieldName string, msg proto.Message, paths []string, needToSnakeCase bool) func(u *sql.UpdateBuilder) {
keyValues := ExtractJsonFieldKeyValues(msg, paths, needToSnakeCase)
if len(keyValues) == 0 {
return nil
}
return func(u *sql.UpdateBuilder) {
u.Set(fieldName,
sql.Expr(
fmt.Sprintf("\"%s\" || jsonb_build_object(%s)", fieldName, strings.Join(keyValues, ",")),
),
)
}
}

View File

@@ -0,0 +1,30 @@
package entgo
import (
"testing"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"github.com/stretchr/testify/require"
)
func TestBuildSetNullUpdate(t *testing.T) {
t.Run("MySQL_Set2", func(t *testing.T) {
s := sql.Dialect(dialect.MySQL).Update("users")
BuildSetNullUpdate(s, []string{"id", "username"})
query, args := s.Query()
require.Equal(t, "UPDATE `users` SET `id` = NULL, `username` = NULL", query)
require.Empty(t, args)
})
t.Run("PostgreSQL_Set2", func(t *testing.T) {
s := sql.Dialect(dialect.Postgres).Update("users")
BuildSetNullUpdate(s, []string{"id", "username"})
query, args := s.Query()
require.Equal(t, `UPDATE "users" SET "id" = NULL, "username" = NULL`, query)
require.Empty(t, args)
})
}

View File

@@ -0,0 +1,234 @@
package fieldmaskutil
import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)
// Filter keeps the msg fields that are listed in the paths and clears all the rest.
//
// This is a handy wrapper for NestedMask.Filter method.
// If the same paths are used to process multiple proto messages use NestedMask.Filter method directly.
func Filter(msg proto.Message, paths []string) {
NestedMaskFromPaths(paths).Filter(msg)
}
// Prune clears all the fields listed in paths from the given msg.
//
// This is a handy wrapper for NestedMask.Prune method.
// If the same paths are used to process multiple proto messages use NestedMask.Filter method directly.
func Prune(msg proto.Message, paths []string) {
NestedMaskFromPaths(paths).Prune(msg)
}
// Overwrite overwrites all the fields listed in paths in the dest msg using values from src msg.
//
// This is a handy wrapper for NestedMask.Overwrite method.
// If the same paths are used to process multiple proto messages use NestedMask.Overwrite method directly.
func Overwrite(src, dest proto.Message, paths []string) {
NestedMaskFromPaths(paths).Overwrite(src, dest)
}
// NestedMask represents a field mask as a recursive map.
type NestedMask map[string]NestedMask
// NestedMaskFromPaths creates an instance of NestedMask for the given paths.
func NestedMaskFromPaths(paths []string) NestedMask {
mask := make(NestedMask)
for _, path := range paths {
curr := mask
var letters []rune
for _, letter := range path {
if letter == '.' {
if len(letters) == 0 {
continue
}
key := string(letters)
c, ok := curr[key]
if !ok {
c = make(NestedMask)
curr[key] = c
}
curr = c
letters = nil
continue
}
letters = append(letters, letter)
}
if len(letters) != 0 {
key := string(letters)
if _, ok := curr[key]; !ok {
curr[key] = make(NestedMask)
}
}
}
return mask
}
// Filter keeps the msg fields that are listed in the paths and clears all the rest.
//
// If the mask is empty then all the fields are kept.
// Paths are assumed to be valid and normalized otherwise the function may panic.
// See google.golang.org/protobuf/types/known/fieldmaskpb for details.
func (mask NestedMask) Filter(msg proto.Message) {
if len(mask) == 0 {
return
}
rft := msg.ProtoReflect()
rft.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
m, ok := mask[string(fd.Name())]
if !ok {
rft.Clear(fd)
return true
}
if len(m) == 0 {
return true
}
if fd.IsMap() {
xmap := rft.Get(fd).Map()
xmap.Range(func(mk protoreflect.MapKey, mv protoreflect.Value) bool {
if mi, ok := m[mk.String()]; ok {
if i, ok := mv.Interface().(protoreflect.Message); ok && len(mi) > 0 {
mi.Filter(i.Interface())
}
} else {
xmap.Clear(mk)
}
return true
})
} else if fd.IsList() {
list := rft.Get(fd).List()
for i := 0; i < list.Len(); i++ {
m.Filter(list.Get(i).Message().Interface())
}
} else if fd.Kind() == protoreflect.MessageKind {
m.Filter(rft.Get(fd).Message().Interface())
}
return true
})
}
// Prune clears all the fields listed in paths from the given msg.
//
// All other fields are kept untouched. If the mask is empty no fields are cleared.
// This operation is the opposite of NestedMask.Filter.
// Paths are assumed to be valid and normalized otherwise the function may panic.
// See google.golang.org/protobuf/types/known/fieldmaskpb for details.
func (mask NestedMask) Prune(msg proto.Message) {
if len(mask) == 0 {
return
}
rft := msg.ProtoReflect()
rft.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
m, ok := mask[string(fd.Name())]
if ok {
if len(m) == 0 {
rft.Clear(fd)
return true
}
if fd.IsMap() {
xmap := rft.Get(fd).Map()
xmap.Range(func(mk protoreflect.MapKey, mv protoreflect.Value) bool {
if mi, ok := m[mk.String()]; ok {
if i, ok := mv.Interface().(protoreflect.Message); ok && len(mi) > 0 {
mi.Prune(i.Interface())
} else {
xmap.Clear(mk)
}
}
return true
})
} else if fd.IsList() {
list := rft.Get(fd).List()
for i := 0; i < list.Len(); i++ {
m.Prune(list.Get(i).Message().Interface())
}
} else if fd.Kind() == protoreflect.MessageKind {
m.Prune(rft.Get(fd).Message().Interface())
}
}
return true
})
}
// Overwrite overwrites all the fields listed in paths in the dest msg using values from src msg.
//
// All other fields are kept untouched. If the mask is empty, no fields are overwritten.
// Supports scalars, messages, repeated fields, and maps.
// If the parent of the field is nil message, the parent is initiated before overwriting the field
// If the field in src is empty value, the field in dest is cleared.
// Paths are assumed to be valid and normalized otherwise the function may panic.
func (mask NestedMask) Overwrite(src, dest proto.Message) {
mask.overwrite(src.ProtoReflect(), dest.ProtoReflect())
}
func (mask NestedMask) overwrite(src, dest protoreflect.Message) {
for k, v := range mask {
srcFD := src.Descriptor().Fields().ByName(protoreflect.Name(k))
destFD := dest.Descriptor().Fields().ByName(protoreflect.Name(k))
if srcFD == nil || destFD == nil {
continue
}
// Leaf mask -> copy value from src to dest
if len(v) == 0 {
if srcFD.Kind() == destFD.Kind() { // TODO: Full type equality check
val := src.Get(srcFD)
if isValid(srcFD, val) {
dest.Set(destFD, val)
} else {
dest.Clear(destFD)
}
}
} else if srcFD.Kind() == protoreflect.MessageKind {
// If dest field is nil
if !dest.Get(destFD).Message().IsValid() {
dest.Set(destFD, protoreflect.ValueOf(dest.Get(destFD).Message().New()))
}
v.overwrite(src.Get(srcFD).Message(), dest.Get(destFD).Message())
}
}
}
func isValid(fd protoreflect.FieldDescriptor, val protoreflect.Value) bool {
if fd.IsMap() {
return val.Map().IsValid()
} else if fd.IsList() {
return val.List().IsValid()
} else if fd.Message() != nil {
return val.Message().IsValid()
}
return true
}
func NilValuePaths(msg proto.Message, paths []string) []string {
if len(paths) == 0 {
return nil
}
var out []string
rft := msg.ProtoReflect()
for _, v := range paths {
fd := rft.Descriptor().Fields().ByName(protoreflect.Name(v))
if fd == nil {
continue
}
if !rft.Has(fd) {
out = append(out, v)
}
}
return out
}

View File

@@ -0,0 +1,59 @@
package fieldmaskutil
import (
"reflect"
"testing"
)
func Test_NestedMaskFromPaths(t *testing.T) {
type args struct {
paths []string
}
tests := []struct {
name string
args args
want NestedMask
}{
{
name: "no nested fields",
args: args{paths: []string{"a", "b", "c"}},
want: NestedMask{"a": NestedMask{}, "b": NestedMask{}, "c": NestedMask{}},
},
{
name: "with nested fields",
args: args{paths: []string{"aaa.bb.c", "dd.e", "f"}},
want: NestedMask{
"aaa": NestedMask{"bb": NestedMask{"c": NestedMask{}}},
"dd": NestedMask{"e": NestedMask{}},
"f": NestedMask{}},
},
{
name: "single field",
args: args{paths: []string{"a"}},
want: NestedMask{"a": NestedMask{}},
},
{
name: "empty fields",
args: args{paths: []string{}},
want: NestedMask{},
},
{
name: "invalid input",
args: args{paths: []string{".", "..", "..."}},
want: NestedMask{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NestedMaskFromPaths(tt.args.paths); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NestedMaskFromPaths() = %v, want %v", got, tt.want)
}
})
}
}
func BenchmarkNestedMaskFromPaths(b *testing.B) {
for i := 0; i < b.N; i++ {
NestedMaskFromPaths([]string{"aaa.bbb.c.d.e.f", "aa.b.cc.ddddddd", "e", "f", "g.h.i.j.k"})
}
}

View File

@@ -7,8 +7,8 @@ import (
"github.com/go-kratos/kratos/v2/log"
"github.com/oschwald/geoip2-golang"
"github.com/tx7do/kratos-utils/geoip"
"github.com/tx7do/kratos-utils/geoip/geolite/assets"
"github.com/tx7do/go-utils/geoip"
"github.com/tx7do/go-utils/geoip/geolite/assets"
)
const defaultOutputLanguage = "zh-CN"

View File

@@ -1,19 +1,23 @@
module github.com/tx7do/kratos-utils/geoip
module github.com/tx7do/go-utils/geoip
go 1.20
go 1.23.0
toolchain go1.24.1
require (
github.com/go-kratos/kratos/v2 v2.7.1
github.com/oschwald/geoip2-golang v1.9.0
github.com/stretchr/testify v1.8.4
golang.org/x/text v0.13.0
github.com/go-kratos/kratos/v2 v2.8.4
github.com/oschwald/geoip2-golang v1.11.0
github.com/stretchr/testify v1.10.0
golang.org/x/text v0.25.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/sys v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

View File

@@ -1,25 +1,30 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/go-kratos/kratos/v2 v2.7.1 h1:PNMUaWxS5ZGDp1EyID5ZosJb1OA/YiHnBxB0yUmocnc=
github.com/go-kratos/kratos/v2 v2.7.1/go.mod h1:CPn82O93OLHjtnbuyOKhAG5TkSvw+mFnL32c4lZFDwU=
github.com/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs=
github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -3,12 +3,12 @@ package qqwry
import (
"encoding/binary"
"errors"
"github.com/tx7do/kratos-utils/geoip"
"github.com/tx7do/go-utils/geoip"
"net"
"strings"
"sync"
"github.com/tx7do/kratos-utils/geoip/qqwry/assets"
"github.com/tx7do/go-utils/geoip/qqwry/assets"
)
type Client struct {

36
gitautotag.sh Normal file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
CURTAG=`git describe --abbrev=0 --tags`;
CURTAG="${CURTAG/v/}"
IFS='.' read -a vers <<< "$CURTAG"
MAJ=${vers[0]}
MIN=${vers[1]}
BUG=${vers[2]}
echo "Current Tag: v$MAJ.$MIN.$BUG"
for cmd in "$@"
do
case $cmd in
"--major")
# $((MAJ+1))
((MAJ+=1))
MIN=0
BUG=0
echo "Incrementing Major Version#"
;;
"--minor")
((MIN+=1))
BUG=0
echo "Incrementing Minor Version#"
;;
"--bug")
((BUG+=1))
echo "Incrementing Bug Version#"
;;
esac
done
NEWTAG="v$MAJ.$MIN.$BUG"
echo "Adding Tag: $NEWTAG";
git tag -a $NEWTAG -m $NEWTAG

15
go.mod
View File

@@ -1,11 +1,16 @@
module github.com/tx7do/kratos-utils
module github.com/tx7do/go-utils
go 1.20
go 1.23.0
toolchain go1.24.1
require (
github.com/sony/sonyflake v1.2.0
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.14.0
github.com/gobwas/glob v0.2.3
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.38.0
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
google.golang.org/protobuf v1.36.6
)
require (

20
go.sum
View File

@@ -1,6 +1,12 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -14,12 +20,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ=
github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

55
gorm/go.mod Normal file
View File

@@ -0,0 +1,55 @@
module github.com/tx7do/go-utils/gorm
go 1.23.0
toolchain go1.24.3
require (
gorm.io/driver/clickhouse v0.6.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.7
gorm.io/driver/sqlserver v1.5.4
gorm.io/gorm v1.26.1
gorm.io/plugin/opentelemetry v0.1.14
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.66.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.34.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/puddle/v2 v2.2.2 // 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/mattn/go-sqlite3 v1.14.28 // indirect
github.com/microsoft/go-mssqldb v1.8.1 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

290
gorm/go.sum Normal file
View File

@@ -0,0 +1,290 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go v56.3.0+incompatible h1:DmhwMrUIvpeoTDiWRDtNHqelNUd3Og8JCkrLHQK795c=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/ClickHouse/ch-go v0.66.0 h1:hLslxxAVb2PHpbHr4n0d6aP8CEIpUYGMVT1Yj/Q5Img=
github.com/ClickHouse/ch-go v0.66.0/go.mod h1:noiHWyLMJAZ5wYuq3R/K0TcRhrNA8h7o1AqHX0klEhM=
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/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/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/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.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/microsoft/go-mssqldb v1.8.1 h1:/LPVjSb992vTa8CMVvliTMT//UAKj/jpe1xb/jJBjIk=
github.com/microsoft/go-mssqldb v1.8.1/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
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/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.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/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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-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-20200114155413-6afb5195e5aa/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/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/clickhouse v0.6.1 h1:t7JMB6sLBXxN8hEO6RdzCbJCwq/jAEVZdwXlmQs1Sd4=
gorm.io/driver/clickhouse v0.6.1/go.mod h1:riMYpJcGZ3sJ/OAZZ1rEP1j/Y0H6cByOAnwz7fo2AyM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
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=
gorm.io/plugin/opentelemetry v0.1.14 h1:xivP39t/0JgcceDl+BLwVAJHihjFEUj0ZocMSBwZ7ZY=
gorm.io/plugin/opentelemetry v0.1.14/go.mod h1:ZAp4v5vU1CCcK9Oo8/va5rl6NStrzpSU+a70evd+W/g=

90
gorm/gorm_client.go Normal file
View File

@@ -0,0 +1,90 @@
package gorm
import (
"fmt"
"gorm.io/gorm"
"gorm.io/plugin/opentelemetry/tracing"
"gorm.io/driver/clickhouse"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/driver/sqlserver"
)
type Client struct {
*gorm.DB
err error
}
func NewClient(driverName, dsn string, enableMigrate, enableTrace, enableMetrics bool, gormCfg *gorm.Config) *Client {
c := &Client{}
if gormCfg == nil {
gormCfg = &gorm.Config{}
}
c.err = c.createGormClient(driverName, dsn, enableMigrate, enableTrace, enableMetrics, gormCfg)
return c
}
func (c *Client) Error() error {
return c.err
}
// createGormClient 创建GORM的客户端
func (c *Client) createGormClient(driverName, dsn string, enableMigrate, enableTrace, enableMetrics bool, gormCfg *gorm.Config) error {
var driver gorm.Dialector
switch driverName {
default:
fallthrough
case "mysql":
driver = mysql.Open(dsn)
break
case "postgres":
driver = postgres.Open(dsn)
break
case "clickhouse":
driver = clickhouse.Open(dsn)
break
case "sqlite":
driver = sqlite.Open(dsn)
break
case "sqlserver":
driver = sqlserver.Open(dsn)
break
}
client, err := gorm.Open(driver, gormCfg)
if err != nil {
return fmt.Errorf("failed opening connection to db: %v", err)
}
if enableTrace {
var opts []tracing.Option
if enableMetrics {
opts = append(opts, tracing.WithoutMetrics())
}
if err = client.Use(tracing.NewPlugin(opts...)); err != nil {
return fmt.Errorf("failed opening connection to db: %v", err)
}
}
// 运行数据库迁移工具
if enableMigrate {
if err = client.AutoMigrate(
getMigrateModels()...,
); err != nil {
return fmt.Errorf("failed creating schema resources: %v", err)
}
}
c.DB = client
return nil
}

13
gorm/migrates.go Normal file
View File

@@ -0,0 +1,13 @@
package gorm
var migrateModels []interface{}
// RegisterMigrateModel 注册用于数据库迁移的数据库模型
func RegisterMigrateModel(model interface{}) {
migrateModels = append(migrateModels, &model)
}
// getMigrateModels 获取用于数据库迁移的数据库模型
func getMigrateModels() []interface{} {
return migrateModels
}

28
id/README.md Normal file
View File

@@ -0,0 +1,28 @@
# ID生成器
## 订单ID
- 电商平台202506041234567890时间戳 + 随机数19-20 位)。
- 支付系统PAY20250604123456789业务前缀 + 时间戳 + 序号)。
- 微信支付1589123456789012345类似 Snowflake 的纯数字 ID
- 美团订单202506041234567890123时间戳 + 商户 ID + 随机数)。
## UUID
| 特性 | GUID/UUID | KSUID | ShortUUID | XID | Snowflake |
|-------|--------------|------------|-----------|----------|----------------|
| 长度 | 36/32字符不含- | 27字符 | 22字符 | 20字符 | 19数字位数 |
| 有序性 | 无序UUIDv4 | 严格时序 | 无序 | 趋势有序 | 严格时序 |
| 时间精度 | 无UUIDv4 | 毫秒级 | 无 | 秒级 | 毫秒级 |
| 分布式安全 | 高(随机数) | 高 | 高 | 高 | 高需配置WorkerID |
| 性能 | 中等 | 中等 | 较低(编码开销) | 极高 | 极高 |
| 时钟依赖 | 无 | 有(需处理时钟回拨) | 无 | 有(但影响较小) | 强依赖(需严格同步) |
| 适用场景 | 跨系统兼容 | 时序索引 | 短ID、URL | 高并发、短ID | 分布式时序ID |
## 选择建议
- **GUID/UUID**: 适用于需要跨系统兼容的场景,特别是当不需要有序性时。
- **KSUID**: 适合需要严格时序的应用,如事件日志、时间序列数据。
- **ShortUUID**: 当需要短ID且不关心有序性时的理想选择适用于URL、短链接等。
- **XID**: 高并发场景下的短ID选择适合需要一定有序性的应用。
- **Snowflake**: 适合分布式系统,特别是需要严格时序和高性能的场景,如大规模分布式应用。

27
id/go.mod Normal file
View File

@@ -0,0 +1,27 @@
module github.com/tx7do/go-utils/id
go 1.23.0
toolchain go1.23.2
require (
github.com/bwmarrin/snowflake v0.3.0
github.com/google/uuid v1.6.0
github.com/lithammer/shortuuid/v4 v4.2.0
github.com/rs/xid v1.6.0
github.com/segmentio/ksuid v1.0.4
github.com/stretchr/testify v1.10.0
github.com/tx7do/go-utils v1.1.27
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/sony/sonyflake v1.2.1 // indirect
go.mongodb.org/mongo-driver v1.17.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

32
id/go.sum Normal file
View File

@@ -0,0 +1,32 @@
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/sony/sonyflake v1.2.1 h1:Jzo4abS84qVNbYamXZdrZF1/6TzNJjEogRfXv7TsG48=
github.com/sony/sonyflake v1.2.1/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

84
id/order_id.go Normal file
View File

@@ -0,0 +1,84 @@
package id
import (
"fmt"
"math/rand"
"strings"
"sync/atomic"
"time"
"github.com/tx7do/go-utils/trans"
)
type idCounter uint32
func (c *idCounter) Increase() uint32 {
cur := *c
atomic.AddUint32((*uint32)(c), 1)
atomic.CompareAndSwapUint32((*uint32)(c), 1000, 0)
return uint32(cur)
}
var orderIdIndex idCounter
// GenerateOrderIdWithRandom 生成20位订单号前缀 + 时间戳 + 随机数
func GenerateOrderIdWithRandom(prefix string, tm *time.Time) string {
// 前缀 + 时间戳14位 + 随机数4位
if tm == nil {
tm = trans.Time(time.Now())
}
timestamp := tm.Format("20060102150405")
randNum := rand.Intn(10000) // 生成0-9999之间的随机数
return fmt.Sprintf("%s%s%d", prefix, timestamp, randNum)
}
// GenerateOrderIdWithIncreaseIndex 生成20位订单号前缀+时间+自增长索引
func GenerateOrderIdWithIncreaseIndex(prefix string, tm *time.Time) string {
if tm == nil {
tm = trans.Time(time.Now())
}
timestamp := tm.Format("20060102150405")
index := orderIdIndex.Increase()
return fmt.Sprintf("%s%s%d", prefix, timestamp, index)
}
// GenerateOrderIdWithTenantId 带商户ID的订单ID生成器202506041234567890123
func GenerateOrderIdWithTenantId(tenantID string) string {
// 时间戳14位 + 商户ID固定 5 位) + 随机数4位
// 时间戳部分(精确到毫秒)
now := time.Now()
timestamp := now.Format("20060102150405")
// 商户ID部分截取或补零到5位
tenantPart := tenantID
if len(tenantPart) > 5 {
tenantPart = tenantPart[:5]
} else {
tenantPart = fmt.Sprintf("%-5s", tenantPart)
tenantPart = strings.ReplaceAll(tenantPart, " ", "0")
}
// 随机数部分4位
n := rand.Int31n(10000)
randomPart := fmt.Sprintf("%04d", n)
return timestamp + tenantPart + randomPart
}
func GenerateOrderIdWithPrefixSonyflake(prefix string) string {
id, _ := NewSonyflakeID()
return fmt.Sprintf("%s%d", prefix, id)
}
func GenerateOrderIdWithPrefixSnowflake(workerId int64, prefix string) string {
id, _ := NewSnowflakeID(workerId)
return fmt.Sprintf("%s%d", prefix, id)
}

158
id/order_id_test.go Normal file
View File

@@ -0,0 +1,158 @@
package id
import (
"fmt"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestGenerateOrderIdWithRandom(t *testing.T) {
prefix := "PT"
// 测试生成的订单号是否包含前缀
orderID := GenerateOrderIdWithRandom(prefix, nil)
assert.Contains(t, orderID, prefix, "订单号应包含前缀")
t.Logf("GenerateOrderIdWithRandom: %s", orderID)
// 测试生成的订单号长度是否正确
assert.Equal(t, len(prefix)+14+4, len(orderID), "订单号长度应为前缀+时间戳+随机数")
}
func TestGenerateOrderIdWithIndex(t *testing.T) {
prefix := "PT"
tm := time.Now()
fmt.Println(GenerateOrderIdWithIncreaseIndex(prefix, &(tm)))
ids := make(map[string]bool)
count := 100
for i := 0; i < count; i++ {
ids[GenerateOrderIdWithIncreaseIndex(prefix, &(tm))] = true
}
assert.Equal(t, count, len(ids))
}
func TestGenerateOrderIdWithIndexThread(t *testing.T) {
tm := time.Now()
var wg sync.WaitGroup
var ids sync.Map
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for i := 0; i < 100; i++ {
id := GenerateOrderIdWithIncreaseIndex("PT", &(tm))
ids.Store(id, true)
}
wg.Done()
}()
}
wg.Wait()
aLen := 0
ids.Range(func(k, v interface{}) bool {
aLen++
return true
})
assert.Equal(t, 1000, aLen)
}
func TestGenerateOrderIdWithTenantId(t *testing.T) {
tenantID := "M9876"
orderID := GenerateOrderIdWithTenantId(tenantID)
t.Logf(orderID)
// 验证订单号长度是否正确
assert.Equal(t, 14+5+4, len(orderID))
// 验证时间戳部分是否正确
timestamp := time.Now().Format("20060102150405")
assert.Contains(t, orderID, timestamp)
t.Logf("timestamp %d", len(timestamp))
// 验证商户ID部分是否正确
assert.Contains(t, orderID, tenantID)
// 验证随机数部分是否为4位数字
randomPart := orderID[len(orderID)-4:]
assert.Regexp(t, `^\d{4}$`, randomPart)
}
func TestGenerateOrderIdWithTenantIdCollision(t *testing.T) {
tenantID := "M9876"
count := 1000 // 生成订单号的数量
ids := make(map[string]bool)
for i := 0; i < count; i++ {
orderID := GenerateOrderIdWithTenantId(tenantID)
if ids[orderID] {
t.Errorf("碰撞的订单号: %s", orderID)
}
ids[orderID] = true
}
t.Logf("生成了 %d 个订单号,没有发生碰撞", count)
}
func TestGenerateOrderIdWithPrefixSonyflake(t *testing.T) {
prefix := "ORD"
orderID := GenerateOrderIdWithPrefixSonyflake(prefix)
t.Logf("order id with prefix sonyflake: %s [%d]", orderID, len(orderID))
// 验证订单号是否包含前缀
assert.Contains(t, orderID, prefix, "订单号应包含前缀")
// 验证订单号是否为有效的数字字符串
assert.Regexp(t, `^ORD\d+$`, orderID, "订单号格式应为前缀加数字")
}
func TestGenerateOrderIdWithPrefixSonyflakeCollision(t *testing.T) {
prefix := "ORD"
count := 100000 // 生成订单号的数量
ids := make(map[string]bool)
for i := 0; i < count; i++ {
orderID := GenerateOrderIdWithPrefixSonyflake(prefix)
if ids[orderID] {
t.Errorf("碰撞的订单号: %s", orderID)
}
ids[orderID] = true
}
t.Logf("生成了 %d 个订单号,没有发生碰撞", count)
}
func TestGenerateOrderIdWithPrefixSnowflake(t *testing.T) {
workerId := int64(1) // 假设使用的 workerId
prefix := "ORD"
orderID := GenerateOrderIdWithPrefixSnowflake(workerId, prefix)
t.Logf("order id with prefix snowflake: %s [%d]", orderID, len(orderID))
// 验证订单号是否包含前缀
assert.Contains(t, orderID, prefix, "订单号应包含前缀")
// 验证订单号是否为有效的数字字符串
assert.Regexp(t, `^ORD\d+$`, orderID, "订单号格式应为前缀加数字")
}
func TestGenerateOrderIdWithPrefixSnowflakeCollision(t *testing.T) {
workerId := int64(1) // 假设使用的 workerId
prefix := "ORD"
count := 1000000 // 生成订单号的数量
ids := make(map[string]bool)
for i := 0; i < count; i++ {
orderID := GenerateOrderIdWithPrefixSnowflake(workerId, prefix)
if ids[orderID] {
t.Errorf("碰撞的订单号: %s", orderID)
}
ids[orderID] = true
}
t.Logf("生成了 %d 个订单号,没有发生碰撞", count)
}

61
id/snowflake.go Normal file
View File

@@ -0,0 +1,61 @@
package id
import (
"errors"
"sync"
"github.com/bwmarrin/snowflake"
)
var snowflakeNodeMap = sync.Map{}
type SnowflakeNode struct {
workerId int64
node *snowflake.Node
sync.Mutex
}
func NewSnowflakeNode(workerId int64) (*SnowflakeNode, error) {
node, err := snowflake.NewNode(workerId)
return &SnowflakeNode{
workerId: workerId,
node: node,
Mutex: sync.Mutex{},
}, err
}
func (sfNode *SnowflakeNode) Generate() int64 {
sfNode.Lock()
defer sfNode.Unlock()
return sfNode.node.Generate().Int64()
}
func (sfNode *SnowflakeNode) GenerateString() string {
sfNode.Lock()
defer sfNode.Unlock()
return sfNode.node.Generate().String()
}
func NewSnowflakeID(workerId int64) (int64, error) {
// 64 位 ID = 41 位时间戳 + 10 位工作节点 ID + 12 位序列号
var node *SnowflakeNode
var err error
find, ok := snowflakeNodeMap.Load(workerId)
if ok {
node = find.(*SnowflakeNode)
} else {
node, err = NewSnowflakeNode(workerId)
if err != nil {
//log.Println(err)
return 0, err
}
snowflakeNodeMap.Store(workerId, node)
}
if node == nil {
//log.Println("snowflake node is nil")
return 0, errors.New("snowflake node is nil")
}
return node.Generate(), nil
}

25
id/sonyflake.go Normal file
View File

@@ -0,0 +1,25 @@
package id
import (
"sync"
"github.com/sony/sonyflake"
)
var (
sf *sonyflake.Sonyflake
sfMu sync.Mutex
)
func NewSonyflakeID() (uint64, error) {
// 64 位 ID = 39 位时间戳 + 8 位机器 ID + 16 位序列号
sfMu.Lock()
defer sfMu.Unlock()
if sf == nil {
sf = sonyflake.NewSonyflake(sonyflake.Settings{})
}
return sf.NextID()
}

36
id/uuid.go Normal file
View File

@@ -0,0 +1,36 @@
package id
import (
"strings"
"github.com/google/uuid"
"github.com/lithammer/shortuuid/v4"
"github.com/rs/xid"
"github.com/segmentio/ksuid"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func NewGUIDv4(withHyphen bool) string {
id := uuid.NewString()
if !withHyphen {
id = strings.ReplaceAll(id, "-", "")
}
return id
}
func NewShortUUID() string {
return shortuuid.New()
}
func NewKSUID() string {
return ksuid.New().String()
}
func NewXID() string {
return xid.New().String()
}
func NewMongoObjectID() string {
objID := primitive.NewObjectID()
return objID.String()
}

266
id/uuid_test.go Normal file
View File

@@ -0,0 +1,266 @@
package id
import (
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewGUIDv4(t *testing.T) {
// 测试带有连字符的 GUID
withHyphen := NewGUIDv4(true)
assert.NotEmpty(t, withHyphen)
assert.Equal(t, 4, strings.Count(withHyphen, "-"), "GUID 应包含 4 个连字符")
// 测试不带连字符的 GUID
withoutHyphen := NewGUIDv4(false)
assert.NotEmpty(t, withoutHyphen)
assert.Equal(t, 0, strings.Count(withoutHyphen, "-"), "GUID 不应包含连字符")
// 验证 GUID 的长度
assert.Equal(t, 36, len(withHyphen), "带连字符的 GUID 长度应为 36")
assert.Equal(t, 32, len(withoutHyphen), "不带连字符的 GUID 长度应为 32")
}
func TestNewGUIDv4CollisionRate(t *testing.T) {
const (
testCount = 100000 // 测试生成的GUID数量
withHyphen = true // 是否带连字符
)
ids := make(map[string]struct{})
for i := 0; i < testCount; i++ {
id := NewGUIDv4(withHyphen)
if _, exists := ids[id]; exists {
t.Errorf("碰撞发生: %s 已存在", id)
}
ids[id] = struct{}{}
}
t.Logf("生成了 %d 个GUID无碰撞。", testCount)
}
func TestNewShortUUID(t *testing.T) {
// 测试生成的 ShortUUID 是否非空
id := NewShortUUID()
assert.NotEmpty(t, id, "生成的 ShortUUID 应该非空")
// 测试生成的 ShortUUID 的长度是否符合预期
assert.True(t, len(id) > 0, "生成的 ShortUUID 长度应该大于 0")
}
func TestNewShortUUIDCollisionRate(t *testing.T) {
const testCount = 100000 // 测试生成的ShortUUID数量
ids := make(map[string]struct{})
for i := 0; i < testCount; i++ {
id := NewShortUUID()
if _, exists := ids[id]; exists {
t.Errorf("碰撞发生: %s 已存在", id)
}
ids[id] = struct{}{}
}
t.Logf("生成了 %d 个ShortUUID无碰撞。", testCount)
}
func TestNewKSUID(t *testing.T) {
// 测试生成的 KSUID 是否非空
id := NewKSUID()
assert.NotEmpty(t, id, "生成的 KSUID 应该非空")
// 测试生成的 KSUID 的长度是否符合预期
assert.Equal(t, 27, len(id), "生成的 KSUID 长度应该为 27")
}
func TestNewKSUIDCollisionRate(t *testing.T) {
const testCount = 100000 // 测试生成的KSUID数量
ids := make(map[string]struct{})
for i := 0; i < testCount; i++ {
id := NewKSUID()
if _, exists := ids[id]; exists {
t.Errorf("碰撞发生: %s 已存在", id)
}
ids[id] = struct{}{}
}
t.Logf("生成了 %d 个KSUID无碰撞。", testCount)
}
func TestNewXID(t *testing.T) {
// 测试生成的 XID 是否非空
id := NewXID()
assert.NotEmpty(t, id, "生成的 XID 应该非空")
// 测试生成的 XID 的长度是否符合预期
assert.Equal(t, 20, len(id), "生成的 XID 长度应该为 20")
}
func TestNewXIDCollisionRate(t *testing.T) {
const testCount = 100000 // 测试生成的XID数量
ids := make(map[string]struct{})
for i := 0; i < testCount; i++ {
id := NewXID()
if _, exists := ids[id]; exists {
t.Errorf("碰撞发生: %s 已存在", id)
}
ids[id] = struct{}{}
}
t.Logf("生成了 %d 个XID无碰撞。", testCount)
}
func TestNewSnowflakeID(t *testing.T) {
tests := []struct {
workerId int64
expectErr bool
}{
{0, false}, // 有效的 workerId
{31, false}, // 有效的 workerId
{32, false}, // 有效的 workerId
{-1, true}, // 无效的 workerId
}
for _, tt := range tests {
id, err := NewSnowflakeID(tt.workerId)
if (err != nil) != tt.expectErr {
t.Errorf("NewSnowflakeID(%d) 错误状态不符合预期: %v", tt.workerId, err)
}
if err == nil && id <= 0 {
t.Errorf("NewSnowflakeID(%d) 生成的ID无效: %d", tt.workerId, id)
}
t.Logf("NewSnowflakeID(%d) ID: %d", tt.workerId, id)
}
}
func TestNewSnowflakeIDCollisionRate(t *testing.T) {
const (
workerId = 0
testCount = 100000 // 测试生成的ID数量
)
ids := make(map[int64]struct{})
for i := 0; i < testCount; i++ {
id, err := NewSnowflakeID(workerId)
if err != nil {
t.Errorf("生成ID时出现错误: %v", err)
continue
}
if _, exists := ids[id]; exists {
t.Errorf("碰撞发生: %d 已存在", id)
}
ids[id] = struct{}{}
}
t.Logf("生成了 %d 个Snowflake ID无碰撞。", testCount)
}
func TestConcurrentNewSnowflakeIDCollisionRate(t *testing.T) {
const (
workerId = 0 // Snowflake 工作节点 ID
testCount = 100000 // 测试生成的 ID 数量
workerCount = 10 // 并发工作线程数
)
var mu sync.Mutex
ids := make(map[int64]struct{})
var wg sync.WaitGroup
for w := 0; w < workerCount; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < testCount/workerCount; i++ {
id, err := NewSnowflakeID(workerId)
if err != nil {
t.Errorf("生成 Snowflake ID 时出现错误: %v", err)
continue
}
mu.Lock()
if _, exists := ids[id]; exists {
t.Errorf("碰撞发生: %d 已存在", id)
}
ids[id] = struct{}{}
mu.Unlock()
}
}()
}
wg.Wait()
t.Logf("生成了 %d 个 Snowflake ID无碰撞。", testCount)
}
func TestNewSonyflakeID(t *testing.T) {
// 测试生成的 Sonyflake ID 是否有效
id, err := NewSonyflakeID()
t.Logf("sonyflake id: %v", id)
assert.NoError(t, err, "生成 Sonyflake ID 时不应出现错误")
assert.True(t, id > 0, "生成的 Sonyflake ID 应该是正数")
}
func TestNewSonyflakeIDCollisionRate(t *testing.T) {
const testCount = 100000 // 测试生成的 Sonyflake ID 数量
ids := make(map[uint64]struct{})
for i := 0; i < testCount; i++ {
id, err := NewSonyflakeID()
if err != nil {
t.Errorf("生成 Sonyflake ID 时出现错误: %v", err)
continue
}
if _, exists := ids[id]; exists {
t.Errorf("碰撞发生: %d 已存在", id)
}
ids[id] = struct{}{}
}
t.Logf("生成了 %d 个 Sonyflake ID无碰撞。", testCount)
}
func TestConcurrentNewSonyflakeIDCollisionRate(t *testing.T) {
const (
testCount = 100000 // 测试生成的 Sonyflake ID 数量
workerCount = 10 // 并发工作线程数
)
var mu sync.Mutex
ids := make(map[uint64]struct{})
var wg sync.WaitGroup
for w := 0; w < workerCount; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < testCount/workerCount; i++ {
id, err := NewSonyflakeID()
if err != nil {
t.Errorf("生成 Sonyflake ID 时出现错误: %v", err)
continue
}
mu.Lock()
if _, exists := ids[id]; exists {
t.Errorf("碰撞发生: %d 已存在", id)
}
ids[id] = struct{}{}
mu.Unlock()
}
}()
}
wg.Wait()
t.Logf("生成了 %d 个 Sonyflake ID无碰撞。", testCount)
}
func TestNewMongoObjectID(t *testing.T) {
// 测试生成的 ObjectID 是否非空
id := NewMongoObjectID()
assert.NotEmpty(t, id, "生成的 Mongo ObjectID 应该非空")
// 测试生成的 ObjectID 的长度是否符合预期
assert.Equal(t, 36, len(id), "生成的 Mongo ObjectID 长度应该为 36")
}

12
ioutil/file.go Normal file
View File

@@ -0,0 +1,12 @@
package ioutil
import "os"
// ReadFile 读取文件
func ReadFile(path string) []byte {
content, err := os.ReadFile(path)
if err != nil {
return nil
}
return content
}

279
ioutil/path.go Normal file
View File

@@ -0,0 +1,279 @@
package ioutil
import (
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/gobwas/glob"
)
// GetWorkingDirPath 获取工作路径
func GetWorkingDirPath() string {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
return dir
}
// GetExePath 获取可执行程序路径
func GetExePath() string {
ex, err := os.Executable()
if err != nil {
panic(err)
}
exePath := filepath.Dir(ex)
return exePath
}
// GetAbsPath 获取绝对路径
func GetAbsPath() string {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
panic(err)
}
return dir
}
// GetFileList 获取文件夹下面的所有文件的列表
func GetFileList(root string) []string {
var files []string
if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
files = append(files, path)
return nil
}); err != nil {
return nil
}
return files
}
// GetFolderNameList 获取当前文件夹下面的所有文件夹名的列表
func GetFolderNameList(root string) []string {
var names []string
fs, _ := os.ReadDir(root)
for _, file := range fs {
if file.IsDir() {
names = append(names, file.Name())
}
}
return names
}
// MatchPath Returns whether a given path matches a glob pattern.
//
// via github.com/gobwas/glob:
//
// Compile creates Glob for given pattern and strings (if any present after pattern) as separators.
// The pattern syntax is:
//
// pattern:
// { term }
//
// term:
// `*` matches any sequence of non-separator characters
// `**` matches any sequence of characters
// `?` matches any single non-separator character
// `[` [ `!` ] { character-range } `]`
// character class (must be non-empty)
// `{` pattern-list `}`
// pattern alternatives
// c matches character c (c != `*`, `**`, `?`, `\`, `[`, `{`, `}`)
// `\` c matches character c
//
// character-range:
// c matches character c (c != `\\`, `-`, `]`)
// `\` c matches character c
// lo `-` hi matches character c for lo <= c <= hi
//
// pattern-list:
// pattern { `,` pattern }
// comma-separated (without spaces) patterns
func MatchPath(pattern string, path string) bool {
if g, err := glob.Compile(pattern); err == nil {
return g.Match(path)
}
return false
}
// ExpandUser replaces the tilde (~) in a path into the current user's home directory.
func ExpandUser(path string) (string, error) {
if u, err := user.Current(); err == nil {
fullTilde := fmt.Sprintf("~%s", u.Name)
if strings.HasPrefix(path, `~/`) || path == `~` {
return strings.Replace(path, `~`, u.HomeDir, 1), nil
}
if strings.HasPrefix(path, fullTilde+`/`) || path == fullTilde {
return strings.Replace(path, fullTilde, u.HomeDir, 1), nil
}
return path, nil
} else {
return path, err
}
}
// IsNonemptyExecutableFile Returns true if the given path is a regular file, is executable by any user, and has a non-zero size.
func IsNonemptyExecutableFile(path string) bool {
if stat, err := os.Stat(path); err == nil && stat.Size() > 0 && (stat.Mode().Perm()&0111) != 0 {
return true
}
return false
}
// IsNonemptyFile Returns true if the given path is a regular file with a non-zero size.
func IsNonemptyFile(path string) bool {
if FileExists(path) {
if stat, err := os.Stat(path); err == nil && stat.Size() > 0 {
return true
}
}
return false
}
// IsNonemptyDir Returns true if the given path is a directory with items in it.
func IsNonemptyDir(path string) bool {
if DirExists(path) {
if entries, err := ioutil.ReadDir(path); err == nil && len(entries) > 0 {
return true
}
}
return false
}
// Exists Returns true if the given path exists.
func Exists(path string) bool {
if _, err := os.Stat(path); err == nil {
return true
}
return false
}
// LinkExists Returns true if the given path exists and is a symbolic link.
func LinkExists(path string) bool {
if stat, err := os.Stat(path); err == nil {
return IsSymlink(stat.Mode())
}
return false
}
// FileExists Returns true if the given path exists and is a regular file.
func FileExists(path string) bool {
if stat, err := os.Stat(path); err == nil {
return stat.Mode().IsRegular()
}
return false
}
// DirExists Returns true if the given path exists and is a directory.
func DirExists(path string) bool {
if stat, err := os.Stat(path); err == nil {
return stat.IsDir()
}
return false
}
// PathExist 路径是否存在
func PathExist(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}
func IsDevice(mode os.FileMode) bool {
return mode&os.ModeDevice != 0
}
func IsCharDevice(mode os.FileMode) bool {
return mode&os.ModeCharDevice != 0
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
func IsSocket(mode os.FileMode) bool {
return mode&os.ModeSocket != 0
}
func IsSticky(mode os.FileMode) bool {
return mode&os.ModeSticky != 0
}
func IsSetuid(mode os.FileMode) bool {
return mode&os.ModeSetuid != 0
}
func IsSetgid(mode os.FileMode) bool {
return mode&os.ModeSetgid != 0
}
func IsTemporary(mode os.FileMode) bool {
return mode&os.ModeTemporary != 0
}
func IsExclusive(mode os.FileMode) bool {
return mode&os.ModeExclusive != 0
}
func IsAppend(mode os.FileMode) bool {
return mode&os.ModeAppend != 0
}
// IsReadable Returns true if the given file can be opened for reading by the current user.
func IsReadable(filename string) bool {
if f, err := os.OpenFile(filename, os.O_RDONLY, 0); err == nil {
defer f.Close()
return true
} else {
return false
}
}
// IsWritable Returns true if the given file can be opened for writing by the current user.
func IsWritable(filename string) bool {
if f, err := os.OpenFile(filename, os.O_WRONLY, 0); err == nil {
defer f.Close()
return true
} else {
return false
}
}
// IsAppendable Returns true if the given file can be opened for appending by the current user.
func IsAppendable(filename string) bool {
if f, err := os.OpenFile(filename, os.O_APPEND, 0); err == nil {
defer f.Close()
return true
} else {
return false
}
}

101
ioutil/path_test.go Normal file
View File

@@ -0,0 +1,101 @@
package ioutil
import (
"os"
"os/user"
"testing"
"github.com/stretchr/testify/require"
)
func TestExpandUser(t *testing.T) {
assert := require.New(t)
var v string
var err error
u, _ := user.Current()
v, err = ExpandUser(`/dev/null`)
assert.Equal(v, `/dev/null`)
assert.Nil(err)
v, err = ExpandUser(`~`)
assert.Equal(v, u.HomeDir)
assert.Nil(err)
v, err = ExpandUser("~" + u.Name)
assert.Equal(v, u.HomeDir)
assert.Nil(err)
v, err = ExpandUser("~/test-123")
assert.Equal(v, u.HomeDir+"/test-123")
assert.Nil(err)
v, err = ExpandUser("~" + u.Name + "/test-123")
assert.Equal(v, u.HomeDir+"/test-123")
assert.Nil(err)
v, err = ExpandUser("~/test-123/~/123")
assert.Equal(v, u.HomeDir+"/test-123/~/123")
assert.Nil(err)
v, err = ExpandUser("~" + u.Name + "/test-123/~" + u.Name + "/123")
assert.Equal(v, u.HomeDir+"/test-123/~"+u.Name+"/123")
assert.Nil(err)
assert.False(IsNonemptyFile(`/nonexistent.txt`))
assert.False(IsNonemptyDir(`/nonexistent/dir`))
assert.True(IsNonemptyFile(`/etc/hosts`))
assert.True(IsNonemptyDir(`/etc`))
x, err := os.Executable()
assert.NoError(err)
assert.True(IsNonemptyExecutableFile(x))
}
func TestMatchPath(t *testing.T) {
require.True(t, MatchPath(`**`, `/hello/there.txt`))
require.True(t, MatchPath(`*.txt`, `/hello/there.txt`))
require.True(t, MatchPath(`**/*.txt`, `/hello/there.txt`))
require.False(t, MatchPath(`**/*.txt`, `/hello/there.jpg`))
require.True(t, MatchPath("* ?at * eyes", "my cat has very bright eyes"))
require.True(t, MatchPath("", ""))
require.False(t, MatchPath("", "b"))
require.True(t, MatchPath("*ä", "åä"))
require.True(t, MatchPath("abc", "abc"))
require.True(t, MatchPath("a*c", "abc"))
require.True(t, MatchPath("a*c", "a12345c"))
require.True(t, MatchPath("a?c", "a1c"))
require.True(t, MatchPath("?at", "cat"))
require.True(t, MatchPath("?at", "fat"))
require.True(t, MatchPath("*", "abc"))
require.True(t, MatchPath(`\*`, "*"))
require.False(t, MatchPath("?at", "at"))
require.True(t, MatchPath("*test", "this is a test"))
require.True(t, MatchPath("this*", "this is a test"))
require.True(t, MatchPath("*is *", "this is a test"))
require.True(t, MatchPath("*is*a*", "this is a test"))
require.True(t, MatchPath("**test**", "this is a test"))
require.True(t, MatchPath("**is**a***test*", "this is a test"))
require.False(t, MatchPath("*is", "this is a test"))
require.False(t, MatchPath("*no*", "this is a test"))
require.True(t, MatchPath("[!a]*", "this is a test3"))
require.True(t, MatchPath("*abc", "abcabc"))
require.True(t, MatchPath("**abc", "abcabc"))
require.True(t, MatchPath("???", "abc"))
require.True(t, MatchPath("?*?", "abc"))
require.True(t, MatchPath("?*?", "ac"))
require.False(t, MatchPath("sta", "stagnation"))
require.True(t, MatchPath("sta*", "stagnation"))
require.False(t, MatchPath("sta?", "stagnation"))
require.False(t, MatchPath("sta?n", "stagnation"))
require.True(t, MatchPath("{abc,def}ghi", "defghi"))
require.True(t, MatchPath("{abc,abcd}a", "abcda"))
require.True(t, MatchPath("{a,ab}{bc,f}", "abc"))
require.True(t, MatchPath("{*,**}{a,b}", "ab"))
require.False(t, MatchPath("{*,**}{a,b}", "ac"))
require.True(t, MatchPath("/{rate,[a-z][a-z][a-z]}*", "/rate"))
require.True(t, MatchPath("/{rate,[0-9][0-9][0-9]}*", "/rate"))
require.True(t, MatchPath("/{rate,[a-z][a-z][a-z]}*", "/usd"))
}

21
jwtutil/go.mod Normal file
View File

@@ -0,0 +1,21 @@
module github.com/tx7do/go-utils/jwtutil
go 1.23.0
toolchain go1.24.1
require (
github.com/go-kratos/kratos/v2 v2.8.4
github.com/golang-jwt/jwt/v5 v5.1.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

28
jwtutil/go.sum Normal file
View File

@@ -0,0 +1,28 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs=
github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw=
github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU=
github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

355
jwtutil/jwt.go Normal file
View File

@@ -0,0 +1,355 @@
package jwtutil
import (
"fmt"
"net/http"
"time"
"github.com/go-kratos/kratos/v2/encoding"
"github.com/golang-jwt/jwt/v5"
)
// ParseJWTPayload 使用 github.com/golang-jwt/jwt/v5 从 JWT 中解析出 payload
func ParseJWTPayload(tokenString string) (jwt.MapClaims, error) {
// 不验证签名,仅解析
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
return claims, nil
}
// ParseJWTClaimsToStruct 解析 JWT 的负载部分,不验证签名,仅解析。
func ParseJWTClaimsToStruct[T any](tokenString string) (*T, error) {
// 不验证签名,仅解析
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
codec := encoding.GetCodec("json")
// 将 claims 转换为目标类型
claimsBytes, err := codec.Marshal(claims)
if err != nil {
return nil, fmt.Errorf("failed to marshal claims: %v", err)
}
var ret T
err = codec.Unmarshal(claimsBytes, &ret)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal claims: %v", err)
}
return &ret, nil
}
// VerifyJWT 验证 JWT 的签名
func VerifyJWT(tokenString string, secretKey []byte) (jwt.MapClaims, error) {
// 解析并验证 JWT
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 确保使用的是 HMAC 签名方法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return secretKey, nil
})
if err != nil {
return nil, err
}
// 验证 token 是否有效
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
func GetJWTClaims(tokenString string) (map[string]interface{}, error) {
claims, err := ParseJWTPayload(tokenString)
if err != nil {
return nil, err
}
return claims, nil
}
// GenerateJWT 生成 JWT
func GenerateJWT(mapClaims jwt.MapClaims, secretKey []byte, signingMethod jwt.SigningMethod) (string, error) {
// 检查密钥是否为空,密钥不能为空,否则会有安全隐患。
if len(secretKey) == 0 {
return "", fmt.Errorf("secret key cannot be empty")
}
// 创建一个新的 JWT Token
token := jwt.NewWithClaims(signingMethod, mapClaims)
// 使用 HMAC 签名方法签名 token
signedToken, err := token.SignedString(secretKey)
if err != nil {
return "", err
}
return signedToken, nil
}
// GenerateGenericJWT 生成 JWT
func GenerateGenericJWT[T any](payload T, secretKey []byte, signingMethod jwt.SigningMethod) (string, error) {
// 检查密钥是否为空,密钥不能为空,否则会有安全隐患。
if len(secretKey) == 0 {
return "", fmt.Errorf("secret key cannot be empty")
}
// 将泛型 payload 转换为 jwt.MapClaims
claimsMap, err := ToMapClaims(payload)
if err != nil {
return "", fmt.Errorf("failed to convert payload to claims: %v", err)
}
// 创建一个新的 JWT Token
token := jwt.NewWithClaims(signingMethod, claimsMap)
// 使用 HMAC 签名方法签名 token
signedToken, err := token.SignedString(secretKey)
if err != nil {
return "", err
}
return signedToken, nil
}
// ToMapClaims 将泛型 payload 转换为 jwt.MapClaims
func ToMapClaims[T any](payload T) (jwt.MapClaims, error) {
codec := encoding.GetCodec("json")
payloadBytes, err := codec.Marshal(payload)
if err != nil {
return nil, err
}
var claims jwt.MapClaims
err = codec.Unmarshal(payloadBytes, &claims)
if err != nil {
return nil, err
}
return claims, nil
}
// RefreshJWT 刷新JWT
func RefreshJWT(tokenString string, secretKey []byte, newExpiration time.Time) (string, error) {
// 解析 JWT 的 payload
claims, err := ParseJWTPayload(tokenString)
if err != nil {
return "", err
}
// 更新过期时间
claims["exp"] = newExpiration.Unix()
// 生成新的 JWT
newToken, err := GenerateJWT(claims, secretKey, jwt.SigningMethodHS256)
if err != nil {
return "", err
}
return newToken, nil
}
// GenerateJWTWithHeader 生成带自定义头部的JWT
func GenerateJWTWithHeader(payload jwt.MapClaims, secretKey []byte, signingMethod jwt.SigningMethod, customHeader map[string]interface{}) (string, error) {
// 检查密钥是否为空
if len(secretKey) == 0 {
return "", fmt.Errorf("secret key cannot be empty")
}
// 创建一个新的 JWT Token
token := jwt.NewWithClaims(signingMethod, payload)
// 添加自定义头部
for key, value := range customHeader {
token.Header[key] = value
}
// 使用密钥签名生成 JWT
signedToken, err := token.SignedString(secretKey)
if err != nil {
return "", err
}
return signedToken, nil
}
// ExtractJWTFromRequest 从请求中提取JWT
func ExtractJWTFromRequest(r *http.Request) (string, error) {
// 从 Authorization Header 中提取 JWT
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return "", fmt.Errorf("authorization header is missing")
}
// 检查是否以 "Bearer " 开头
const bearerPrefix = "Bearer "
if len(authHeader) <= len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
return "", fmt.Errorf("invalid authorization header format")
}
// 提取 JWT 部分
token := authHeader[len(bearerPrefix):]
return token, nil
}
// GenerateShortLivedJWT 生成短期有效的JWT
func GenerateShortLivedJWT(payload jwt.MapClaims, secretKey []byte, signingMethod jwt.SigningMethod, duration time.Duration) (string, error) {
// 检查密钥是否为空
if len(secretKey) == 0 {
return "", fmt.Errorf("secret key cannot be empty")
}
// 设置过期时间
expirationTime := time.Now().Add(duration).Unix()
payload["exp"] = expirationTime
// 创建一个新的 JWT Token
token := jwt.NewWithClaims(signingMethod, payload)
// 使用密钥签名生成 JWT
signedToken, err := token.SignedString(secretKey)
if err != nil {
return "", err
}
return signedToken, nil
}
// ValidateJWTAudience 验证JWT受众
func ValidateJWTAudience(tokenString string, expectedAudience string) (bool, error) {
// 解析 JWT 的 payload
claims, err := ParseJWTPayload(tokenString)
if err != nil {
return false, err
}
// 获取 `aud` 字段
audience, err := claims.GetAudience()
if err != nil {
return false, err
}
// 验证 `aud` 是否包含预期值
for _, aud := range audience {
if aud == expectedAudience {
return true, nil
}
}
return false, nil
}
// ValidateJWTAlgorithm 验证JWT算法
func ValidateJWTAlgorithm(tokenString string, expectedAlgorithm string) (bool, error) {
// 使用 jwt.Parser 解析 JWT不验证签名
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return false, err
}
// 获取 `alg` 字段
alg, ok := token.Header["alg"].(string)
if !ok {
return false, fmt.Errorf("invalid or missing algorithm in token header")
}
// 验证算法是否符合预期
if alg != expectedAlgorithm {
return false, nil
}
return true, nil
}
// IsJWTExpired 检查JWT是否过期
func IsJWTExpired(tokenString string) (bool, error) {
// 解析 JWT 的 payload
claims, err := ParseJWTPayload(tokenString)
if err != nil {
return false, err
}
// 获取 `exp` 字段
exp, err := claims.GetExpirationTime()
if err != nil {
return false, err
}
// 检查当前时间是否超过 `exp`
if time.Now().After(exp.Time) {
return true, nil
}
return false, nil
}
// GetJWTHeader 获取JWT头部
func GetJWTHeader(tokenString string) (map[string]interface{}, error) {
// 使用 jwt.Parser 解析 JWT不验证签名
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, err
}
return token.Header, nil
}
// ValidateJWTIssuer 验证JWT发行者
func ValidateJWTIssuer(tokenString string, expectedIssuer string) (bool, error) {
// 解析 JWT 的 payload
claims, err := ParseJWTPayload(tokenString)
if err != nil {
return false, err
}
// 获取 `iss` 字段
issuer, err := claims.GetIssuer()
if err != nil {
return false, err
}
// 验证 `iss` 是否符合预期
if issuer != expectedIssuer {
return false, nil
}
return true, nil
}
// GetJWTIssuedAt 获取JWT签发时间
func GetJWTIssuedAt(tokenString string) (*time.Time, error) {
claims, err := ParseJWTPayload(tokenString)
if err != nil {
return nil, err
}
iat, err := claims.GetIssuedAt()
if err != nil {
return nil, err
}
return &iat.Time, nil
}

519
jwtutil/jwt_test.go Normal file
View File

@@ -0,0 +1,519 @@
package jwtutil
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
_ "github.com/go-kratos/kratos/v2/encoding/json"
_ "github.com/go-kratos/kratos/v2/encoding/proto"
)
func TestParseJWTPayload(t *testing.T) {
var secretKey = []byte("secret")
// 测试有效的 JWT 负载解析
token, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
"exp": 1672728000,
}, secretKey, jwt.SigningMethodHS256)
if err != nil {
t.Fatalf("failed to create token: %v", err)
}
result, err := ParseJWTPayload(token)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "userId", result["sub"])
assert.Equal(t, float64(1672728000), result["exp"]) // 注意JSON 解码后数字为 float64
// 测试无效的 JWT
invalidToken := "invalid.token.string"
result, err = ParseJWTPayload(invalidToken)
assert.Error(t, err)
assert.Nil(t, result)
// 测试空字符串
result, err = ParseJWTPayload("")
assert.Error(t, err)
assert.Nil(t, result)
}
func TestParseJWTClaimsToStruct(t *testing.T) {
// 测试有效的 JWT 负载解析
type Payload struct {
Sub string `json:"sub"`
Exp int64 `json:"exp"`
}
var secretKey = []byte("secret")
// 验证签名以生成有效的 token
token, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
"exp": 1672728000,
}, secretKey, jwt.SigningMethodHS256)
if err != nil {
t.Fatalf("failed to create token: %v", err)
}
result, err := ParseJWTClaimsToStruct[Payload](token)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "userId", result.Sub)
assert.Equal(t, int64(1672728000), result.Exp)
// 测试无效的 JWT
invalidToken := "invalid.token.string"
result, err = ParseJWTClaimsToStruct[Payload](invalidToken)
assert.Error(t, err)
assert.Nil(t, result)
// 测试空字符串
result, err = ParseJWTClaimsToStruct[Payload]("")
assert.Error(t, err)
assert.Nil(t, result)
}
func TestVerifyJWT(t *testing.T) {
// 测试有效的 JWT 验证
secretKey := []byte("secret")
token, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
}, secretKey, jwt.SigningMethodHS256)
if err != nil {
t.Fatalf("failed to create token: %v", err)
}
result, err := VerifyJWT(token, secretKey)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "userId", result["sub"])
// 测试无效的签名
invalidSecretKey := []byte("invalid_secret")
result, err = VerifyJWT(token, invalidSecretKey)
assert.Error(t, err)
assert.Nil(t, result)
// 测试无效的 JWT 格式
invalidToken := "invalid.token.string"
result, err = VerifyJWT(invalidToken, secretKey)
assert.Error(t, err)
assert.Nil(t, result)
// 测试空字符串
result, err = VerifyJWT("", secretKey)
assert.Error(t, err)
assert.Nil(t, result)
}
func TestGenerateGenericJWT(t *testing.T) {
// 定义测试用的 payload 和密钥
type Payload struct {
Sub string `json:"sub"`
Exp int64 `json:"exp"`
}
secretKey := []byte("secret")
payload := Payload{
Sub: "userId",
Exp: 1672728000,
}
// 测试生成有效的 JWT
token, err := GenerateGenericJWT(payload, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, token)
// 验证生成的 JWT 是否正确
parsedPayload, err := ParseJWTClaimsToStruct[Payload](token)
assert.NoError(t, err)
assert.NotNil(t, parsedPayload)
assert.Equal(t, payload.Sub, parsedPayload.Sub)
assert.Equal(t, payload.Exp, parsedPayload.Exp)
// 测试使用空密钥生成 JWT
emptySecretKey := []byte("")
token, err = GenerateGenericJWT(payload, emptySecretKey, jwt.SigningMethodHS256)
assert.Error(t, err)
assert.Empty(t, token)
}
func TestGenerateJWT(t *testing.T) {
// 定义测试用的 payload 和密钥
payload := jwt.MapClaims{
"sub": "userId",
"exp": 1672728000,
}
secretKey := []byte("secret")
// 测试生成有效的 JWT
token, err := GenerateJWT(payload, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, token)
// 验证生成的 JWT 是否正确
parsedPayload, err := ParseJWTPayload(token)
assert.NoError(t, err)
assert.NotNil(t, parsedPayload)
assert.Equal(t, payload["sub"], parsedPayload["sub"])
assert.Equal(t, float64(payload["exp"].(int)), parsedPayload["exp"].(float64)) // 注意JSON 解码后数字为 float64
// 测试使用空密钥生成 JWT
emptySecretKey := []byte("")
token, err = GenerateJWT(payload, emptySecretKey, jwt.SigningMethodHS256)
assert.Error(t, err)
assert.Empty(t, token)
}
func TestRefreshJWT(t *testing.T) {
secretKey := []byte("secret")
// 创建一个初始的 JWT
originalToken, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
"exp": time.Now().Add(1 * time.Hour).Unix(),
}, secretKey, jwt.SigningMethodHS256)
if err != nil {
t.Fatalf("failed to create token: %v", err)
}
// 刷新 JWT设置新的过期时间
newExpiration := time.Now().Add(2 * time.Hour)
refreshedToken, err := RefreshJWT(originalToken, secretKey, newExpiration)
assert.NoError(t, err)
assert.NotEmpty(t, refreshedToken)
// 验证刷新后的 JWT 是否包含新的过期时间
claims, err := ParseJWTPayload(refreshedToken)
assert.NoError(t, err)
assert.NotNil(t, claims)
exp, ok := claims["exp"].(float64)
assert.True(t, ok)
assert.Equal(t, newExpiration.Unix(), int64(exp))
// 测试无效的 JWT
invalidToken := "invalid.token.string"
_, err = RefreshJWT(invalidToken, secretKey, newExpiration)
assert.Error(t, err)
// 测试空字符串
_, err = RefreshJWT("", secretKey, newExpiration)
assert.Error(t, err)
}
func TestGenerateJWTWithHeader(t *testing.T) {
// 定义测试用的 payload、密钥和自定义头部
payload := jwt.MapClaims{
"sub": "userId",
"exp": time.Now().Add(1 * time.Hour).Unix(),
}
secretKey := []byte("secret")
customHeader := map[string]interface{}{
"kid": "key-id-123",
}
// 测试生成有效的 JWT
token, err := GenerateJWTWithHeader(payload, secretKey, jwt.SigningMethodHS256, customHeader)
assert.NoError(t, err)
assert.NotEmpty(t, token)
// 验证生成的 JWT 是否包含自定义头部
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
parsedToken, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
assert.NoError(t, err)
assert.NotNil(t, parsedToken)
// 检查头部是否包含自定义字段
assert.Equal(t, "key-id-123", parsedToken.Header["kid"])
// 测试使用空密钥生成 JWT
emptySecretKey := []byte("")
token, err = GenerateJWTWithHeader(payload, emptySecretKey, jwt.SigningMethodHS256, customHeader)
assert.Error(t, err)
assert.Empty(t, token)
}
func TestExtractJWTFromRequest(t *testing.T) {
// 测试有效的 Authorization Header
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer valid.jwt.token")
token, err := ExtractJWTFromRequest(req)
assert.NoError(t, err)
assert.Equal(t, "valid.jwt.token", token)
// 测试缺少 Authorization Header
req = httptest.NewRequest(http.MethodGet, "/", nil)
token, err = ExtractJWTFromRequest(req)
assert.Error(t, err)
assert.Equal(t, "authorization header is missing", err.Error())
assert.Empty(t, token)
// 测试无效的 Authorization Header 格式
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "InvalidFormat")
token, err = ExtractJWTFromRequest(req)
assert.Error(t, err)
assert.Equal(t, "invalid authorization header format", err.Error())
assert.Empty(t, token)
// 测试空的 Bearer Token
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer ")
token, err = ExtractJWTFromRequest(req)
assert.Error(t, err)
assert.Empty(t, token)
}
func TestGenerateShortLivedJWT(t *testing.T) {
secretKey := []byte("secret")
payload := jwt.MapClaims{
"sub": "userId",
}
// 测试生成短期有效的 JWT
duration := 1 * time.Hour
token, err := GenerateShortLivedJWT(payload, secretKey, jwt.SigningMethodHS256, duration)
assert.NoError(t, err)
assert.NotEmpty(t, token)
// 验证生成的 JWT 是否包含正确的过期时间
claims, err := ParseJWTPayload(token)
assert.NoError(t, err)
assert.NotNil(t, claims)
exp, ok := claims["exp"].(float64)
assert.True(t, ok)
expectedExp := time.Now().Add(duration).Unix()
assert.InDelta(t, expectedExp, int64(exp), 5) // 允许 5 秒的时间误差
// 测试空密钥
emptySecretKey := []byte("")
token, err = GenerateShortLivedJWT(payload, emptySecretKey, jwt.SigningMethodHS256, duration)
assert.Error(t, err)
assert.Empty(t, token)
}
func TestValidateJWTAudience(t *testing.T) {
secretKey := []byte("secret")
// 创建一个包含受众的 JWT
token, err := GenerateJWT(jwt.MapClaims{
"aud": []string{"audience1", "audience2"},
}, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, token)
// 测试受众验证成功
valid, err := ValidateJWTAudience(token, "audience1")
assert.NoError(t, err)
assert.True(t, valid)
// 测试受众验证失败
valid, err = ValidateJWTAudience(token, "audience3")
assert.NoError(t, err)
assert.False(t, valid)
// 测试无效的 JWT
invalidToken := "invalid.token.string"
valid, err = ValidateJWTAudience(invalidToken, "audience1")
assert.Error(t, err)
assert.False(t, valid)
// 测试空字符串
valid, err = ValidateJWTAudience("", "audience1")
assert.Error(t, err)
assert.False(t, valid)
}
func TestValidateJWTAlgorithm(t *testing.T) {
secretKey := []byte("secret")
// 测试有效的 JWT 算法验证
token, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
}, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, token)
valid, err := ValidateJWTAlgorithm(token, "HS256")
assert.NoError(t, err)
assert.True(t, valid)
// 测试无效的算法
valid, err = ValidateJWTAlgorithm(token, "RS256")
assert.NoError(t, err)
assert.False(t, valid)
// 测试无效的 JWT
invalidToken := "invalid.token.string"
valid, err = ValidateJWTAlgorithm(invalidToken, "HS256")
assert.Error(t, err)
assert.False(t, valid)
// 测试空字符串
valid, err = ValidateJWTAlgorithm("", "HS256")
assert.Error(t, err)
assert.False(t, valid)
}
func TestIsJWTExpired(t *testing.T) {
secretKey := []byte("secret")
// 测试未过期的 JWT
notExpiredToken, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
"exp": time.Now().Add(1 * time.Hour).Unix(),
}, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, notExpiredToken)
isExpired, err := IsJWTExpired(notExpiredToken)
assert.NoError(t, err)
assert.False(t, isExpired)
// 测试已过期的 JWT
expiredToken, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
"exp": time.Now().Add(-1 * time.Hour).Unix(),
}, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, expiredToken)
isExpired, err = IsJWTExpired(expiredToken)
assert.NoError(t, err)
assert.True(t, isExpired)
// 测试无效的 JWT
invalidToken := "invalid.token.string"
isExpired, err = IsJWTExpired(invalidToken)
assert.Error(t, err)
assert.False(t, isExpired)
// 测试空字符串
isExpired, err = IsJWTExpired("")
assert.Error(t, err)
assert.False(t, isExpired)
}
func TestGetJWTHeader(t *testing.T) {
secretKey := []byte("secret")
// 测试有效的 JWT 头部解析
token, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
}, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, token)
header, err := GetJWTHeader(token)
assert.NoError(t, err)
assert.NotNil(t, header)
assert.Equal(t, "JWT", header["typ"])
assert.Equal(t, "HS256", header["alg"])
// 测试无效的 JWT
invalidToken := "invalid.token.string"
header, err = GetJWTHeader(invalidToken)
assert.Error(t, err)
assert.Nil(t, header)
// 测试空字符串
header, err = GetJWTHeader("")
assert.Error(t, err)
assert.Nil(t, header)
}
func TestValidateJWTIssuer(t *testing.T) {
secretKey := []byte("secret")
// 测试有效的发行者验证
token, err := GenerateJWT(jwt.MapClaims{
"iss": "trusted-issuer",
}, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, token)
valid, err := ValidateJWTIssuer(token, "trusted-issuer")
assert.NoError(t, err)
assert.True(t, valid)
// 测试无效的发行者
valid, err = ValidateJWTIssuer(token, "untrusted-issuer")
assert.NoError(t, err)
assert.False(t, valid)
// 测试无效的 JWT
invalidToken := "invalid.token.string"
valid, err = ValidateJWTIssuer(invalidToken, "trusted-issuer")
assert.Error(t, err)
assert.False(t, valid)
// 测试空字符串
valid, err = ValidateJWTIssuer("", "trusted-issuer")
assert.Error(t, err)
assert.False(t, valid)
}
func TestGetJWTIssuedAt(t *testing.T) {
secretKey := []byte("secret")
// 测试有效的 JWT 签发时间
issuedAt := time.Now().Add(-1 * time.Hour).Unix()
token, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
"iat": issuedAt,
}, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, token)
iat, err := GetJWTIssuedAt(token)
assert.NoError(t, err)
assert.NotNil(t, iat)
assert.Equal(t, issuedAt, iat.Unix())
// 测试无效的 JWT
invalidToken := "invalid.token.string"
iat, err = GetJWTIssuedAt(invalidToken)
assert.Error(t, err)
assert.Nil(t, iat)
// 测试空字符串
iat, err = GetJWTIssuedAt("")
assert.Error(t, err)
assert.Nil(t, iat)
}
func TestGetJWTClaims(t *testing.T) {
secretKey := []byte("secret")
// 测试有效的 JWT
token, err := GenerateJWT(jwt.MapClaims{
"sub": "userId",
"exp": time.Now().Add(1 * time.Hour).Unix(),
}, secretKey, jwt.SigningMethodHS256)
assert.NoError(t, err)
assert.NotEmpty(t, token)
claims, err := GetJWTClaims(token)
assert.NoError(t, err)
assert.NotNil(t, claims)
assert.Equal(t, "userId", claims["sub"])
// 测试无效的 JWT
invalidToken := "invalid.token.string"
claims, err = GetJWTClaims(invalidToken)
assert.Error(t, err)
assert.Nil(t, claims)
// 测试空字符串
claims, err = GetJWTClaims("")
assert.Error(t, err)
assert.Nil(t, claims)
}

94
maputils/maputils.go Normal file
View File

@@ -0,0 +1,94 @@
// This package includes utility functions for handling and manipulating maputils.
// It draws inspiration from JavaScript and Python and uses Go generics as a basis.
package maputils
// Keys - takes a map with keys K and values V, returns a slice of type K of the map's keys.
// Note: Go maps do not preserve insertion order.
func Keys[K comparable, V any](mapInstance map[K]V) []K {
keys := make([]K, len(mapInstance))
i := 0
for k := range mapInstance {
keys[i] = k
i++
}
return keys
}
// Values - takes a map with keys K and values V, returns a slice of type V of the map's values.
// Note: Go maps do not preserve insertion order.
func Values[K comparable, V any](mapInstance map[K]V) []V {
values := make([]V, len(mapInstance))
i := 0
for _, v := range mapInstance {
values[i] = v
i++
}
return values
}
// Merge - takes an arbitrary number of map instances with keys K and values V and merges them into a single map.
// Note: merge works from left to right. If a key already exists in a previous map, its value is over-written.
func Merge[K comparable, V any](mapInstances ...map[K]V) map[K]V {
var mergedMapSize int
for _, mapInstance := range mapInstances {
mergedMapSize += len(mapInstance)
}
mergedMap := make(map[K]V, mergedMapSize)
for _, mapInstance := range mapInstances {
for k, v := range mapInstance {
mergedMap[k] = v
}
}
return mergedMap
}
// ForEach - given a map with keys K and values V, executes the passed in function for each key-value pair.
func ForEach[K comparable, V any](mapInstance map[K]V, function func(key K, value V)) {
for key, value := range mapInstance {
function(key, value)
}
}
// Drop - takes a map with keys K and values V, and a slice of keys K, dropping all the key-value pairs that match the keys in the slice.
// Note: this function will modify the passed in map. To get a different object, use the Copy function to pass a copy to this function.
func Drop[K comparable, V any](mapInstance map[K]V, keys []K) map[K]V {
for _, key := range keys {
delete(mapInstance, key)
}
return mapInstance
}
// Copy - takes a map with keys K and values V and returns a copy of the map.
func Copy[K comparable, V any](mapInstance map[K]V) map[K]V {
mapCopy := make(map[K]V, len(mapInstance))
for key, value := range mapInstance {
mapCopy[key] = value
}
return mapCopy
}
// Filter - takes a map with keys K and values V, and executes the passed in function for each key-value pair.
// If the filter function returns true, the key-value pair will be included in the output, otherwise it is filtered out.
func Filter[K comparable, V any](mapInstance map[K]V, function func(key K, value V) bool) map[K]V {
mapCopy := make(map[K]V, len(mapInstance))
for key, value := range mapInstance {
if function(key, value) {
mapCopy[key] = value
}
}
return mapCopy
}

139
maputils/maputils_test.go Normal file
View File

@@ -0,0 +1,139 @@
package maputils_test
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tx7do/go-utils/maputils"
)
func TestKeys(t *testing.T) {
var daysMap = map[string]int{
"Sunday": 1,
"Monday": 2,
"Tuesday": 3,
"Wednesday": 4,
"Thursday": 5,
"Friday": 6,
"Saturday": 7,
}
keys := maputils.Keys(daysMap)
days := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
sort.Strings(days)
sort.Strings(keys)
assert.Equal(t, days, keys)
}
func TestValues(t *testing.T) {
var daysMap = map[string]int{
"Sunday": 1,
"Monday": 2,
"Tuesday": 3,
"Wednesday": 4,
"Thursday": 5,
"Friday": 6,
"Saturday": 7,
}
values := maputils.Values(daysMap)
sort.Ints(values)
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7}, values)
}
func TestMerge(t *testing.T) {
first := map[string]string{
"George": "Harrison",
"Betty": "Davis",
}
second := map[string]string{
"Ronald": "Reagen",
"Betty": "Stewart",
}
assert.Equal(t,
map[string]string{
"George": "Harrison",
"Betty": "Stewart",
"Ronald": "Reagen",
}, maputils.Merge(first, second))
}
func TestForEach(t *testing.T) {
var daysMap = map[string]int{
"Sunday": 1,
"Monday": 2,
"Tuesday": 3,
"Wednesday": 4,
"Thursday": 5,
"Friday": 6,
"Saturday": 7,
}
sum := 0
maputils.ForEach(daysMap, func(_ string, day int) {
sum += day
})
assert.Equal(t, 28, sum)
}
func TestDrop(t *testing.T) {
var daysMap = map[string]int{
"Sunday": 1,
"Monday": 2,
"Tuesday": 3,
"Wednesday": 4,
"Thursday": 5,
"Friday": 6,
"Saturday": 7,
}
expectedResult := map[string]int{
"Sunday": 1,
"Friday": 6,
}
assert.Equal(t, expectedResult, maputils.Drop(daysMap, []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Saturday"}))
// ensure we do not modify the original value
assert.Equal(t, expectedResult, daysMap)
}
func TestCopy(t *testing.T) {
var daysMap = map[string]int{
"Sunday": 1,
"Monday": 2,
"Tuesday": 3,
"Wednesday": 4,
"Thursday": 5,
"Friday": 6,
"Saturday": 7,
}
daysCopy := maputils.Copy(daysMap)
assert.Equal(t, daysMap, daysCopy)
delete(daysCopy, "Sunday")
assert.NotEqual(t, daysMap, daysCopy)
}
func TestFilter(t *testing.T) {
var daysMap = map[string]int{
"Sunday": 1,
"Monday": 2,
"Tuesday": 3,
"Wednesday": 4,
"Thursday": 5,
"Friday": 6,
"Saturday": 7,
}
var expectedResult = map[string]int{
"Monday": 2,
"Wednesday": 4,
"Friday": 6,
}
filteredDays := maputils.Filter(daysMap, func(_ string, value int) bool {
return value%2 == 0
})
assert.Equal(t, expectedResult, filteredDays)
}

134
math/gaussian.go Normal file
View File

@@ -0,0 +1,134 @@
package math
import (
"math"
)
// prop
//mean: the mean (μ) of the distribution
//variance: the variance (σ^2) of the distribution
//standardDeviation: the standard deviation (σ) of the distribution
// combination
type Gaussian struct {
mean float64
variance float64
standardDeviation float64
}
func NewGaussian(mean, variance float64) *Gaussian {
if variance <= 0.0 {
panic("error")
}
return &Gaussian{
mean: mean,
variance: variance,
standardDeviation: math.Sqrt(float64(variance)),
}
}
// Erfc Complementary error function
// From Numerical Recipes in C 2e p221
func Erfc(x float64) float64 {
z := math.Abs(x)
t := 1 / (1 + z/2)
r := t * math.Exp(-z*z-1.26551223+t*(1.00002368+
t*(0.37409196+t*(0.09678418+t*(-0.18628806+
t*(0.27886807+t*(-1.13520398+t*(1.48851587+
t*(-0.82215223+t*0.17087277)))))))))
if x >= 0 {
return r
} else {
return 2 - r
}
}
// Ierfc Inverse complementary error function
// From Numerical Recipes 3e p265
func Ierfc(x float64) float64 {
if x >= 2 {
return -100
}
if x <= 0 {
return 100
}
var xx float64
if x < 1 {
xx = x
} else {
xx = 2 - x
}
t := math.Sqrt(-2 * math.Log(xx/2))
r := -0.70711 * ((2.30753+t*0.27061)/
(1+t*(0.99229+t*0.04481)) - t)
for j := 0; j < 2; j++ {
e := Erfc(r) - xx
r += e / (1.12837916709551257*math.Exp(-(r*r)) - r*e)
}
if x < 1 {
return r
} else {
return -r
}
}
// fromPrecisionMean Construct a new distribution from the precision and precisionmean
func fromPrecisionMean(precision, precisionmean float64) *Gaussian {
return NewGaussian(precisionmean/precision, 1/precision)
}
/// PROB
// Pdf pdf(x): the probability density function, which describes the probability
// of a random variable taking on the value x
func (g *Gaussian) Pdf(x float64) float64 {
m := g.standardDeviation * math.Sqrt(2*math.Pi)
e := math.Exp(-math.Pow(x-g.mean, 2) / (2 * g.variance))
return e / m
}
// Cdf cdf(x): the cumulative distribution function,
// which describes the probability of a random
// variable falling in the interval (−∞, x]
func (g *Gaussian) Cdf(x float64) float64 {
return 0.5 * Erfc(-(x-g.mean)/(g.standardDeviation*math.Sqrt(2)))
}
// Ppf ppf(x): the percent point function, the inverse of cdf
func (g *Gaussian) Ppf(x float64) float64 {
return g.mean - g.standardDeviation*math.Sqrt(2)*Ierfc(2*x)
}
// Add add(d): returns the result of adding this and the given distribution
func (g *Gaussian) Add(d *Gaussian) *Gaussian {
return NewGaussian(g.mean+d.mean, g.variance+d.variance)
}
// Sub sub(d): returns the result of subtracting this and the given distribution
func (g *Gaussian) Sub(d *Gaussian) *Gaussian {
return NewGaussian(g.mean-d.mean, g.variance+d.variance)
}
// Scale scale(c): returns the result of scaling this distribution by the given constant
func (g *Gaussian) Scale(c float64) *Gaussian {
return NewGaussian(g.mean*c, g.variance*c*c)
}
// Mul mul(d): returns the product distribution of this and the given distribution. If a constant is passed in the distribution is scaled.
func (g *Gaussian) Mul(d *Gaussian) *Gaussian {
precision := 1 / g.variance
dprecision := 1 / d.variance
return fromPrecisionMean(precision+dprecision, precision*g.mean+dprecision*d.mean)
}
// Div div(d): returns the quotient distribution of this and the given distribution. If a constant is passed in the distribution is scaled by 1/d.
func (g *Gaussian) Div(d *Gaussian) *Gaussian {
precision := 1 / g.variance
dprecision := 1 / d.variance
return fromPrecisionMean(precision-dprecision, precision*g.mean-dprecision*d.mean)
}

47
math/gaussian_test.go Normal file
View File

@@ -0,0 +1,47 @@
package math
import (
"fmt"
"testing"
)
func TestGaussian(t *testing.T) {
g := NewGaussian(3.0, 1)
fmt.Printf("g: %#v\n", g)
fmt.Printf("pdf: %f\n", g.Pdf(5))
fmt.Printf("cdf: %f\n", g.Cdf(2))
fmt.Printf("ppf: %f\n", g.Ppf(5))
d := NewGaussian(0, 1)
fmt.Printf("ppf: %f, %f\n", d.Pdf(-2), 0.053991)
fmt.Printf("ppf: %f, %f\n", d.Pdf(-1), 0.241971)
fmt.Printf("ppf: %f, %f\n", d.Pdf(0), 0.398942)
fmt.Printf("ppf: %f, %f\n", d.Pdf(1), 0.241971)
fmt.Printf("ppf: %f, %f\n", d.Pdf(2), 0.053991)
fmt.Printf("cdf: %f, %f\n", d.Cdf(-1.28155), 0.1)
fmt.Printf("cdf: %f, %f\n", d.Cdf(-0.67499), 0.25)
fmt.Printf("cdf: %f, %f\n", d.Cdf(0), 0.5)
fmt.Printf("cdf: %f, %f\n", d.Cdf(0.67499), 0.75)
fmt.Printf("cdf: %f, %f\n", d.Cdf(1.28155), 0.9)
fmt.Printf("ppf: %f, %f\n", d.Ppf(0.1), -1.28155)
fmt.Printf("ppf: %f, %f\n", d.Ppf(0.25), -0.67499)
fmt.Printf("ppf: %f, %f\n", d.Ppf(0.5), 0.0)
fmt.Printf("ppf: %f, %f\n", d.Ppf(0.75), 0.67449)
fmt.Printf("ppf: %f, %f\n", d.Ppf(0.9), 1.28155)
d = d.Mul(NewGaussian(0, 1))
fmt.Printf("Mul: %#v\n", d)
fmt.Printf("%#v\n%#v", NewGaussian(1, 1).Scale(2), NewGaussian(2, 4))
d = NewGaussian(1, 1).Div(NewGaussian(1, 2))
fmt.Printf("div\n")
fmt.Printf("%#v\n%#v\n", d, NewGaussian(1, 2))
fmt.Printf("%#v\n%#v\n", NewGaussian(1, 1).Scale(1/(1.0/2.0)), NewGaussian(2, 4))
fmt.Printf("ADD:\n%#v\n%#v\n", NewGaussian(1, 1).Add(NewGaussian(1, 2)), NewGaussian(2, 3))
fmt.Printf("SUB:\n%#v\n%#v\n", NewGaussian(1, 1).Sub(NewGaussian(1, 2)), NewGaussian(0, 3))
fmt.Printf("SCALE:\n%#v\n%#v\n", NewGaussian(1, 1).Scale(2), NewGaussian(2, 4))
}

71
math/math.go Normal file
View File

@@ -0,0 +1,71 @@
package math
import (
"math"
)
// Sign 符号函数Sign function简称sgn是一个逻辑函数用以判断实数的正负号。为避免和英文读音相似的正弦函数sine混淆它亦称为Signum function。
func Sign[T int | int8 | int16 | int32 | int64 | float32 | float64](x T) T {
switch {
case x < 0: // x < 0 : -1
return -1
case x > 0: // x > 0 : +1
return +1
default: // x == 0 : 0
return 0
}
}
// Mean 计算给定数据的平均值
func Mean(num []float64) float64 {
var count = len(num)
var sum float64 = 0
for i := 0; i < count; i++ {
sum += num[i]
}
return sum / float64(count)
}
// Variance 使用平均值计算给定数据的方差
func Variance(mean float64, num []float64) float64 {
var count = len(num)
var variance float64 = 0
for i := 0; i < count; i++ {
variance += math.Pow(num[i]-mean, 2)
}
return variance / float64(count)
}
// StandardDeviation 使用方差计算给定数据的标准偏差
func StandardDeviation(num []float64) float64 {
var mean = Mean(num)
var variance = Variance(mean, num)
return math.Sqrt(variance)
}
// SumInt 计算整数数组的和
func SumInt[T int | int32 | int64](array []T) int64 {
var sum int64
for _, v := range array {
sum = sum + int64(v)
}
return sum
}
// SumUint 计算整数数组的和
func SumUint[T uint | uint32 | uint64](array []T) uint64 {
var sum uint64
for _, v := range array {
sum = sum + uint64(v)
}
return sum
}
// SumFloat 计算浮点数数组的和
func SumFloat[T float32 | float64](array []T) float64 {
var sum float64
for _, v := range array {
sum = sum + float64(v)
}
return sum
}

30
math/math_test.go Normal file
View File

@@ -0,0 +1,30 @@
package math
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSign(t *testing.T) {
assert.True(t, Sign(2) == 1)
assert.True(t, Sign(-2) == -1)
assert.True(t, Sign(0) == 0)
assert.True(t, Sign(int64(2)) == 1)
assert.True(t, Sign(int64(-2)) == -1)
assert.True(t, Sign(int64(0)) == 0)
assert.True(t, Sign(float32(2)) == 1)
assert.True(t, Sign(float32(-2)) == -1)
assert.True(t, Sign(float32(0)) == 0)
assert.True(t, Sign(float64(2)) == 1)
assert.True(t, Sign(float64(-2)) == -1)
assert.True(t, Sign(float64(0)) == 0)
}
func TestStandardDeviation(t *testing.T) {
assert.Equal(t, StandardDeviation([]float64{3, 5, 9, 1, 8, 6, 58, 9, 4, 10}), 15.8117045254457)
assert.Equal(t, StandardDeviation([]float64{1, 3, 5, 7, 9, 11, 2, 4, 6, 8}), 3.0397368307141326)
}

View File

@@ -1,47 +0,0 @@
package order_id
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
"github.com/tx7do/kratos-utils/trans"
)
type idCounter uint32
func (c *idCounter) Increase() uint32 {
cur := *c
atomic.AddUint32((*uint32)(c), 1)
atomic.CompareAndSwapUint32((*uint32)(c), 1000, 0)
return uint32(cur)
}
var orderIdIndex idCounter
// GenerateOrderIdWithRandom 生成20位订单号前缀+时间+随机数
func GenerateOrderIdWithRandom(prefix string, split string, tm *time.Time) string {
if tm == nil {
tm = trans.Time(time.Now())
}
index := rand.Intn(1000)
return fmt.Sprintf("%s%s%.4d%s%.2d%s%.2d%s%.2d%s%.2d%s%.2d%s%.4d", prefix, split,
tm.Year(), split, tm.Month(), split, tm.Day(), split,
tm.Hour(), split, tm.Minute(), split, tm.Second(), split, index)
}
// GenerateOrderIdWithIncreaseIndex 生成20位订单号前缀+时间+自增长索引
func GenerateOrderIdWithIncreaseIndex(prefix string, split string, tm *time.Time) string {
if tm == nil {
tm = trans.Time(time.Now())
}
index := orderIdIndex.Increase()
return fmt.Sprintf("%s%s%.4d%s%.2d%s%.2d%s%.2d%s%.2d%s%.2d%s%.4d", prefix, split,
tm.Year(), split, tm.Month(), split, tm.Day(), split,
tm.Hour(), split, tm.Minute(), split, tm.Second(), split, index)
}

View File

@@ -1,63 +0,0 @@
package order_id
import (
"fmt"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tx7do/kratos-utils/trans"
)
func TestGenerateOrderIdWithRandom(t *testing.T) {
fmt.Println(GenerateOrderIdWithRandom("PT", "-", trans.Time(time.Now())))
tm := time.Now()
var ids map[string]bool
ids = make(map[string]bool)
count := 100
for i := 0; i < count; i++ {
ids[GenerateOrderIdWithRandom("PT", "", trans.Time(tm))] = true
}
assert.Equal(t, count, len(ids))
}
func TestGenerateOrderIdWithIndex(t *testing.T) {
tm := time.Now()
fmt.Println(GenerateOrderIdWithIncreaseIndex("PT", "", trans.Time(tm)))
ids := make(map[string]bool)
count := 100
for i := 0; i < count; i++ {
ids[GenerateOrderIdWithIncreaseIndex("PT", "", trans.Time(tm))] = true
}
assert.Equal(t, count, len(ids))
}
func TestGenerateOrderIdWithIndexThread(t *testing.T) {
tm := time.Now()
var wg sync.WaitGroup
var ids sync.Map
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for i := 0; i < 100; i++ {
id := GenerateOrderIdWithIncreaseIndex("PT", "", trans.Time(tm))
ids.Store(id, true)
}
wg.Done()
}()
}
wg.Wait()
aLen := 0
ids.Range(func(k, v interface{}) bool {
aLen++
return true
})
assert.Equal(t, 1000, aLen)
}

View File

@@ -1,5 +1,11 @@
package pagination
const (
DefaultPage = 1 // 默认页数
DefaultPageSize = 10 // 默认每页行数
)
// GetPageOffset 计算偏移量
func GetPageOffset(pageNum, pageSize int32) int {
return int((pageNum - 1) * pageSize)
}

View File

@@ -1,18 +1,52 @@
package rand
import (
"github.com/tx7do/go-utils/math"
"math/rand"
"time"
)
var RANDOM = rand.New(rand.NewSource(time.Now().UnixNano()))
var rnd = rand.New(rand.NewSource(Seed(UnixNanoSeed)))
func init() {
if rnd != nil {
rnd = rand.New(rand.NewSource(Seed(UnixNanoSeed)))
}
}
func Float32() float32 {
return rnd.Float32()
}
func Float64() float64 {
return rnd.Float64()
}
func Intn(n int) int {
return rnd.Intn(n)
}
func Int31n(n int32) int32 {
return rnd.Int31n(n)
}
func Int63n(n int64) int64 {
return rnd.Int63n(n)
}
// RandomInt 根据区间产生随机数
func RandomInt(min, max int) int {
if min >= max {
return max
}
return RANDOM.Intn(max-min) + min
return min + Intn(max-min+1)
}
// RandomInt32 根据区间产生随机数
func RandomInt32(min, max int32) int32 {
if min >= max {
return max
}
return min + Int31n(max-min+1)
}
// RandomInt64 根据区间产生随机数
@@ -20,5 +54,95 @@ func RandomInt64(min, max int64) int64 {
if min >= max {
return max
}
return RANDOM.Int63n(max-min) + min
return min + Int63n(max-min+1)
}
// RandomString 随机字符串,包含大小写字母和数字
func RandomString(l int) string {
if l <= 0 {
return ""
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
bytes := make([]byte, l)
for i := 0; i < l; i++ {
bytes[i] = charset[Intn(len(charset))]
}
return string(bytes)
}
// RandomChoice 随机选择数组中的元素
func RandomChoice[T any](array []T, n int) []T {
if n <= 0 {
return nil
}
if n == 1 {
return []T{array[Intn(len(array))]}
}
tmp := make([]T, len(array))
copy(tmp, array)
if len(tmp) <= n {
return tmp
}
Shuffle(tmp)
return tmp[:n]
}
// Shuffle 随机打乱数组
func Shuffle[T any](array []T) {
if array == nil {
return
}
for i := range array {
j := Intn(i + 1)
array[i], array[j] = array[j], array[i]
}
}
// WeightedChoice 根据权重随机返回对应选项的索引O(n)
func WeightedChoice(weightArray []int) int {
if weightArray == nil {
return -1
}
total := math.SumInt(weightArray)
rv := Int63n(total)
for i, v := range weightArray {
if rv < int64(v) {
return i
}
rv -= int64(v)
}
return len(weightArray) - 1
}
// NonWeightedChoice 根据权重随机返回对应选项的索引O(n). 权重大于等于0
func NonWeightedChoice(weightArray []int) int {
if weightArray == nil {
return -1
}
for i, weight := range weightArray {
if weight < 0 {
weightArray[i] = 0
}
}
total := math.SumInt(weightArray)
rv := Int63n(total)
for i, v := range weightArray {
if rv < int64(v) {
return i
}
rv -= int64(v)
}
return len(weightArray) - 1
}

View File

@@ -7,11 +7,289 @@ import (
"github.com/stretchr/testify/assert"
)
func TestFloat32_GeneratesValueWithinRange(t *testing.T) {
for i := 0; i < 1000; i++ {
value := Float32()
assert.True(t, value >= 0.0)
assert.True(t, value < 1.0)
}
}
func TestFloat32_GeneratesDifferentValues(t *testing.T) {
values := make(map[float32]bool)
for i := 0; i < 1000; i++ {
value := Float32()
values[value] = true
}
assert.Greater(t, len(values), 1)
}
func TestFloat64_GeneratesValueWithinRange(t *testing.T) {
for i := 0; i < 1000; i++ {
value := Float64()
assert.True(t, value >= 0.0)
assert.True(t, value < 1.0)
}
}
func TestFloat64_GeneratesDifferentValues(t *testing.T) {
values := make(map[float64]bool)
for i := 0; i < 1000; i++ {
value := Float64()
values[value] = true
}
assert.Greater(t, len(values), 1)
}
func TestRandomInt(t *testing.T) {
for i := 0; i < 1000; i++ {
n := RandomInt(1, 100)
n := RandomInt(1, 10)
fmt.Println(n)
assert.True(t, n >= 1)
assert.True(t, n < 100)
assert.True(t, n <= 100)
}
}
func TestRandomInt_MinEqualsMax(t *testing.T) {
n := RandomInt(5, 5)
assert.Equal(t, 5, n)
}
func TestRandomInt_MinGreaterThanMax(t *testing.T) {
n := RandomInt(10, 5)
assert.Equal(t, 5, n)
}
func TestRandomInt_NegativeRange(t *testing.T) {
for i := 0; i < 1000; i++ {
n := RandomInt(-10, -1)
assert.True(t, n >= -10)
assert.True(t, n <= -1)
}
}
func TestRandomInt_ZeroRange(t *testing.T) {
for i := 0; i < 1000; i++ {
n := RandomInt(0, 0)
assert.Equal(t, 0, n)
}
}
func TestShuffle_EmptyArray(t *testing.T) {
var array []int
Shuffle(array)
assert.Equal(t, array, array)
}
func TestShuffle_SingleElementArray(t *testing.T) {
array := []int{1}
Shuffle(array)
assert.Equal(t, []int{1}, array)
}
func TestShuffle_MultipleElementsArray(t *testing.T) {
array := []int{1, 2, 3, 4, 5}
original := make([]int, len(array))
copy(original, array)
Shuffle(array)
assert.ElementsMatch(t, original, array)
}
func TestShuffle_ArrayWithDuplicates(t *testing.T) {
array := []int{1, 2, 2, 3, 3, 3}
original := make([]int, len(array))
copy(original, array)
Shuffle(array)
fmt.Println(array)
assert.ElementsMatch(t, original, array)
}
func TestRandomChoice_EmptyArray(t *testing.T) {
result := RandomChoice([]int{}, 3)
assert.Nil(t, result)
}
func TestRandomChoice_NegativeN(t *testing.T) {
array := []int{1, 2, 3}
result := RandomChoice(array, -1)
assert.Nil(t, result)
}
func TestRandomChoice_ZeroN(t *testing.T) {
array := []int{1, 2, 3}
result := RandomChoice(array, 0)
assert.Nil(t, result)
}
func TestRandomChoice_NGreaterThanArrayLength(t *testing.T) {
array := []int{1, 2, 3}
result := RandomChoice(array, 5)
assert.ElementsMatch(t, array, result)
}
func TestRandomChoice_NEqualToArrayLength(t *testing.T) {
array := []int{1, 2, 3}
result := RandomChoice(array, 3)
assert.ElementsMatch(t, array, result)
}
func TestRandomChoice_NLessThanArrayLength(t *testing.T) {
array := []int{1, 2, 3, 4, 5}
result := RandomChoice(array, 3)
assert.Len(t, result, 3)
}
func TestRandomInt32_MinEqualsMax(t *testing.T) {
result := RandomInt32(5, 5)
assert.Equal(t, int32(5), result)
}
func TestRandomInt32_MinGreaterThanMax(t *testing.T) {
result := RandomInt32(10, 5)
assert.Equal(t, int32(5), result)
}
func TestRandomInt32_PositiveRange(t *testing.T) {
for i := 0; i < 1000; i++ {
result := RandomInt32(1, 10)
assert.True(t, result >= 1)
assert.True(t, result <= 10)
}
}
func TestRandomInt32_NegativeRange(t *testing.T) {
for i := 0; i < 1000; i++ {
result := RandomInt32(-10, -1)
assert.True(t, result >= -10)
assert.True(t, result <= -1)
}
}
func TestRandomInt32_ZeroRange(t *testing.T) {
for i := 0; i < 1000; i++ {
result := RandomInt32(0, 0)
assert.Equal(t, int32(0), result)
}
}
func TestRandomInt64_MinEqualsMax(t *testing.T) {
result := RandomInt64(5, 5)
assert.Equal(t, int64(5), result)
}
func TestRandomInt64_MinGreaterThanMax(t *testing.T) {
result := RandomInt64(10, 5)
assert.Equal(t, int64(5), result)
}
func TestRandomInt64_PositiveRange(t *testing.T) {
for i := 0; i < 1000; i++ {
result := RandomInt64(1, 10)
assert.True(t, result >= 1)
assert.True(t, result <= 10)
}
}
func TestRandomInt64_NegativeRange(t *testing.T) {
for i := 0; i < 1000; i++ {
result := RandomInt64(-10, -1)
assert.True(t, result >= -10)
assert.True(t, result <= -1)
}
}
func TestRandomInt64_ZeroRange(t *testing.T) {
for i := 0; i < 1000; i++ {
result := RandomInt64(0, 0)
assert.Equal(t, int64(0), result)
}
}
func TestRandomString_LengthZero(t *testing.T) {
result := RandomString(0)
assert.Equal(t, "", result)
t.Logf("LengthZero: %s", result)
}
func TestRandomString_PositiveLength(t *testing.T) {
result := RandomString(10)
assert.Len(t, result, 10)
t.Logf("PositiveLength: %s", result)
}
func TestRandomString_ContainsOnlyValidCharacters(t *testing.T) {
result := RandomString(100)
for _, char := range result {
assert.True(t, (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9'))
}
t.Logf("ContainsOnlyValidCharacters: %s", result)
}
func TestRandomString_NegativeLength(t *testing.T) {
result := RandomString(-5)
assert.Equal(t, "", result)
}
func TestWeightedChoice_EmptyArray(t *testing.T) {
result := WeightedChoice([]int{})
assert.Equal(t, -1, result)
}
func TestWeightedChoice_AllZeroWeights(t *testing.T) {
result := WeightedChoice([]int{0, 0, 0})
assert.Equal(t, 2, result)
}
func TestWeightedChoice_NegativeWeights(t *testing.T) {
result := WeightedChoice([]int{-1, -2, -3})
assert.Equal(t, 2, result)
}
func TestWeightedChoice_MixedWeights(t *testing.T) {
weightArray := []int{1, 0, 3, 0, 2}
counts := make([]int, len(weightArray))
for i := 0; i < 1000; i++ {
choice := WeightedChoice(weightArray)
counts[choice]++
}
assert.Greater(t, counts[0], 0)
assert.Greater(t, counts[2], 0)
assert.Greater(t, counts[4], 0)
}
func TestWeightedChoice_SingleElement(t *testing.T) {
result := WeightedChoice([]int{5})
assert.Equal(t, 0, result)
}
func TestNonWeightedChoice_EmptyArray(t *testing.T) {
result := NonWeightedChoice([]int{})
assert.Equal(t, -1, result)
}
func TestNonWeightedChoice_AllZeroWeights(t *testing.T) {
result := NonWeightedChoice([]int{0, 0, 0})
assert.Equal(t, 2, result)
}
func TestNonWeightedChoice_NegativeWeights(t *testing.T) {
result := NonWeightedChoice([]int{-1, -2, -3})
assert.Equal(t, 2, result)
}
func TestNonWeightedChoice_MixedWeights(t *testing.T) {
weightArray := []int{1, 0, 3, 0, 2}
counts := make([]int, len(weightArray))
for i := 0; i < 1000; i++ {
choice := NonWeightedChoice(weightArray)
counts[choice]++
}
assert.Greater(t, counts[0], 0)
assert.Greater(t, counts[2], 0)
assert.Greater(t, counts[4], 0)
}
func TestNonWeightedChoice_SingleElement(t *testing.T) {
result := NonWeightedChoice([]int{5})
assert.Equal(t, 0, result)
}

179
rand/randomizer.go Normal file
View File

@@ -0,0 +1,179 @@
package rand
import (
"github.com/tx7do/go-utils/math"
"math/rand"
)
type Randomizer struct {
rnd *rand.Rand
}
func NewRandomizer(seedType SeedType) *Randomizer {
return &Randomizer{
rnd: rand.New(rand.NewSource(Seed(seedType))),
}
}
func (r *Randomizer) Float32() float32 {
return r.rnd.Float32()
}
func (r *Randomizer) Float64() float64 {
return r.rnd.Float64()
}
func (r *Randomizer) Int() int {
return r.rnd.Int()
}
func (r *Randomizer) Int31() int32 {
return r.rnd.Int31()
}
func (r *Randomizer) Int63() int64 {
return r.rnd.Int63()
}
func (r *Randomizer) Uint32() uint32 {
return r.rnd.Uint32()
}
func (r *Randomizer) Uint64() uint64 {
return r.rnd.Uint64()
}
func (r *Randomizer) Intn(n int) int {
return r.rnd.Intn(n)
}
func (r *Randomizer) Int31n(n int32) int32 {
return r.rnd.Int31n(n)
}
func (r *Randomizer) Int63n(n int64) int64 {
return r.rnd.Int63n(n)
}
// RangeInt 根据区间产生随机数
func (r *Randomizer) RangeInt(min, max int) int {
if min >= max {
return max
}
return min + r.Intn(max-min+1)
}
// RangeInt32 根据区间产生随机数
func (r *Randomizer) RangeInt32(min, max int32) int32 {
if min >= max {
return max
}
return min + r.Int31n(max-min+1)
}
// RangeInt64 根据区间产生随机数
func (r *Randomizer) RangeInt64(min, max int64) int64 {
if min >= max {
return max
}
return min + r.Int63n(max-min+1)
}
// RangeUint 根据区间产生随机数
func (r *Randomizer) RangeUint(min, max uint) uint {
if min >= max {
return max
}
return min + uint(r.Intn(int(max-min+1)))
}
// RangeUint32 根据区间产生随机数
func (r *Randomizer) RangeUint32(min, max uint32) uint32 {
if min >= max {
return max
}
return min + uint32(r.Int31n(int32(max-min+1)))
}
// RangeUint64 根据区间产生随机数
func (r *Randomizer) RangeUint64(min, max uint64) uint64 {
if min >= max {
return max
}
return min + uint64(r.Int63n(int64(max-min+1)))
}
// RangeFloat32 根据区间产生随机数
func (r *Randomizer) RangeFloat32(min, max float32) float32 {
if min >= max {
return max
}
return min + r.Float32()*(max-min)
}
// RangeFloat64 根据区间产生随机数
func (r *Randomizer) RangeFloat64(min, max float64) float64 {
if min >= max {
return max
}
return min + r.Float64()*(max-min)
}
// RandomString 随机字符串,包含大小写字母和数字
func (r *Randomizer) RandomString(l int) string {
bytes := make([]byte, l)
for i := 0; i < l; i++ {
x := r.Intn(3)
switch x {
case 0:
bytes[i] = byte(r.RangeInt(65, 90)) //大写字母
case 1:
bytes[i] = byte(r.RangeInt(97, 122))
case 2:
bytes[i] = byte(r.Intn(10))
}
}
return string(bytes)
}
// WeightedChoice 根据权重随机返回对应选项的索引O(n)
func (r *Randomizer) WeightedChoice(weightArray []int) int {
if weightArray == nil {
return -1
}
total := math.SumInt(weightArray)
rv := r.Int63n(total)
for i, v := range weightArray {
if rv < int64(v) {
return i
}
rv -= int64(v)
}
return len(weightArray) - 1
}
// NonWeightedChoice 根据权重随机返回对应选项的索引O(n). 权重大于等于0
func (r *Randomizer) NonWeightedChoice(weightArray []int) int {
if weightArray == nil {
return -1
}
for i, weight := range weightArray {
if weight < 0 {
weightArray[i] = 0
}
}
total := math.SumInt(weightArray)
rv := r.Int63n(total)
for i, v := range weightArray {
if rv < int64(v) {
return i
}
rv -= int64(v)
}
return len(weightArray) - 1
}

86
rand/randomizer_test.go Normal file
View File

@@ -0,0 +1,86 @@
package rand
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestRandomizer_RangeUint32_MinEqualsMax(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
result := r.RangeUint32(5, 5)
assert.Equal(t, uint32(5), result)
}
func TestRandomizer_RangeUint32_MinGreaterThanMax(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
result := r.RangeUint32(10, 5)
assert.Equal(t, uint32(5), result)
}
func TestRandomizer_RangeUint32_PositiveRange(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
for i := 0; i < 1000; i++ {
result := r.RangeUint32(1, 10)
assert.True(t, result >= 1)
assert.True(t, result <= 10)
}
}
func TestRandomizer_RangeUint64_MinEqualsMax(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
result := r.RangeUint64(5, 5)
assert.Equal(t, uint64(5), result)
}
func TestRandomizer_RangeUint64_MinGreaterThanMax(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
result := r.RangeUint64(10, 5)
assert.Equal(t, uint64(5), result)
}
func TestRandomizer_RangeUint64_PositiveRange(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
for i := 0; i < 1000; i++ {
result := r.RangeUint64(1, 10)
assert.True(t, result >= 1)
assert.True(t, result <= 10)
}
}
func TestRandomizer_WeightedChoice_EmptyArray(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
result := r.WeightedChoice([]int{})
assert.Equal(t, -1, result)
}
func TestRandomizer_WeightedChoice_AllZeroWeights(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
result := r.WeightedChoice([]int{0, 0, 0})
assert.Equal(t, 2, result)
}
func TestRandomizer_WeightedChoice_MixedWeights(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
weightArray := []int{1, 0, 3, 0, 2}
counts := make([]int, len(weightArray))
for i := 0; i < 1000; i++ {
choice := r.WeightedChoice(weightArray)
counts[choice]++
}
assert.Greater(t, counts[0], 0)
assert.Greater(t, counts[2], 0)
assert.Greater(t, counts[4], 0)
}
func TestRandomizer_RandomString_LengthZero(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
result := r.RandomString(0)
assert.Equal(t, "", result)
}
func TestRandomizer_RandomString_CorrectLength(t *testing.T) {
r := NewRandomizer(UnixNanoSeed)
length := 50
result := r.RandomString(length)
assert.Equal(t, length, len(result))
}

99
rand/seeder.go Normal file
View File

@@ -0,0 +1,99 @@
package rand
import (
cryptoRand "crypto/rand"
"encoding/binary"
"golang.org/x/exp/rand"
"hash/maphash"
mathRand "math/rand"
"runtime"
"time"
)
type SeedType string
const (
UnixNanoSeed SeedType = "UnixNano"
MapHashSeed SeedType = "MapHash"
CryptoRandSeed SeedType = "CryptoRand"
RandomStringSeed SeedType = "RandomString"
)
type Seeder struct {
seedType SeedType
}
func NewSeeder(seedType SeedType) *Seeder {
return &Seeder{
seedType: seedType,
}
}
func (r *Seeder) UnixNano() int64 {
// 获取当前时间戳
timestamp := time.Now().UnixNano()
// 生成一个随机数
var randomBytes [8]byte
_, err := rand.Read(randomBytes[:])
if err != nil {
panic("failed to generate random bytes")
}
randomPart := int64(binary.LittleEndian.Uint64(randomBytes[:]))
// 获取 Goroutine ID或其他唯一标识
goroutineID := int64(runtime.NumGoroutine())
// 结合时间戳、随机数和 Goroutine ID
seed := timestamp ^ randomPart ^ goroutineID
return seed
}
func (r *Seeder) MapHash() int64 {
return int64(new(maphash.Hash).Sum64())
}
func (r *Seeder) CryptoRand() int64 {
var b [8]byte
_, err := cryptoRand.Read(b[:])
if err != nil {
panic("cannot seed math/rand package with cryptographically secure random number generator")
}
seed := int64(binary.LittleEndian.Uint64(b[:]))
return seed
}
var Alpha = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
func (r *Seeder) RandomString() int64 {
const size = 8
buf := make([]byte, size)
for i := 0; i < size; i++ {
buf[i] = Alpha[mathRand.Intn(len(Alpha))]
}
seed := int64(binary.LittleEndian.Uint64(buf[:]))
return seed
}
func (r *Seeder) Seed() int64 {
switch r.seedType {
default:
fallthrough
case UnixNanoSeed:
return r.UnixNano()
case MapHashSeed:
return r.MapHash()
case CryptoRandSeed:
return r.CryptoRand()
case RandomStringSeed:
return r.RandomString()
}
}
// Seed generates a seed based on the specified SeedType.
func Seed(seedType SeedType) int64 {
randomizer := NewSeeder(seedType)
return randomizer.Seed()
}

80
rand/seeder_test.go Normal file
View File

@@ -0,0 +1,80 @@
package rand
import (
"fmt"
"testing"
)
func TestUnixNanoSeed(t *testing.T) {
seeder := NewSeeder(UnixNanoSeed)
var seeds = make(map[int64]bool)
for i := 0; i < 100000; i++ {
seed := seeder.Seed()
seeds[seed] = true
}
fmt.Println("UnixNano Seed", len(seeds))
}
func TestMapHashSeed(t *testing.T) {
seeder := NewSeeder(MapHashSeed)
var seeds = make(map[int64]bool)
for i := 0; i < 100000; i++ {
seed := seeder.Seed()
seeds[seed] = true
}
fmt.Println("MapHash Seed", len(seeds))
}
func TestCryptoRandSeed(t *testing.T) {
seeder := NewSeeder(CryptoRandSeed)
var seeds = make(map[int64]bool)
for i := 0; i < 100000; i++ {
seed := seeder.Seed()
seeds[seed] = true
}
fmt.Println("CryptoRand Seed", len(seeds))
}
func TestRandomStringSeed(t *testing.T) {
seeder := NewSeeder(RandomStringSeed)
var seeds = make(map[int64]bool)
for i := 0; i < 100000; i++ {
seed := seeder.Seed()
seeds[seed] = true
}
fmt.Println("RandomString Seed", len(seeds))
}
func TestSeed(t *testing.T) {
var seeds = make(map[int64]bool)
for i := 0; i < 100000; i++ {
seed := Seed(UnixNanoSeed)
seeds[seed] = true
}
fmt.Println("UnixNano Seed", len(seeds))
seeds = make(map[int64]bool)
for i := 0; i < 100000; i++ {
seed := Seed(MapHashSeed)
seeds[seed] = true
}
fmt.Println("MapHash Seed", len(seeds))
seeds = make(map[int64]bool)
for i := 0; i < 100000; i++ {
seed := Seed(CryptoRandSeed)
seeds[seed] = true
}
fmt.Println("CryptoRand Seed", len(seeds))
seeds = make(map[int64]bool)
for i := 0; i < 100000; i++ {
seed := Seed(RandomStringSeed)
seeds[seed] = true
}
fmt.Println("RandomString Seed", len(seeds))
}

375
sliceutil/sliceutils.go Normal file
View File

@@ -0,0 +1,375 @@
// This package includes utility functions for handling and manipulating a slice.
// It draws inspiration from JavaScript and Python and uses Go generics as a basis.
package sliceutil
import (
"golang.org/x/exp/constraints"
)
// Filter - given a slice of type T, executes the given predicate function on each element in the slice.
// The predicate is passed the current element, the current index and the slice itself as function arguments.
// If the predicate returns true, the value is included in the result, otherwise it is filtered out.
func Filter[T any](slice []T, predicate func(value T, index int, slice []T) bool) (filtered []T) {
for i, el := range slice {
if ok := predicate(el, i, slice); ok {
filtered = append(filtered, el)
}
}
return filtered
}
// ForEach - given a slice of type T, executes the passed in function for each element in the slice.
// The function is passed the current element, the current index and the slice itself as function arguments.
func ForEach[T any](slice []T, function func(value T, index int, slice []T)) {
for i, el := range slice {
function(el, i, slice)
}
}
// Map - given a slice of type T, executes the passed in mapper function for each element in the slice, returning a slice of type R.
// The function is passed the current element, the current index and the slice itself as function arguments.
func Map[T any, R any](slice []T, mapper func(value T, index int, slice []T) R) (mapped []R) {
if len(slice) > 0 {
mapped = make([]R, len(slice))
for i, el := range slice {
mapped[i] = mapper(el, i, slice)
}
}
return mapped
}
// Reduce - given a slice of type T, executes the passed in reducer function for each element in the slice, returning a result of type R.
// The function is passed the accumulator, current element, current index and the slice itself as function arguments.
// The third argument to reduce is the initial value of type R to use.
func Reduce[T any, R any](slice []T, reducer func(acc R, value T, index int, slice []T) R, initial R) R {
acc := initial
for i, el := range slice {
acc = reducer(acc, el, i, slice)
}
return acc
}
// Find - given a slice of type T, executes the passed in predicate function for each element in the slice.
// If the predicate returns true - a pointer to the element is returned. If no element is found, nil is returned.
// The function is passed the current element, the current index and the slice itself as function arguments.
func Find[T any](slice []T, predicate func(value T, index int, slice []T) bool) *T {
for i, el := range slice {
if ok := predicate(el, i, slice); ok {
return &el
}
}
return nil
}
// FindIndex - given a slice of type T, executes the passed in predicate function for each element in the slice.
// If the predicate returns true - the index of the element is returned. If no element is found, -1 is returned.
// The function is passed the current element, the current index and the slice itself as function arguments.
func FindIndex[T any](slice []T, predicate func(value T, index int, slice []T) bool) int {
for i, el := range slice {
if ok := predicate(el, i, slice); ok {
return i
}
}
return -1
}
// FindIndexOf - given a slice of type T and a value of type T, return ths first index of an element equal to value.
// If no element is found, -1 is returned.
func FindIndexOf[T comparable](slice []T, value T) int {
for i, el := range slice {
if el == value {
return i
}
}
return -1
}
// FindLastIndex - given a slice of type T, executes the passed in predicate function for each element in the slice starting from its end.
// If no element is found, -1 is returned. The function is passed the current element, the current index and the slice itself as function arguments.
func FindLastIndex[T any](slice []T, predicate func(value T, index int, slice []T) bool) int {
for i := len(slice) - 1; i > 0; i-- {
el := slice[i]
if ok := predicate(el, i, slice); ok {
return i
}
}
return -1
}
// FindLastIndexOf - given a slice of type T and a value of type T, returning the last index matching the given value.
// If no element is found, -1 is returned.
func FindLastIndexOf[T comparable](slice []T, value T) int {
for i := len(slice) - 1; i > 0; i-- {
el := slice[i]
if el == value {
return i
}
}
return -1
}
// FindIndexes - given a slice of type T, executes the passed in predicate function for each element in the slice.
// Returns a slice containing all indexes of elements for which the predicate returned true. If no element are found, returns a nil int slice.
// The function is passed the current element, the current index and the slice itself as function arguments.
func FindIndexes[T any](slice []T, predicate func(value T, index int, slice []T) bool) []int {
var indexes []int
for i, el := range slice {
if ok := predicate(el, i, slice); ok {
indexes = append(indexes, i)
}
}
return indexes
}
// FindIndexesOf - given a slice of type T and a value of type T, returns a slice containing all indexes match the given value.
// If no element are found, returns a nil int slice.
func FindIndexesOf[T comparable](slice []T, value T) []int {
var indexes []int
for i, el := range slice {
if el == value {
indexes = append(indexes, i)
}
}
return indexes
}
// Includes - given a slice of type T and a value of type T, determines whether the value is contained by the slice.
// Note: T is constrained to comparable types only and comparison is determined using the equality operator.
func Includes[T comparable](slice []T, value T) bool {
for _, el := range slice {
if el == value {
return true
}
}
return false
}
// Some - given a slice of type T, executes the given predicate for each element of the slice.
// If the predicate returns true for any element, it returns true, otherwise it returns false.
// The function is passed the current element, the current index and the slice itself as function arguments.
func Some[T any](slice []T, predicate func(value T, index int, slice []T) bool) bool {
for i, el := range slice {
if ok := predicate(el, i, slice); ok {
return true
}
}
return false
}
// Every - given a slice of type T, executes the given predicate for each element of the slice.
// If the predicate returns true for all elements, it returns true, otherwise it returns false.
// The function is passed the current element, the current index and the slice itself as function arguments.
func Every[T any](slice []T, predicate func(value T, index int, slice []T) bool) bool {
for i, el := range slice {
if ok := predicate(el, i, slice); !ok {
return false
}
}
return true
}
// Merge - receives slices of type T and merges them into a single slice of type T.
// Note: The elements are merged in their order in a slice,
// i.e. first the elements of the first slice, then that of the second and so forth.
func Merge[T any](slices ...[]T) (mergedSlice []T) {
if len(slices) > 0 {
mergedSliceCap := 0
for _, slice := range slices {
mergedSliceCap += len(slice)
}
if mergedSliceCap > 0 {
mergedSlice = make([]T, 0, mergedSliceCap)
for _, slice := range slices {
mergedSlice = append(mergedSlice, slice...)
}
}
}
return mergedSlice
}
// Sum - receives a slice of type T and returns a value T that is the sum of the numbers.
// Note: T is constrained to be a number type.
func Sum[T constraints.Complex | constraints.Integer | constraints.Float](slice []T) (result T) {
for _, el := range slice {
result += el
}
return result
}
// Remove - receives a slice of type T and an index, removing the element at the given index.
// Note: this function does not modify the input slice.
func Remove[T any](slice []T, i int) []T {
if len(slice) == 0 || i > len(slice)-1 {
return slice
}
copied := Copy(slice)
if i == 0 {
return copied[1:]
}
if i != len(copied)-1 {
return append(copied[:i], copied[i+1:]...)
}
return copied[:i]
}
// Insert - receives a slice of type T, an index and a value.
// The value is inserted at the given index. If there is an existing value at this index, it is shifted to the next index.
// Note: this function does not modify the input slice.
func Insert[T any](slice []T, i int, value T) []T {
if len(slice) == i {
return append(slice, value)
}
slice = append(slice[:i+1], slice[i:]...)
slice[i] = value
return slice
}
// Copy - receives a slice of type T and copies it.
func Copy[T any](slice []T) []T {
duplicate := make([]T, len(slice), cap(slice))
copy(duplicate, slice)
return duplicate
}
// Intersection - takes a variadic number of slices of type T and returns a slice of type T containing any values that exist in all the slices.
// For example, given []int{1, 2, 3}, []int{1, 7, 3}, the intersection would be []int{1, 3}.
func Intersection[T comparable](slices ...[]T) []T {
possibleIntersections := map[T]int{}
for i, slice := range slices {
for _, el := range slice {
if i == 0 {
possibleIntersections[el] = 0
} else if _, elementExists := possibleIntersections[el]; elementExists {
possibleIntersections[el] = i
}
}
}
intersected := make([]T, 0)
for _, el := range slices[0] {
if lastVisitorIndex, exists := possibleIntersections[el]; exists && lastVisitorIndex == len(slices)-1 {
intersected = append(intersected, el)
delete(possibleIntersections, el)
}
}
return intersected
}
// Difference - takes a variadic number of slices of type T and returns a slice of type T containing the elements that are different between the slices.
// For example, given []int{1, 2, 3}, []int{2, 3, 4}, []{3, 4, 5}, the difference would be []int{1, 5}.
func Difference[T comparable](slices ...[]T) []T {
possibleDifferences := map[T]int{}
nonDifferentElements := map[T]int{}
for i, slice := range slices {
for _, el := range slice {
if lastVisitorIndex, elementExists := possibleDifferences[el]; elementExists && lastVisitorIndex != i {
nonDifferentElements[el] = i
} else if !elementExists {
possibleDifferences[el] = i
}
}
}
differentElements := make([]T, 0)
for _, slice := range slices {
for _, el := range slice {
if _, exists := nonDifferentElements[el]; !exists {
differentElements = append(differentElements, el)
}
}
}
return differentElements
}
// Union - takes a variadic number of slices of type T and returns a slice of type T containing the unique elements in the different slices
// For example, given []int{1, 2, 3}, []int{2, 3, 4}, []int{3, 4, 5}, the union would be []int{1, 2, 3, 4, 5}.
func Union[T comparable](slices ...[]T) []T {
return Unique(Merge(slices...))
}
// Reverse - takes a slice of type T and returns a slice of type T with a reverse order of elements.
func Reverse[T any](slice []T) []T {
result := make([]T, len(slice))
itemCount := len(slice)
middle := itemCount / 2
result[middle] = slice[middle]
for i := 0; i < middle; i++ {
mirrorIdx := itemCount - i - 1
result[i], result[mirrorIdx] = slice[mirrorIdx], slice[i]
}
return result
}
// Unique - receives a slice of type T and returns a slice of type T containing all unique elements.
func Unique[T comparable](slice []T) []T {
unique := make([]T, 0)
visited := map[T]bool{}
for _, value := range slice {
if exists := visited[value]; !exists {
unique = append(unique, value)
visited[value] = true
}
}
return unique
}
// Chunk - receives a slice of type T and size N and returns a slice of slices T of size N.
func Chunk[T any](input []T, size int) [][]T {
var chunks [][]T
for i := 0; i < len(input); i += size {
end := i + size
if end > len(input) {
end = len(input)
}
chunks = append(chunks, input[i:end])
}
return chunks
}
// Pluck - receives a slice of type I and a getter func to a field
// and returns a slice containing the requested field's value from each item in the slice.
func Pluck[I any, O any](input []I, getter func(I) *O) []O {
var output []O
for _, item := range input {
field := getter(item)
if field != nil {
output = append(output, *field)
}
}
return output
}
// Flatten - receives a slice of slices of type I and flattens it to a slice of type I.
func Flatten[I any](input [][]I) (output []I) {
if len(input) > 0 {
var outputSize int
for _, item := range input {
outputSize += len(item)
}
if outputSize > 0 {
output = make([]I, 0, outputSize)
for _, item := range input {
output = append(output, item...)
}
}
}
return output
}

View File

@@ -0,0 +1,301 @@
package sliceutil_test
import (
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tx7do/go-utils/sliceutil"
)
type MyInt int
type Pluckable struct {
Code string
Value string
}
var numerals = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var numeralsWithUserDefinedType = []MyInt{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var days = []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
var lastNames = []string{"Jacobs", "Vin", "Jacobs", "Smith"}
func TestFilter(t *testing.T) {
expectedResult := []int{0, 2, 4, 6, 8}
actualResult := sliceutil.Filter(numerals, func(value int, _ int, _ []int) bool {
return value%2 == 0
})
assert.Equal(t, expectedResult, actualResult)
}
func TestForEach(t *testing.T) {
result := 0
sliceutil.ForEach(numerals, func(value int, _ int, _ []int) {
result += value
})
assert.Equal(t, 45, result)
}
func TestMap(t *testing.T) {
expectedResult := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}
actualResult := sliceutil.Map(numerals, func(value int, _ int, _ []int) string {
return strconv.Itoa(value)
})
assert.Equal(t, expectedResult, actualResult)
assert.Nil(t, sliceutil.Map([]int{}, func(_ int, _ int, _ []int) string {
return ""
}))
assert.Nil(t, sliceutil.Map([]int(nil), func(_ int, _ int, _ []int) string {
return ""
}))
}
func TestReduce(t *testing.T) {
expectedResult := map[string]string{"result": "0123456789"}
actualResult := sliceutil.Reduce(
numerals,
func(acc map[string]string, cur int, _ int, _ []int) map[string]string {
acc["result"] += strconv.Itoa(cur)
return acc
},
map[string]string{"result": ""},
)
assert.Equal(t, expectedResult, actualResult)
}
func TestFind(t *testing.T) {
expectedResult := "Wednesday"
actualResult := sliceutil.Find(days, func(value string, index int, slice []string) bool {
return strings.Contains(value, "Wed")
})
assert.Equal(t, expectedResult, *actualResult)
assert.Nil(t, sliceutil.Find(days, func(value string, index int, slice []string) bool {
return strings.Contains(value, "Rishon")
}))
}
func TestFindIndex(t *testing.T) {
expectedResult := 3
actualResult := sliceutil.FindIndex(days, func(value string, index int, slice []string) bool {
return strings.Contains(value, "Wed")
})
assert.Equal(t, expectedResult, actualResult)
assert.Equal(t, -1, sliceutil.FindIndex(days, func(value string, index int, slice []string) bool {
return strings.Contains(value, "Rishon")
}))
}
func TestFindIndexOf(t *testing.T) {
expectedResult := 3
actualResult := sliceutil.FindIndexOf(days, "Wednesday")
assert.Equal(t, expectedResult, actualResult)
assert.Equal(t, -1, sliceutil.FindIndexOf(days, "Rishon"))
}
func TestFindLastIndex(t *testing.T) {
expectedResult := 2
actualResult := sliceutil.FindLastIndex(lastNames, func(value string, index int, slice []string) bool {
return value == "Jacobs"
})
assert.Equal(t, expectedResult, actualResult)
assert.Equal(t, -1, sliceutil.FindLastIndex(lastNames, func(value string, index int, slice []string) bool {
return value == "Hamudi"
}))
}
func TestFindLastIndexOf(t *testing.T) {
expectedResult := 2
actualResult := sliceutil.FindLastIndexOf(lastNames, "Jacobs")
assert.Equal(t, expectedResult, actualResult)
assert.Equal(t, -1, sliceutil.FindLastIndexOf(lastNames, "Hamudi"))
}
func TestFindIndexes(t *testing.T) {
expectedResult := []int{0, 2}
actualResult := sliceutil.FindIndexes(lastNames, func(value string, index int, slice []string) bool {
return value == "Jacobs"
})
assert.Equal(t, expectedResult, actualResult)
assert.Nil(t, sliceutil.FindIndexes(lastNames, func(value string, index int, slice []string) bool {
return value == "Hamudi"
}))
}
func TestFindIndexesOf(t *testing.T) {
expectedResult := []int{0, 2}
actualResult := sliceutil.FindIndexesOf(lastNames, "Jacobs")
assert.Equal(t, expectedResult, actualResult)
assert.Nil(t, sliceutil.FindIndexesOf(lastNames, "Hamudi"))
}
func TestIncludes(t *testing.T) {
assert.True(t, sliceutil.Includes(numerals, 1))
assert.False(t, sliceutil.Includes(numerals, 11))
}
func TestAny(t *testing.T) {
assert.True(t, sliceutil.Some(numerals, func(value int, _ int, _ []int) bool {
return value%5 == 0
}))
assert.False(t, sliceutil.Some(numerals, func(value int, _ int, _ []int) bool {
return value == 11
}))
}
func TestAll(t *testing.T) {
assert.True(t, sliceutil.Every([]int{1, 1, 1}, func(value int, _ int, _ []int) bool {
return value == 1
}))
assert.False(t, sliceutil.Every([]int{1, 1, 1, 2}, func(value int, _ int, _ []int) bool {
return value == 1
}))
}
func TestMerge(t *testing.T) {
result := sliceutil.Merge(numerals[:5], numerals[5:])
assert.Equal(t, numerals, result)
assert.Nil(t, sliceutil.Merge([]int(nil), []int(nil)))
assert.Nil(t, sliceutil.Merge([]int{}, []int{}))
}
func TestSum(t *testing.T) {
result := sliceutil.Sum(numerals)
assert.Equal(t, 45, result)
}
func TestSum2(t *testing.T) {
result := sliceutil.Sum(numeralsWithUserDefinedType)
assert.Equal(t, MyInt(45), result)
}
func TestRemove(t *testing.T) {
testSlice := []int{1, 2, 3}
result := sliceutil.Remove(testSlice, 1)
assert.Equal(t, []int{1, 3}, result)
assert.Equal(t, []int{1, 2, 3}, testSlice)
result = sliceutil.Remove(result, 1)
assert.Equal(t, []int{1}, result)
result = sliceutil.Remove(result, 3)
assert.Equal(t, []int{1}, result)
result = sliceutil.Remove(result, 0)
assert.Equal(t, []int{}, result)
result = sliceutil.Remove(result, 1)
assert.Equal(t, []int{}, result)
}
func TestCopy(t *testing.T) {
testSlice := []int{1, 2, 3}
copiedSlice := sliceutil.Copy(testSlice)
copiedSlice[0] = 2
assert.NotEqual(t, testSlice, copiedSlice)
}
func TestInsert(t *testing.T) {
testSlice := []int{1, 2}
result := sliceutil.Insert(testSlice, 0, 3)
assert.Equal(t, []int{3, 1, 2}, result)
assert.NotEqual(t, testSlice, result)
assert.Equal(t, []int{1, 3, 2}, sliceutil.Insert(testSlice, 1, 3))
assert.Equal(t, []int{1, 2, 3}, sliceutil.Insert(testSlice, 2, 3))
}
func TestIntersection(t *testing.T) {
expectedResult := []int{3, 4, 5}
first := []int{1, 2, 3, 4, 5}
second := []int{2, 3, 4, 5, 6}
third := []int{3, 4, 5, 6, 7}
assert.Equal(t, expectedResult, sliceutil.Intersection(first, second, third))
}
func TestDifference(t *testing.T) {
expectedResult := []int{1, 7}
first := []int{1, 2, 3, 4, 5}
second := []int{2, 3, 4, 5, 6}
third := []int{3, 4, 5, 6, 7}
assert.Equal(t, expectedResult, sliceutil.Difference(first, second, third))
}
func TestUnion(t *testing.T) {
expectedResult := []int{1, 2, 3, 4, 5, 6, 7}
first := []int{1, 2, 3, 4, 5}
second := []int{2, 3, 4, 5, 6}
third := []int{3, 4, 5, 6, 7}
assert.Equal(t, expectedResult, sliceutil.Union(first, second, third))
}
func TestReverse(t *testing.T) {
expectedResult := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
assert.Equal(t, expectedResult, sliceutil.Reverse(numerals))
// ensure does not modify the original
assert.Equal(t, expectedResult, sliceutil.Reverse(numerals))
// test basic odd length case
expectedResult = []int{9, 8, 7, 6, 5, 4, 3, 2, 1}
assert.Equal(t, expectedResult, sliceutil.Reverse(numerals[1:]))
}
func TestUnique(t *testing.T) {
duplicates := []int{6, 6, 6, 9, 0, 0, 0}
expectedResult := []int{6, 9, 0}
assert.Equal(t, expectedResult, sliceutil.Unique(duplicates))
// Ensure original is unaltered
assert.NotEqual(t, expectedResult, duplicates)
}
func TestChunk(t *testing.T) {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
assert.Equal(t, [][]int{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}}, sliceutil.Chunk(numbers, 2))
assert.Equal(t, [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10}}, sliceutil.Chunk(numbers, 3))
}
func TestPluck(t *testing.T) {
items := []Pluckable{
{
Code: "azer",
Value: "Azer",
},
{
Code: "tyuio",
Value: "Tyuio",
},
}
assert.Equal(t, []string{"azer", "tyuio"}, sliceutil.Pluck(items, func(item Pluckable) *string {
return &item.Code
}))
assert.Equal(t, []string{"Azer", "Tyuio"}, sliceutil.Pluck(items, func(item Pluckable) *string {
return &item.Value
}))
}
func TestFlatten(t *testing.T) {
items := [][]int{
{1, 2, 3, 4},
{5, 6},
{7, 8},
{9, 10, 11},
}
flattened := sliceutil.Flatten(items)
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, flattened)
assert.Nil(t, sliceutil.Flatten([][]int{}))
assert.Nil(t, sliceutil.Flatten([][]int(nil)))
assert.Nil(t, sliceutil.Flatten([][]int{{}, {}}))
assert.Nil(t, sliceutil.Flatten([][]int{nil, nil}))
assert.Nil(t, sliceutil.Flatten([][]int{{}, nil}))
}

22
slug/go.mod Normal file
View File

@@ -0,0 +1,22 @@
module github.com/tx7do/go-utils/slug
go 1.23.0
toolchain go1.23.2
require (
github.com/gosimple/slug v1.15.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

27
slug/go.sum Normal file
View File

@@ -0,0 +1,27 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

29
slug/slug.go Normal file
View File

@@ -0,0 +1,29 @@
package slug
import (
"github.com/gosimple/slug"
)
// Generate 生成短链接
func Generate(input string) string {
slug.Lowercase = true
return slug.MakeLang(input, "en")
}
// GenerateCaseSensitive 生成大小写敏感的短链接
func GenerateCaseSensitive(input string) string {
slug.Lowercase = false
return slug.MakeLang(input, "en")
}
// GenerateEnglish 生成英文短链接
func GenerateEnglish(input string) string {
slug.Lowercase = true
return slug.MakeLang(input, "en")
}
// GenerateGerman 生成德文短链接
func GenerateGerman(input string) string {
slug.Lowercase = true
return slug.MakeLang(input, "de")
}

88
slug/slug_test.go Normal file
View File

@@ -0,0 +1,88 @@
package slug
import (
"fmt"
"testing"
"github.com/gosimple/slug"
"github.com/stretchr/testify/assert"
)
func TestGoSimple(t *testing.T) {
// 俄文
text := slug.Make("Hellö Wörld хелло ворлд")
assert.Equal(t, text, "hello-world-khello-vorld")
fmt.Println(text)
// 繁体中文
someText := slug.Make("影師")
assert.Equal(t, someText, "ying-shi")
fmt.Println(someText)
// 简体中文
cnText := slug.Make("天下太平")
assert.Equal(t, cnText, "tian-xia-tai-ping")
fmt.Println(cnText)
// 英文
enText := slug.MakeLang("This & that", "en")
assert.Equal(t, enText, "this-and-that")
fmt.Println(enText)
// 德文
deText := slug.MakeLang("Diese & Dass", "de")
assert.Equal(t, deText, "diese-und-dass")
fmt.Println(deText)
// 保持大小写
slug.Lowercase = false
deUppercaseText := slug.MakeLang("Diese & Dass", "de")
assert.Equal(t, deUppercaseText, "Diese-und-Dass")
fmt.Println(deUppercaseText)
// 字符替换
slug.CustomSub = map[string]string{
"water": "sand",
}
textSub := slug.Make("water is hot")
assert.Equal(t, textSub, "sand-is-hot")
fmt.Println(textSub)
}
func TestGenerate(t *testing.T) {
// 俄文
text := Generate("Hellö Wörld хелло ворлд")
assert.Equal(t, text, "hello-world-khello-vorld")
fmt.Println(text)
// 繁体中文
someText := Generate("影師")
assert.Equal(t, someText, "ying-shi")
fmt.Println(someText)
// 简体中文
cnText := Generate("天下太平")
assert.Equal(t, cnText, "tian-xia-tai-ping")
fmt.Println(cnText)
// 英文
enText := GenerateEnglish("This & that")
assert.Equal(t, enText, "this-and-that")
fmt.Println(enText)
enText = GenerateCaseSensitive("This & That")
assert.Equal(t, enText, "This-and-That")
fmt.Println(enText)
// 德文
deText := GenerateGerman("Diese & Dass")
assert.Equal(t, deText, "diese-und-dass")
fmt.Println(deText)
slug.CustomSub = map[string]string{
"water": "sand",
}
textSub := Generate("water is hot")
assert.Equal(t, textSub, "sand-is-hot")
fmt.Println(textSub)
}

View File

@@ -1,50 +0,0 @@
package sonyflake
import (
"github.com/sony/sonyflake"
"time"
)
var (
sf *sonyflake.Sonyflake
)
func startTime() time.Time {
return time.Now()
}
func getMachineID() (uint16, error) {
return 0, nil
}
func checkMachineID(uint16) bool {
return false
}
// InitSonyflake 初始化Sonyflake节点单体
func InitSonyflake() {
settings := sonyflake.Settings{
/*StartTime: startTime(),
MachineID: getMachineID,
CheckMachineID: checkMachineID,*/
}
sf = sonyflake.NewSonyflake(settings)
if sf == nil {
panic("sonyflake not created")
}
}
// GenerateSonyflake 生成 Sonyflake ID
func GenerateSonyflake() uint64 {
if sf == nil {
InitSonyflake()
}
if sf == nil {
return 0
}
id, err := sf.NextID()
if err != nil {
return 0
}
return id
}

View File

@@ -1,35 +0,0 @@
package sonyflake
import (
"fmt"
"testing"
"time"
)
func init() {
InitSonyflake()
}
func TestGenerateSonyflake(t *testing.T) {
for i := 0; i < 100; i++ {
id := GenerateSonyflake()
fmt.Println(id)
}
}
func TestGenerateTime(t *testing.T) {
// 秒
fmt.Printf("时间戳(秒):%v;\n", time.Now().Unix())
fmt.Printf("时间戳(纳秒转换为秒):%v;\n", time.Now().UnixNano()/1e9)
// 毫秒
fmt.Printf("时间戳(毫秒):%v;\n", time.Now().UnixMilli())
fmt.Printf("时间戳(纳秒转换为毫秒):%v;\n", time.Now().UnixNano()/1e6)
// 微秒
fmt.Printf("时间戳(微秒):%v;\n", time.Now().UnixMicro())
fmt.Printf("时间戳(纳秒转换为微秒):%v;\n", time.Now().UnixNano()/1e3)
// 纳秒
fmt.Printf("时间戳(纳秒):%v;\n", time.Now().UnixNano())
}

20
stringutil/replace.go Normal file
View File

@@ -0,0 +1,20 @@
package stringutil
import (
"fmt"
"regexp"
)
// ReplaceJSONField 使用正则表达式替换 JSON 字符串中指定字段的值
// fieldNames: 要替换的多个字段名,使用竖线(|)分隔(例如:"tenantId|tenant_id"
// newValue: 新的值(字符串形式,将被包装在引号中)
// jsonStr: 原始 JSON 字符串
func ReplaceJSONField(fieldNames, newValue, jsonStr string) string {
// 构建正则表达式模式
// 匹配模式: ("field1"|"field2"|...): "任意值"
pattern := fmt.Sprintf(`(?i)("(%s)")\s*:\s*"([^"]*)"`, fieldNames)
re := regexp.MustCompile(pattern)
// 替换匹配到的内容
return re.ReplaceAllString(jsonStr, fmt.Sprintf(`${1}: "%s"`, newValue))
}

View File

@@ -0,0 +1,38 @@
package stringutil
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestReplaceJSONField(t *testing.T) {
// 测试替换单个字段
jsonStr := `{"tenantId": "123", "name": "test"}`
result := ReplaceJSONField("tenantId", "456", jsonStr)
expected := `{"tenantId": "456", "name": "test"}`
assert.Equal(t, expected, result)
// 测试替换多个字段
jsonStr = `{"tenantId": "123", "tenant_id": "789", "name": "test"}`
result = ReplaceJSONField("tenantId|tenant_id", "456", jsonStr)
expected = `{"tenantId": "456", "tenant_id": "456", "name": "test"}`
assert.Equal(t, expected, result)
// 测试字段不存在
jsonStr = `{"name": "test"}`
result = ReplaceJSONField("tenantId", "456", jsonStr)
expected = `{"name": "test"}`
assert.Equal(t, expected, result)
// 测试空 JSON 字符串
jsonStr = ``
result = ReplaceJSONField("tenantId", "456", jsonStr)
expected = ``
assert.Equal(t, expected, result)
// 测试空字段名
jsonStr = `{"tenantId": "123"}`
result = ReplaceJSONField("", "456", jsonStr)
expected = `{"tenantId": "123"}`
assert.Equal(t, expected, result)
}

37
structutil/structutils.go Normal file
View File

@@ -0,0 +1,37 @@
package structutil
import "reflect"
// ForEach given a struct, calls the passed in function with each visible struct field's name, value and tag.
func ForEach[T any](structInstance T, function func(key string, value any, tag reflect.StructTag)) {
typeOf := reflect.TypeOf(structInstance)
valueOf := reflect.ValueOf(structInstance)
fields := reflect.VisibleFields(typeOf)
for _, field := range fields {
value := valueOf.FieldByName(field.Name)
function(field.Name, value.Interface(), field.Tag)
}
}
// ToMap given a struct, converts it to a map[string]any.
// This function also takes struct tag names as optional parameters - if passed in, the struct tags will be used to remap or omit values.
func ToMap[T any](structInstance T, structTags ...string) map[string]any {
output := make(map[string]any, 0)
ForEach(structInstance, func(key string, value any, tag reflect.StructTag) {
if tag != "" {
for _, s := range structTags {
if tagValue, isPresent := tag.Lookup(s); isPresent && tagValue != "" {
key = tagValue
break
}
}
}
if key != "-" {
output[key] = value
}
})
return output
}

View File

@@ -0,0 +1,55 @@
package structutil_test
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tx7do/go-utils/structutil"
)
type TestStruct struct {
First string
Second int
Third bool `struct:"third"`
}
func TestForEach(t *testing.T) {
instance := TestStruct{
"moishe",
22,
true,
}
structutil.ForEach(instance, func(key string, value any, tag reflect.StructTag) {
switch key {
case "First":
assert.Equal(t, "moishe", value.(string))
assert.Zero(t, tag)
case "Second":
assert.Equal(t, 22, value.(int))
assert.Zero(t, tag)
case "Third":
assert.Equal(t, true, value.(bool))
assert.Equal(t, "third", tag.Get("struct"))
}
})
}
func TestToMap(t *testing.T) {
instance := TestStruct{
"moishe",
22,
true,
}
assert.Equal(t, map[string]any{
"First": "moishe",
"Second": 22,
"Third": true,
}, structutil.ToMap(instance))
assert.Equal(t, map[string]any{
"First": "moishe",
"Second": 22,
"third": true,
}, structutil.ToMap(instance, "struct"))
}

14
tag.bat Normal file
View File

@@ -0,0 +1,14 @@
git tag v1.1.28
git tag bank_card/v1.1.5
git tag geoip/v1.1.5
git tag translator/v1.1.2
git tag copierutil/v0.0.5
git tag jwtutil/v0.0.2
git tag id/v0.0.1
git tag slug/v0.0.1
git tag entgo/v1.1.30
git tag gorm/v1.1.6
git push origin --tags

View File

@@ -1,9 +0,0 @@
package util
const (
DateLayout = "2006-01-02"
ClockLayout = "15:04:05"
TimeLayout = DateLayout + " " + ClockLayout
DefaultTimeLocationName = "Asia/Shanghai"
)

View File

@@ -1,116 +0,0 @@
package util
import (
"time"
"github.com/tx7do/kratos-utils/trans"
)
var DefaultTimeLocation *time.Location
func RefreshDefaultTimeLocation(name string) {
DefaultTimeLocation, _ = time.LoadLocation(name)
}
// UnixMilliToStringPtr 毫秒时间戳 -> 字符串
func UnixMilliToStringPtr(tm *int64) *string {
if tm == nil {
return nil
}
str := time.UnixMilli(*tm).Format(TimeLayout)
return &str
}
// StringToUnixMilliInt64Ptr 字符串 -> 毫秒时间戳
func StringToUnixMilliInt64Ptr(tm *string) *int64 {
theTime := StringTimeToTime(tm)
if theTime == nil {
return nil
}
unixTime := theTime.UnixMilli()
return &unixTime
}
// StringTimeToTime 时间字符串 -> 时间
func StringTimeToTime(str *string) *time.Time {
if str == nil {
return nil
}
if len(*str) == 0 {
return nil
}
if DefaultTimeLocation == nil {
RefreshDefaultTimeLocation(DefaultTimeLocationName)
}
var err error
var theTime time.Time
theTime, err = time.ParseInLocation(TimeLayout, *str, DefaultTimeLocation)
if err == nil {
return &theTime
}
theTime, err = time.ParseInLocation(DateLayout, *str, DefaultTimeLocation)
if err == nil {
return &theTime
}
theTime, err = time.ParseInLocation(ClockLayout, *str, DefaultTimeLocation)
if err == nil {
return &theTime
}
return nil
}
// TimeToTimeString 时间 -> 时间字符串
func TimeToTimeString(tm *time.Time) *string {
if tm == nil {
return nil
}
return trans.String(tm.Format(TimeLayout))
}
// StringDateToTime 字符串 -> 时间
func StringDateToTime(str *string) *time.Time {
if str == nil {
return nil
}
if len(*str) == 0 {
return nil
}
if DefaultTimeLocation == nil {
RefreshDefaultTimeLocation(DefaultTimeLocationName)
}
var err error
var theTime time.Time
theTime, err = time.ParseInLocation(TimeLayout, *str, DefaultTimeLocation)
if err == nil {
return &theTime
}
theTime, err = time.ParseInLocation(DateLayout, *str, DefaultTimeLocation)
if err == nil {
return &theTime
}
theTime, err = time.ParseInLocation(ClockLayout, *str, DefaultTimeLocation)
if err == nil {
return &theTime
}
return nil
}
// TimeToDateString 时间 -> 日期字符串
func TimeToDateString(tm *time.Time) *string {
if tm == nil {
return nil
}
return trans.String(tm.Format(DateLayout))
}

View File

@@ -1,35 +0,0 @@
package util
import (
"fmt"
"testing"
"time"
"github.com/tx7do/kratos-utils/trans"
)
func TestUnixMilliToStringPtr(t *testing.T) {
now := time.Now().UnixMilli()
str := UnixMilliToStringPtr(&now)
fmt.Println(now)
fmt.Println(*str)
fmt.Println(*UnixMilliToStringPtr(trans.Int64(1677135885288)))
fmt.Println(*UnixMilliToStringPtr(trans.Int64(1677647430853)))
fmt.Println(*UnixMilliToStringPtr(trans.Int64(1677647946234)))
fmt.Println(*UnixMilliToStringPtr(trans.Int64(1678245350773)))
fmt.Println("START: ", *StringToUnixMilliInt64Ptr(trans.String("2023-03-09 00:00:00")))
fmt.Println("END: ", *StringToUnixMilliInt64Ptr(trans.String("2023-03-09 23:59:59")))
fmt.Println(StringTimeToTime(trans.String("2023-03-01 00:00:00")))
fmt.Println(*StringDateToTime(trans.String("2023-03-01")))
fmt.Println(StringTimeToTime(trans.String("2023-03-08 00:00:00")).UnixMilli())
fmt.Println(StringDateToTime(trans.String("2023-03-07")).UnixMilli())
}
func TestTimeToDateString(t *testing.T) {
fmt.Println(*TimeToTimeString(trans.Time(time.Now())))
fmt.Println(*TimeToDateString(trans.Time(time.Now())))
}

61
timeutil/consts.go Normal file
View File

@@ -0,0 +1,61 @@
package timeutil
import "time"
const (
DateLayout = "2006-01-02"
ClockLayout = "15:04:05"
TimeLayout = DateLayout + " " + ClockLayout
DefaultTimeLocationName = "Asia/Shanghai"
)
// More predefined layouts for use in Time.Format and time.Parse.
const (
DT14 = "20060102150405"
DT8 = "20060102"
DT8MDY = "01022006"
DT6 = "200601"
MonthDay = "1/2"
DIN5008FullDate = "02.01.2006" // German DIN 5008 standard
DIN5008Date = "02.01.06"
RFC3339FullDate = time.DateOnly
RFC3339Milli = "2006-01-02T15:04:05.999Z07:00"
RFC3339Dash = "2006-01-02T15-04-05Z07-00"
ISO8601 = "2006-01-02T15:04:05Z0700"
ISO8601TZHour = "2006-01-02T15:04:05Z07"
ISO8601NoTZ = "2006-01-02T15:04:05"
ISO8601MilliNoTZ = "2006-01-02T15:04:05.999"
ISO8601Milli = "2006-01-02T15:04:05.999Z0700"
ISO8601CompactZ = "20060102T150405Z0700"
ISO8601CompactNoTZ = "20060102T150405"
ISO8601YM = "2006-01"
ISO9075 = time.DateTime // ISO/IEC 9075 used by MySQL, BigQuery, etc.
ISO9075MicroTZ = "2006-01-02 15:04:05.999999-07" // ISO/IEC 9075 used by PostgreSQL
RFC5322 = "Mon, 2 Jan 2006 15:04:05 -0700" // RFC5322 = "Mon Jan 02 15:04:05 -0700 2006"
SQLTimestamp = ISO9075
SQLTimestampMinutes = "2006-01-02 15:04"
Ruby = "2006-01-02 15:04:05 -0700" // Ruby Time.now.to_s
InsightlyAPIQuery = "_1/_2/2006 _3:04:05 PM"
DateMDY = "1/2/2006" // an underscore results in a space.
DateMDYSlash = "01/02/2006"
DateDMYDash = "_2-01-2006" // Jira XML Date format
DateDMYHM2 = "02:01:06 15:04" // GMT time in format dd:mm:yy hh:mm
DateYMD = RFC3339FullDate
DateTextUS = "January 2, 2006"
DateTextUSAbbr3 = "Jan 2, 2006"
DateTextEU = "2 January 2006"
DateTextEUAbbr3 = "2 Jan 2006"
MonthAbbrYear = "Jan 2006"
MonthYear = "January 2006"
)
const (
RFC3339Min = "0000-01-01T00:00:00Z"
RFC3339Max = "9999-12-31T23:59:59Z"
RFC3339Zero = "0001-01-01T00:00:00Z" // Golang zero value
RFC3339ZeroUnix = "1970-01-01T00:00:00Z"
RFC3339YMDZeroUnix = int64(-62135596800)
)
var ReferenceTimeValue time.Time = time.Date(2006, 1, 2, 15, 4, 5, 999999999, time.FixedZone("MST", -7*60*60))

View File

@@ -1,4 +1,4 @@
package util
package timeutil
import (
"math"

Some files were not shown because too many files have changed in this diff Show More