Compare commits

...

20 Commits

Author SHA1 Message Date
6a8c7782a5 Merge remote-tracking branch 'origin/main' into Branch_v1.1.29
# Conflicts:
#	query_parser/go.sum
2025-08-14 23:53:38 +08:00
e40c75d4ad 添加 2025-08-14 23:44:49 +08:00
90a722b99f 修复无法格式化输出bool的问题 2025-08-14 20:26:33 +08:00
Bobo
f7d3dc108f feat: Timestamp Conversions. 2025-07-07 10:33:58 +08:00
Bobo
1452f14cb8 feat: query_parser support new one filter parse pattern. 2025-06-24 11:31:07 +08:00
Bobo
e83795fc95 feat: query_parser support new one filter parse pattern. 2025-06-24 11:11:34 +08:00
Bobo
bed43b3576 feat: query_parser support new one filter parse pattern. 2025-06-24 10:48:26 +08:00
Bobo
b9665d4f9b feat: entgo. 2025-06-24 00:25:03 +08:00
Bobo
1d53d3af4c feat: query_parser. 2025-06-23 23:46:37 +08:00
Bobo
a502539417 feat: query_parser + stringcase. 2025-06-23 23:44:49 +08:00
Bobo
0cd2eb95f2 feat: query_parser + stringcase. 2025-06-23 23:44:34 +08:00
Bobo
2e38fe77d7 feat: query_parser + stringcase. 2025-06-23 23:44:00 +08:00
Bobo
55c80024c2 feat: query_parser + stringcase. 2025-06-23 23:26:59 +08:00
Bobo
de36ab695d feat: refactor type mapper. 2025-06-16 14:49:21 +08:00
Bobo
a1b8326783 feat: fix mapper bug. 2025-06-16 14:17:19 +08:00
Bobo
38f3cb8a3b feat: fix mapper bug. 2025-06-16 11:59:09 +08:00
Bobo
be5aa063df feat: password. 2025-06-14 23:28:15 +08:00
Bobo
3ca4745bac feat: refactor. 2025-06-14 21:44:33 +08:00
Bobo
83c2ec5048 feat: mapper. 2025-06-12 08:59:14 +08:00
Bobo
f48c7373d7 feat: mapper. 2025-06-11 23:03:10 +08:00
55 changed files with 3460 additions and 182 deletions

View File

@@ -1,26 +1,26 @@
module github.com/tx7do/go-utils/entgo
go 1.23.0
go 1.24
toolchain go1.23.2
toolchain go1.24.4
replace github.com/tx7do/go-utils/id => ../id
require (
entgo.io/contrib v0.6.0
entgo.io/ent v0.14.4
github.com/XSAM/otelsql v0.38.0
github.com/XSAM/otelsql v0.39.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.28
github.com/tx7do/go-utils v1.1.29
github.com/tx7do/go-utils/id v0.0.2
go.opentelemetry.io/otel v1.36.0
google.golang.org/protobuf v1.36.6
)
require (
ariga.io/atlas v0.34.0 // indirect
ariga.io/atlas v0.35.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
@@ -43,15 +43,15 @@ require (
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.mongodb.org/mongo-driver v1.17.3 // indirect
go.mongodb.org/mongo-driver v1.17.4 // 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.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
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,13 +1,13 @@
ariga.io/atlas v0.34.0 h1:4hdy+2x+xNs6Lx2anuJ/4Q7lCaqddbEj5CtRDVOBu0M=
ariga.io/atlas v0.34.0/go.mod h1:WJesu2UCpGQvgUh3oVP94EiRT61nNy1W/VN5g+vqP1I=
ariga.io/atlas v0.35.0 h1:tzco6CEZm1/jGD2ifHhKFlsQB7Bfsc/mty4zwm6Mlbc=
ariga.io/atlas v0.35.0/go.mod h1:9ZAIr/V85596AVxmN8edyVHYKKpnNsDMdnHLsEliW7k=
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/XSAM/otelsql v0.39.0 h1:4o374mEIMweaeevL7fd8Q3C710Xi2Jh/c8G4Qy9bvCY=
github.com/XSAM/otelsql v0.39.0/go.mod h1:uMOXLUX+wkuAuP0AR3B45NXX7E9lJS2mERa8gqdU8R0=
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=
@@ -71,34 +71,34 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6
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.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
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/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
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.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/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.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=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
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=

View File

@@ -57,10 +57,14 @@ func ExtractJsonFieldKeyValues(msg proto.Message, paths []string, needToSnakeCas
v := rft.Get(fd)
switch v.Interface().(type) {
case int32, int64, uint32, uint64, float32, float64, bool:
case bool:
keyValues = append(keyValues, fmt.Sprintf("%t", v.Interface()))
case int32, int64, uint32, uint64, float32, float64:
keyValues = append(keyValues, fmt.Sprintf("%d", v.Interface()))
case string:
keyValues = append(keyValues, fmt.Sprintf("'%s'", v.Interface()))
default:
keyValues = append(keyValues, fmt.Sprintf("%v", v.Interface()))
}
}

4
go.mod
View File

@@ -8,8 +8,8 @@ require (
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
golang.org/x/crypto v0.39.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
google.golang.org/protobuf v1.36.6
)

8
go.sum
View File

@@ -22,10 +22,10 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
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/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=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
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=

View File

@@ -94,6 +94,7 @@ func TestNewXID(t *testing.T) {
// 测试生成的 XID 是否非空
id := NewXID()
assert.NotEmpty(t, id, "生成的 XID 应该非空")
t.Logf("xid: %s", id)
// 测试生成的 XID 的长度是否符合预期
assert.Equal(t, 20, len(id), "生成的 XID 长度应该为 20")

95
mapper/README.md Normal file
View File

@@ -0,0 +1,95 @@
# 类型映射器
类型映射器的作用是将一个数据结构的字段映射到另一个数据结构的字段,通常用于对象之间的数据转换。它可以简化不同类型或结构之间的数据传递,减少手动赋值的代码量,提高开发效率。例如,在处理数据库实体与业务模型或 API 请求/响应模型之间的转换时,类型映射器非常有用。
## 基于Copier的类型映射器
```go
package main
import (
"github.com/tx7do/go-utils/mapper"
)
func main() {
type DtoType struct {
Name string
Age int
}
type EntityType struct {
Name string
Age int
}
mapper := mapper.NewCopierMapper[DtoType, EntityType]()
// 测试 ToEntity 方法
dto := &DtoType{Name: "Alice", Age: 25}
entity := mapper.ToEntity(dto)
// 测试 ToEntity 方法,传入 nil
entityNil := mapper.ToEntity(nil)
// 测试 ToDTO 方法
entity = &EntityType{Name: "Bob", Age: 30}
dtoResult := mapper.ToDTO(entity)
// 测试 ToDTO 方法,传入 nil
dtoNil := mapper.ToDTO(nil)
}
```
## ent 与 protobuf 的枚举类型映射器
```go
package main
import "github.com/tx7do/go-utils/mapper"
func main() {
type DtoType int32
type EntityType string
const (
DtoTypeOne DtoType = 1
DtoTypeTwo DtoType = 2
)
const (
EntityTypeOne EntityType = "One"
EntityTypeTwo EntityType = "Two"
)
nameMap := map[int32]string{
1: "One",
2: "Two",
}
valueMap := map[string]int32{
"One": 1,
"Two": 2,
}
converter := mapper.NewEnumTypeConverter[DtoType, EntityType](nameMap, valueMap)
// 测试 ToEntity 方法
dto := DtoTypeOne
entity := converter.ToEntity(&dto)
// 测试 ToEntity 方法,传入不存在的值
dtoInvalid := DtoType(3)
entityInvalid := converter.ToEntity(&dtoInvalid)
// 测试 ToDTO 方法
tmpEntityTwo := EntityTypeTwo
entity = &tmpEntityTwo
dtoResult := converter.ToDTO(entity)
// 测试 ToDTO 方法,传入不存在的值
tmpEntityThree := EntityType("Three")
entityInvalid = &tmpEntityThree
dtoInvalidResult := converter.ToDTO(entityInvalid)
}
```

82
mapper/enum_converter.go Normal file
View File

@@ -0,0 +1,82 @@
package mapper
import (
"github.com/jinzhu/copier"
)
type EnumTypeConverter[DTO ~int32, ENTITY ~string] struct {
nameMap map[int32]string
valueMap map[string]int32
}
func NewEnumTypeConverter[DTO ~int32, ENTITY ~string](
nameMap map[int32]string,
valueMap map[string]int32,
) *EnumTypeConverter[DTO, ENTITY] {
return &EnumTypeConverter[DTO, ENTITY]{
valueMap: valueMap,
nameMap: nameMap,
}
}
func (m *EnumTypeConverter[DTO, ENTITY]) ToEntity(dto *DTO) *ENTITY {
if dto == nil {
return nil
}
find, ok := m.nameMap[int32(*dto)]
if !ok {
return nil
}
entity := ENTITY(find)
return &entity
}
func (m *EnumTypeConverter[DTO, ENTITY]) ToDTO(entity *ENTITY) *DTO {
if entity == nil {
return nil
}
find, ok := m.valueMap[string(*entity)]
if !ok {
return nil
}
dto := DTO(find)
return &dto
}
func (m *EnumTypeConverter[DTO, ENTITY]) NewConverterPair() []copier.TypeConverter {
srcType := ENTITY("")
dstType := DTO(0)
fromFn := m.ToDTO
toFn := m.ToEntity
return NewGenericTypeConverterPair(&srcType, &dstType, fromFn, 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
},
},
}
}

20
mapper/go.mod Normal file
View File

@@ -0,0 +1,20 @@
module github.com/tx7do/go-utils/mapper
go 1.23.0
toolchain go1.24.3
require github.com/jinzhu/copier v0.4.0
require github.com/stretchr/testify v1.10.0
require (
github.com/davecgh/go-spew v1.1.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.10.0 // 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 => ../

25
mapper/go.sum Normal file
View File

@@ -0,0 +1,25 @@
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/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
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.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=
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=

10
mapper/interface.go Normal file
View File

@@ -0,0 +1,10 @@
package mapper
// Mapper defines the interface for converting between Data Transfer Objects (DTOs) and Database Entities.
type Mapper[DTO any, ENTITY any] interface {
// ToEntity converts a DTO to a Database Entity.
ToEntity(*DTO) *ENTITY
// ToDTO converts a Database Entity to a DTO.
ToDTO(*ENTITY) *DTO
}

51
mapper/mapper.go Normal file
View File

@@ -0,0 +1,51 @@
package mapper
import (
"github.com/jinzhu/copier"
)
type CopierMapper[DTO any, ENTITY any] struct {
copierOption copier.Option
}
func NewCopierMapper[DTO any, ENTITY any]() *CopierMapper[DTO, ENTITY] {
return &CopierMapper[DTO, ENTITY]{
copierOption: copier.Option{
Converters: []copier.TypeConverter{},
},
}
}
func (m *CopierMapper[DTO, ENTITY]) AppendConverter(converter copier.TypeConverter) {
m.copierOption.Converters = append(m.copierOption.Converters, converter)
}
func (m *CopierMapper[DTO, ENTITY]) AppendConverters(converters []copier.TypeConverter) {
m.copierOption.Converters = append(m.copierOption.Converters, converters...)
}
func (m *CopierMapper[DTO, ENTITY]) ToEntity(dto *DTO) *ENTITY {
if dto == nil {
return nil
}
var entity ENTITY
if err := copier.CopyWithOption(&entity, dto, m.copierOption); err != nil {
panic(err) // Handle error appropriately in production code
}
return &entity
}
func (m *CopierMapper[DTO, ENTITY]) ToDTO(entity *ENTITY) *DTO {
if entity == nil {
return nil
}
var dto DTO
if err := copier.CopyWithOption(&dto, entity, m.copierOption); err != nil {
panic(err) // Handle error appropriately in production code
}
return &dto
}

93
mapper/mapper_test.go Normal file
View File

@@ -0,0 +1,93 @@
package mapper
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCopierMapper(t *testing.T) {
type DtoType struct {
Name string
Age int
}
type EntityType struct {
Name string
Age int
}
mapper := NewCopierMapper[DtoType, EntityType]()
// 测试 ToEntity 方法
dto := &DtoType{Name: "Alice", Age: 25}
entity := mapper.ToEntity(dto)
assert.NotNil(t, entity)
assert.Equal(t, "Alice", entity.Name)
assert.Equal(t, 25, entity.Age)
// 测试 ToEntity 方法,传入 nil
entityNil := mapper.ToEntity(nil)
assert.Nil(t, entityNil)
// 测试 ToDTO 方法
entity = &EntityType{Name: "Bob", Age: 30}
dtoResult := mapper.ToDTO(entity)
assert.NotNil(t, dtoResult)
assert.Equal(t, "Bob", dtoResult.Name)
assert.Equal(t, 30, dtoResult.Age)
// 测试 ToDTO 方法,传入 nil
dtoNil := mapper.ToDTO(nil)
assert.Nil(t, dtoNil)
}
func TestEnumTypeConverter(t *testing.T) {
type DtoType int32
type EntityType string
const (
DtoTypeOne DtoType = 1
DtoTypeTwo DtoType = 2
)
const (
EntityTypeOne EntityType = "One"
EntityTypeTwo EntityType = "Two"
)
nameMap := map[int32]string{
1: "One",
2: "Two",
}
valueMap := map[string]int32{
"One": 1,
"Two": 2,
}
converter := NewEnumTypeConverter[DtoType, EntityType](nameMap, valueMap)
// 测试 ToEntity 方法
dto := DtoTypeOne
entity := converter.ToEntity(&dto)
assert.NotNil(t, entity)
assert.Equal(t, "One", string(*entity))
// 测试 ToEntity 方法,传入不存在的值
dtoInvalid := DtoType(3)
entityInvalid := converter.ToEntity(&dtoInvalid)
assert.Nil(t, entityInvalid)
// 测试 ToDTO 方法
tmpEntityTwo := EntityTypeTwo
entity = &tmpEntityTwo
dtoResult := converter.ToDTO(entity)
assert.NotNil(t, dtoResult)
assert.Equal(t, DtoType(2), *dtoResult)
// 测试 ToDTO 方法,传入不存在的值
tmpEntityThree := EntityType("Three")
entityInvalid = &tmpEntityThree
dtoInvalidResult := converter.ToDTO(entityInvalid)
assert.Nil(t, dtoInvalidResult)
}

14
password/README.md Normal file
View File

@@ -0,0 +1,14 @@
# 密码加解密
算法列表
| 算法名 | 特点 | 用途 |
|-----------------------------------------------|-------------------------------|------------------------|
| Bcrypt | 基于 Blowfish 算法,内置盐值,支持调整计算成本。 | 密码存储,防止暴力破解。 |
| Argon2 | 内存硬性哈希算法,支持多种参数调整(内存、时间、并行度)。 | 码存储,适合高安全性需求。 |
| PBKDF2 | 基于 HMAC 的密钥派生函数,支持多种哈希算法。 | 密码存储,兼容性好。 |
| SHA-256/SHA-512 | 快速单向哈希算法,无内置盐值。 | 数据完整性校验,需手动添加盐值用于密码存储。 |
| AES (Advanced Encryption Standard) | 对称加密算法,支持 128/192/256 位密钥。 | 数据加密传输或存储。 |
| RSA | 非对称加密算法,基于大整数分解难题。 | 数据加密、数字签名。 |
| ECDSA/ECDH (椭圆曲线算法) | 基于椭圆曲线的非对称加密,密钥更短但安全性高。 | 数据完整性和认证。 |
| HMAC (Hash-based Message Authentication Code) | 基于哈希算法的消息认证码。 | 数据完整性和认证。 |

111
password/argon2.go Normal file
View File

@@ -0,0 +1,111 @@
package password
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
// Argon2Crypto 实现 Argon2id 密码哈希算法
type Argon2Crypto struct {
// 参数可配置,默认使用推荐值
Memory uint32
Iterations uint32
Parallelism uint8
SaltLength uint32
KeyLength uint32
}
// NewArgon2Crypto 创建带默认参数的 Argon2 加密器
func NewArgon2Crypto() *Argon2Crypto {
return &Argon2Crypto{
Memory: 64 * 1024, // 64MB
Iterations: 3,
Parallelism: 2,
SaltLength: 16,
KeyLength: 32,
}
}
// Encrypt 实现密码加密
func (a *Argon2Crypto) Encrypt(password string) (string, error) {
// 生成随机盐值
salt := make([]byte, a.SaltLength)
if _, err := rand.Read(salt); err != nil {
return "", err
}
// 生成哈希
hash := argon2.IDKey(
[]byte(password),
salt,
a.Iterations,
a.Memory,
a.Parallelism,
a.KeyLength,
)
// 格式化输出(兼容标准格式)
return fmt.Sprintf(
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version,
a.Memory,
a.Iterations,
a.Parallelism,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}
// Verify 验证密码
func (a *Argon2Crypto) Verify(password, encrypted string) (bool, error) {
// 解析哈希字符串
parts := strings.Split(encrypted, "$")
if len(parts) != 6 {
return false, errors.New("无效的 Argon2 哈希格式")
}
// 解析参数
var version int
var memory, iterations uint32
var parallelism uint8
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
if err != nil || version != argon2.Version {
return false, errors.New("不支持的 Argon2 版本")
}
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, &parallelism)
if err != nil {
return false, errors.New("无效的 Argon2 参数")
}
// 解码盐值和哈希
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false, err
}
decodedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false, err
}
keyLength := uint32(len(decodedHash))
// 使用相同参数生成新哈希
newHash := argon2.IDKey(
[]byte(password),
salt,
iterations,
memory,
parallelism,
keyLength,
)
// 安全比较
return subtle.ConstantTimeCompare(newHash, decodedHash) == 1, nil
}

41
password/argon2_test.go Normal file
View File

@@ -0,0 +1,41 @@
package password
import (
"testing"
)
func TestArgon2Crypto_EncryptAndVerify(t *testing.T) {
crypto := NewArgon2Crypto()
// 测试加密
password := "securepassword"
encrypted, err := crypto.Encrypt(password)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Fatal("加密结果为空")
}
t.Log(encrypted)
// 测试验证成功
isValid, err := crypto.Verify(password, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("验证未通过,密码应匹配")
}
// 测试验证失败
isValid, err = crypto.Verify("wrongpassword", encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if isValid {
t.Fatal("验证通过,但密码不应匹配")
}
}

34
password/bcrypt.go Normal file
View File

@@ -0,0 +1,34 @@
package password
import (
"errors"
"golang.org/x/crypto/bcrypt"
)
type BCryptCrypto struct{}
func NewBCryptCrypto() *BCryptCrypto {
return &BCryptCrypto{}
}
// Encrypt 使用 bcrypt 加密密码,返回加密后的字符串和空盐值
func (b *BCryptCrypto) Encrypt(password string) (encrypted string, err error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
// Verify 验证密码是否匹配加密后的字符串
func (b *BCryptCrypto) Verify(password, encrypted string) (bool, error) {
err := bcrypt.CompareHashAndPassword([]byte(encrypted), []byte(password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return false, nil
}
return false, err
}
return true, nil
}

41
password/bcrypt_test.go Normal file
View File

@@ -0,0 +1,41 @@
package password
import (
"testing"
)
func TestBCryptCrypto_EncryptAndVerify(t *testing.T) {
crypto := NewBCryptCrypto()
// 测试加密
password := "securepassword"
encrypted, err := crypto.Encrypt(password)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Fatal("加密结果为空")
}
t.Log(encrypted)
// 测试验证成功
isValid, err := crypto.Verify(password, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("验证未通过,密码应匹配")
}
// 测试验证失败
isValid, err = crypto.Verify("wrongpassword", encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if isValid {
t.Fatal("验证通过,但密码不应匹配")
}
}

142
password/ecdsa_ecdh.go Normal file
View File

@@ -0,0 +1,142 @@
package password
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"math/big"
"strings"
)
// ECDSACrypto 实现基于 ECDSA 的加密和验证
type ECDSACrypto struct {
privateKey *ecdsa.PrivateKey
publicKey *ecdsa.PublicKey
}
// NewECDSACrypto 创建一个新的 ECDSACrypto 实例
func NewECDSACrypto() (*ECDSACrypto, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
return &ECDSACrypto{
privateKey: privateKey,
publicKey: &privateKey.PublicKey,
}, nil
}
// Encrypt 使用 ECDSA 对消息进行签名
func (e *ECDSACrypto) Encrypt(plainPassword string) (string, error) {
if plainPassword == "" {
return "", errors.New("密码不能为空")
}
hash := sha256.Sum256([]byte(plainPassword))
r, s, err := ecdsa.Sign(rand.Reader, e.privateKey, hash[:])
if err != nil {
return "", err
}
signature := r.String() + "$" + s.String()
return "ecdsa$" + signature, nil
}
// Verify 验证消息的签名是否有效
func (e *ECDSACrypto) Verify(plainPassword, encrypted string) (bool, error) {
if plainPassword == "" || encrypted == "" {
return false, errors.New("密码或加密字符串不能为空")
}
parts := strings.SplitN(encrypted, "$", 3)
if len(parts) != 3 || parts[0] != "ecdsa" {
return false, errors.New("加密字符串格式无效")
}
r := new(big.Int)
s := new(big.Int)
r.SetString(parts[1], 10)
s.SetString(parts[2], 10)
hash := sha256.Sum256([]byte(plainPassword))
return ecdsa.Verify(e.publicKey, hash[:], r, s), nil
}
// ECDHCrypto 实现基于 ECDH 的密钥交换
type ECDHCrypto struct {
privateKey *ecdsa.PrivateKey
publicKey *ecdsa.PublicKey
}
// NewECDHCrypto 创建一个新的 ECDHCrypto 实例
func NewECDHCrypto() (*ECDHCrypto, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
return &ECDHCrypto{
privateKey: privateKey,
publicKey: &privateKey.PublicKey,
}, nil
}
// Encrypt 返回公钥作为加密结果
func (e *ECDHCrypto) Encrypt(plainPassword string) (string, error) {
if plainPassword == "" {
return "", errors.New("密码不能为空")
}
publicKeyBytes := elliptic.Marshal(e.privateKey.Curve, e.publicKey.X, e.publicKey.Y)
return "ecdh$" + base64.StdEncoding.EncodeToString(publicKeyBytes), nil
}
// Verify 验证共享密钥是否一致
func (e *ECDHCrypto) Verify(plainPassword, encrypted string) (bool, error) {
if plainPassword == "" || encrypted == "" {
return false, errors.New("密码或加密字符串不能为空")
}
parts := strings.SplitN(encrypted, "$", 2)
if len(parts) != 2 || parts[0] != "ecdh" {
return false, errors.New("加密字符串格式无效")
}
publicKeyBytes, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return false, err
}
x, y := elliptic.Unmarshal(e.privateKey.Curve, publicKeyBytes)
if x == nil || y == nil {
return false, errors.New("无效的公钥")
}
sharedX, _ := e.privateKey.Curve.ScalarMult(x, y, e.privateKey.D.Bytes())
expectedHash := sha256.Sum256(sharedX.Bytes())
actualHash := sha256.Sum256([]byte(plainPassword))
return expectedHash == actualHash, nil
}
func (e *ECDHCrypto) DeriveSharedSecret(publicKey string) ([]byte, error) {
// 解码对方的公钥
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKey)
if err != nil {
return nil, err
}
// 反序列化公钥
x, y := elliptic.Unmarshal(e.privateKey.Curve, publicKeyBytes)
if x == nil || y == nil {
return nil, errors.New("无效的公钥")
}
// 计算共享密钥
sharedX, _ := e.privateKey.Curve.ScalarMult(x, y, e.privateKey.D.Bytes())
// 返回共享密钥的字节表示
return sharedX.Bytes(), nil
}

View File

@@ -0,0 +1,60 @@
package password
import (
"testing"
)
func TestECDSACrypto_EncryptAndVerify(t *testing.T) {
crypto, err := NewECDSACrypto()
if err != nil {
t.Fatalf("创建 ECDSACrypto 实例失败: %v", err)
}
message := "test message"
// 签名消息
encrypted, err := crypto.Encrypt(message)
if err != nil {
t.Fatalf("签名失败: %v", err)
}
// 验证签名
isValid, err := crypto.Verify(message, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("签名验证未通过")
}
}
func TestECDHCrypto_EncryptAndVerify(t *testing.T) {
crypto1, err := NewECDHCrypto()
if err != nil {
t.Fatalf("创建 ECDHCrypto 实例1失败: %v", err)
}
crypto2, err := NewECDHCrypto()
if err != nil {
t.Fatalf("创建 ECDHCrypto 实例2失败: %v", err)
}
message := "test message"
// 获取公钥
encrypted, err := crypto1.Encrypt(message)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
// 验证共享密钥
isValid, err := crypto2.Verify(message, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("共享密钥验证未通过")
}
}

11
password/go.mod Normal file
View File

@@ -0,0 +1,11 @@
module github.com/tx7do/go-utils/password
go 1.23.0
toolchain go1.23.2
require golang.org/x/crypto v0.39.0
require golang.org/x/sys v0.33.0 // indirect
replace github.com/tx7do/go-utils => ../

4
password/go.sum Normal file
View File

@@ -0,0 +1,4 @@
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

50
password/hmac.go Normal file
View File

@@ -0,0 +1,50 @@
package password
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
)
// HMACCrypto 实现基于 HMAC 的加密和验证
type HMACCrypto struct {
secretKey []byte
}
// NewHMACCrypto 创建一个新的 HMACCrypto 实例
func NewHMACCrypto(secretKey string) *HMACCrypto {
return &HMACCrypto{
secretKey: []byte(secretKey),
}
}
// Encrypt 使用 HMAC-SHA256 对数据进行加密
func (h *HMACCrypto) Encrypt(data string) (string, error) {
if data == "" {
return "", errors.New("数据不能为空")
}
mac := hmac.New(sha256.New, h.secretKey)
_, err := mac.Write([]byte(data))
if err != nil {
return "", err
}
hash := mac.Sum(nil)
return hex.EncodeToString(hash), nil
}
// Verify 验证数据的 HMAC 值是否匹配
func (h *HMACCrypto) Verify(data, encrypted string) (bool, error) {
if data == "" || encrypted == "" {
return false, errors.New("数据或加密字符串不能为空")
}
expectedHash, err := h.Encrypt(data)
if err != nil {
return false, err
}
return hmac.Equal([]byte(expectedHash), []byte(encrypted)), nil
}

43
password/hmac_test.go Normal file
View File

@@ -0,0 +1,43 @@
package password
import (
"testing"
)
func TestHMACCrypto_EncryptAndVerify(t *testing.T) {
secretKey := "mysecretkey"
crypto := NewHMACCrypto(secretKey)
data := "testdata"
// 测试加密
encrypted, err := crypto.Encrypt(data)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Fatal("加密结果为空")
}
t.Log(encrypted)
// 测试验证
isValid, err := crypto.Verify(data, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("验证结果不匹配")
}
// 测试验证失败的情况
isValid, err = crypto.Verify("wrongdata", encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if isValid {
t.Fatal("验证结果错误,预期验证失败")
}
}

29
password/interface.go Normal file
View File

@@ -0,0 +1,29 @@
package password
import (
"errors"
"strings"
)
// Crypto 密码加解密接口
type Crypto interface {
// Encrypt 加密密码,返回加密后的字符串(包含算法标识和盐值)
Encrypt(plainPassword string) (encrypted string, err error)
// Verify 验证密码是否匹配
Verify(plainPassword, encrypted string) (bool, error)
}
func CreateCrypto(algorithm string) (Crypto, error) {
algorithm = strings.ToLower(algorithm)
switch algorithm {
case "bcrypt":
return NewBCryptCrypto(), nil
case "pbkdf2":
return NewPBKDF2Crypto(), nil
case "argon2":
return NewArgon2Crypto(), nil
default:
return nil, errors.New("不支持的加密算法")
}
}

150
password/pbkdf2.go Normal file
View File

@@ -0,0 +1,150 @@
package password
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"hash"
"strconv"
"strings"
)
// PBKDF2Crypto 实现 PBKDF2-HMAC 密码哈希算法
type PBKDF2Crypto struct {
// 可配置参数,默认使用推荐值
Iterations int
KeyLength int
Hash func() hash.Hash
HashName string
}
// NewPBKDF2Crypto 创建带默认参数的 PBKDF2 加密器 (SHA256)
func NewPBKDF2Crypto() *PBKDF2Crypto {
return &PBKDF2Crypto{
Iterations: 310000, // NIST 推荐最小值
KeyLength: 32, // 256-bit
Hash: sha256.New,
HashName: "sha256",
}
}
// NewPBKDF2WithSHA512 创建使用 SHA512 的 PBKDF2 加密器
func NewPBKDF2WithSHA512() *PBKDF2Crypto {
return &PBKDF2Crypto{
Iterations: 600000, // SHA512 需要更多迭代
KeyLength: 64, // 512-bit
Hash: sha512.New,
HashName: "sha512",
}
}
// Encrypt 实现密码加密
func (p *PBKDF2Crypto) Encrypt(password string) (string, error) {
// 生成随机盐值 (16 bytes 推荐最小值)
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return "", err
}
// 生成密钥
key := pbkdf2Key([]byte(password), salt, p.Iterations, p.KeyLength, p.Hash)
// 格式: pbkdf2:<hash>:<iterations>:<base64-salt>:<base64-key>
return fmt.Sprintf(
"pbkdf2:%s:%d:%s:%s",
p.HashName,
p.Iterations,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(key),
), nil
}
// Verify 验证密码
func (p *PBKDF2Crypto) Verify(password, encrypted string) (bool, error) {
// 解析哈希字符串
parts := strings.Split(encrypted, ":")
if len(parts) != 5 || parts[0] != "pbkdf2" {
return false, errors.New("无效的 PBKDF2 哈希格式")
}
// 解析参数
hashName := parts[1]
iterations, err := strconv.Atoi(parts[2])
if err != nil {
return false, errors.New("无效的迭代次数")
}
salt, err := base64.RawStdEncoding.DecodeString(parts[3])
if err != nil {
return false, err
}
expectedKey, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false, err
}
// 根据哈希名称选择哈希函数
hashFunc, ok := getHashFunction(hashName)
if !ok {
return false, fmt.Errorf("不支持的哈希算法: %s", hashName)
}
// 生成新密钥
keyLength := len(expectedKey)
newKey := pbkdf2Key([]byte(password), salt, iterations, keyLength, hashFunc)
// 安全比较
return hmac.Equal(newKey, expectedKey), nil
}
// pbkdf2Key 实现 PBKDF2 核心算法
func pbkdf2Key(password, salt []byte, iterations, keyLength int, hashFunc func() hash.Hash) []byte {
prf := hmac.New(hashFunc, password)
hashLength := prf.Size()
blockCount := (keyLength + hashLength - 1) / hashLength
output := make([]byte, 0, blockCount*hashLength)
for i := 1; i <= blockCount; i++ {
// U1 = PRF(password, salt || INT(i))
prf.Reset()
prf.Write(salt)
binary.BigEndian.PutUint32(make([]byte, 4), uint32(i))
prf.Write([]byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i)})
u := prf.Sum(nil)
// F = U1 ⊕ U2 ⊕ ... ⊕ U_iterations
f := make([]byte, len(u))
copy(f, u)
for j := 1; j < iterations; j++ {
prf.Reset()
prf.Write(u)
u = prf.Sum(nil)
for k := 0; k < len(f); k++ {
f[k] ^= u[k]
}
}
output = append(output, f...)
}
return output[:keyLength]
}
// getHashFunction 根据名称获取哈希函数
func getHashFunction(name string) (func() hash.Hash, bool) {
switch name {
case "sha256":
return sha256.New, true
case "sha512":
return sha512.New, true
default:
return nil, false
}
}

77
password/pbkdf2_test.go Normal file
View File

@@ -0,0 +1,77 @@
package password
import (
"testing"
)
func TestPBKDF2Crypto_EncryptAndVerify(t *testing.T) {
crypto := NewPBKDF2Crypto()
// 测试加密
password := "securepassword"
encrypted, err := crypto.Encrypt(password)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Fatal("加密结果为空")
}
t.Log(encrypted)
// 测试验证成功
isValid, err := crypto.Verify(password, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("验证未通过,密码应匹配")
}
// 测试验证失败
isValid, err = crypto.Verify("wrongpassword", encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if isValid {
t.Fatal("验证通过,但密码不应匹配")
}
}
func TestPBKDF2WithSHA512_EncryptAndVerify(t *testing.T) {
crypto := NewPBKDF2WithSHA512()
// 测试加密
password := "securepassword"
encrypted, err := crypto.Encrypt(password)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Fatal("加密结果为空")
}
t.Log(encrypted)
// 测试验证成功
isValid, err := crypto.Verify(password, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("验证未通过,密码应匹配")
}
// 测试验证失败
isValid, err = crypto.Verify("wrongpassword", encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if isValid {
t.Fatal("验证通过,但密码不应匹配")
}
}

73
password/rsa.go Normal file
View File

@@ -0,0 +1,73 @@
package password
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
)
// RSACrypto 实现 RSA 加密和解密
type RSACrypto struct {
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
}
// NewRSACrypto 创建一个新的 RSACrypto 实例
func NewRSACrypto(keySize int) (*RSACrypto, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
if err != nil {
return nil, err
}
return &RSACrypto{
privateKey: privateKey,
publicKey: &privateKey.PublicKey,
}, nil
}
// Encrypt 使用公钥加密数据
func (r *RSACrypto) Encrypt(data string) (string, error) {
encryptedBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, r.publicKey, []byte(data), nil)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(encryptedBytes), nil
}
// Decrypt 使用私钥解密数据
func (r *RSACrypto) Decrypt(encryptedData string) (string, error) {
decodedData, err := base64.StdEncoding.DecodeString(encryptedData)
if err != nil {
return "", err
}
decryptedBytes, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, r.privateKey, decodedData, nil)
if err != nil {
return "", err
}
return string(decryptedBytes), nil
}
// ExportPrivateKey 导出私钥为 PEM 格式
func (r *RSACrypto) ExportPrivateKey() (string, error) {
privateKeyBytes := x509.MarshalPKCS1PrivateKey(r.privateKey)
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: privateKeyBytes,
})
return string(privateKeyPEM), nil
}
// ExportPublicKey 导出公钥为 PEM 格式
func (r *RSACrypto) ExportPublicKey() (string, error) {
publicKeyBytes, err := x509.MarshalPKIXPublicKey(r.publicKey)
if err != nil {
return "", err
}
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: publicKeyBytes,
})
return string(publicKeyPEM), nil
}

63
password/rsa_test.go Normal file
View File

@@ -0,0 +1,63 @@
package password
import (
"testing"
)
func TestRSACrypto_EncryptAndDecrypt(t *testing.T) {
// 创建 RSACrypto 实例
crypto, err := NewRSACrypto(2048)
if err != nil {
t.Fatalf("创建 RSACrypto 实例失败: %v", err)
}
// 测试加密
originalData := "securedata"
encryptedData, err := crypto.Encrypt(originalData)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encryptedData == "" {
t.Fatal("加密结果为空")
}
t.Log(encryptedData)
// 测试解密
decryptedData, err := crypto.Decrypt(encryptedData)
if err != nil {
t.Fatalf("解密失败: %v", err)
}
if decryptedData != originalData {
t.Fatalf("解密结果不匹配,期望: %s实际: %s", originalData, decryptedData)
}
}
func TestRSACrypto_ExportKeys(t *testing.T) {
// 创建 RSACrypto 实例
crypto, err := NewRSACrypto(2048)
if err != nil {
t.Fatalf("创建 RSACrypto 实例失败: %v", err)
}
// 测试导出私钥
privateKey, err := crypto.ExportPrivateKey()
if err != nil {
t.Fatalf("导出私钥失败: %v", err)
}
if privateKey == "" {
t.Fatal("导出的私钥为空")
}
// 测试导出公钥
publicKey, err := crypto.ExportPublicKey()
if err != nil {
t.Fatalf("导出公钥失败: %v", err)
}
if publicKey == "" {
t.Fatal("导出的公钥为空")
}
}

110
password/sha.go Normal file
View File

@@ -0,0 +1,110 @@
package password
import (
"errors"
"fmt"
"hash"
"strings"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
)
// SHACrypto 实现 SHA-256/SHA-512 密码哈希算法(带盐值)
type SHACrypto struct {
Hash func() hash.Hash
HashName string
SaltLength int
}
// NewSHA256Crypto 创建 SHA-256 加密器
func NewSHA256Crypto() *SHACrypto {
return &SHACrypto{
Hash: sha256.New,
HashName: "sha256",
SaltLength: 16, // 16 字节盐值
}
}
// NewSHA512Crypto 创建 SHA-512 加密器
func NewSHA512Crypto() *SHACrypto {
return &SHACrypto{
Hash: sha512.New,
HashName: "sha512",
SaltLength: 16, // 16 字节盐值
}
}
// Encrypt 实现密码加密(带盐值)
func (s *SHACrypto) Encrypt(password string) (string, error) {
// 生成随机盐值
salt := make([]byte, s.SaltLength)
if _, err := rand.Read(salt); err != nil {
return "", err
}
// 计算哈希
hashValue := s.Hash()
hashValue.Write(salt)
hashValue.Write([]byte(password))
hashBytes := hashValue.Sum(nil)
// 格式: sha256:$salt$hash 或 sha512:$salt$hash
return fmt.Sprintf(
"%s$%s$%s",
s.HashName,
base64.RawStdEncoding.EncodeToString(salt),
hex.EncodeToString(hashBytes),
), nil
}
// Verify 验证密码
func (s *SHACrypto) Verify(password, encrypted string) (bool, error) {
// 解析哈希字符串
parts := strings.Split(encrypted, "$")
if len(parts) != 3 {
return false, errors.New("无效的 SHA 哈希格式")
}
hashName := parts[0]
if hashName != s.HashName {
return false, fmt.Errorf("哈希算法不匹配: 期望 %s, 实际 %s", s.HashName, hashName)
}
// 解码盐值
salt, err := base64.RawStdEncoding.DecodeString(parts[1])
if err != nil {
return false, err
}
// 解码原始哈希值
originalHash, err := hex.DecodeString(parts[2])
if err != nil {
return false, err
}
// 计算新哈希
hashValue := s.Hash()
hashValue.Write(salt)
hashValue.Write([]byte(password))
newHash := hashValue.Sum(nil)
// 安全比较
return compareHash(newHash, originalHash), nil
}
// compareHash 安全比较两个哈希值
func compareHash(h1, h2 []byte) bool {
if len(h1) != len(h2) {
return false
}
result := 0
for i := 0; i < len(h1); i++ {
result |= int(h1[i] ^ h2[i])
}
return result == 0
}

53
password/sha_test.go Normal file
View File

@@ -0,0 +1,53 @@
package password
import (
"testing"
)
func TestSHACrypto_EncryptAndVerify_SHA256(t *testing.T) {
crypto := NewSHA256Crypto()
password := "securepassword"
encrypted, err := crypto.Encrypt(password)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Fatal("加密结果为空")
}
t.Log(encrypted)
isValid, err := crypto.Verify(password, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("验证结果不匹配")
}
}
func TestSHACrypto_EncryptAndVerify_SHA512(t *testing.T) {
crypto := NewSHA512Crypto()
password := "securepassword"
encrypted, err := crypto.Encrypt(password)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Fatal("加密结果为空")
}
t.Log(encrypted)
isValid, err := crypto.Verify(password, encrypted)
if err != nil {
t.Fatalf("验证失败: %v", err)
}
if !isValid {
t.Fatal("验证结果不匹配")
}
}

165
query_parser/README.md Normal file
View File

@@ -0,0 +1,165 @@
# 查询解析器
## 排序规则
排序操作本质上是`SQL`里面的`Order By`条件。
| 序列 | 示例 | 备注 |
|----|--------------------|--------------|
| 升序 | `["type"]` | |
| 降序 | `["-create_time"]` | 字段名前加`-`是为降序 |
## 过滤规则
过滤器操作,其实质上是将用户的查询条件转换为数据库查询语句中的`WHERE`子句。通过这种方式,用户可以根据需要筛选数据,获取更精确的结果。
一个完整的过滤器分为三个部分:
1. **字段名 (Field)**:要查询的字段。
2. **操作符 (Operator)**:用于指定查询的类型。
3. **值 (Value)**:要查询的具体值。
如果只是普通的查询,只需要传递`字段名`即可,但是如果需要一些特殊的查询,那么就需要加入`操作符`了。
过滤器的`操作符`规则我借鉴并遵循了Python中一些ORM的规则比如
- [Tortoise ORM Filtering][1]。
- [Django Field lookups][2]
过滤器通过query参数传递它必须要遵循某一种格式或者说规则。在这里我实现了两种格式
- **JSON格式**:使用`JSON`对象来表示查询条件。
- **自定义字符串格式**:使用`自定义的字符串`来表示查询条件。
### JSON格式
在这套规则里面我们有2个分隔符
- **双下划线** `__`:用于分隔`字段名``操作符`,如果没有操作符则视作等于操作。
- **点号** `.`:用于分隔`字段名``JSON字段名`
```text
{字段名}__{操作符} : {查询值}
{字段名}.{JSON字段名}__{操作符} : {查询值}
{{字段名1}__{操作符1} : {查询值1}, {字段名2}__{操作符2} : {查询值2}}
[{{字段名1}__{操作符1} : {查询值1}}, {{字段名1}__{操作符2} : {查询值2}}]
```
| 查找类型 | 示例 | 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")` | |
| 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`,两个日期一致就会导致查询不到数据。 |
| 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%';` | |
| icontains | `{"name__icontains" : "L"}` | `WHERE name ILIKE '%L%';` | |
| startswith | `{"name__startswith" : "La"}` | `WHERE name LIKE 'La%';` | |
| istartswith | `{"name__istartswith" : "La"}` | `WHERE name ILIKE 'La%';` | |
| endswith | `{"name__endswith" : "a"}` | `WHERE name LIKE '%a';` | |
| iendswith | `{"name__iendswith" : "a"}` | `WHERE name ILIKE '%a';` | |
| exact | `{"name__exact" : "a"}` | `WHERE name LIKE 'a';` | |
| iexact | `{"name__iexact" : "a"}` | `WHERE name ILIKE 'a';` | |
| regex | `{"title__regex" : "^(An?\|The) +"}` | MySQL: `WHERE title REGEXP BINARY '^(An?\|The) +'` <br> Oracle: `WHERE REGEXP_LIKE(title, '^(An?\|The) +', 'c');` <br> PostgreSQL: `WHERE title ~ '^(An?\|The) +';` <br> SQLite: `WHERE title REGEXP '^(An?\|The) +';` | |
| iregex | `{"title__iregex" : "^(an?\|the) +"}` | MySQL: `WHERE title REGEXP '^(an?\|the) +'` <br> Oracle: `WHERE REGEXP_LIKE(title, '^(an?\|the) +', 'i');` <br> PostgreSQL: `WHERE title ~* '^(an?\|the) +';` <br> SQLite: `WHERE title REGEXP '(?i)^(an?\|the) +';` | |
| search | | | |
以及将日期提取出来的查找类型:
| 查找类型 | 示例 | SQL | 备注 |
|--------------|--------------------------------------|---------------------------------------------------|----------------------|
| date | `{"pub_date__date" : "2023-01-01"}` | `WHERE DATE(pub_date) = '2023-01-01'` | |
| year | `{"pub_date__year" : "2023"}` | `WHERE EXTRACT('YEAR' FROM pub_date) = '2023'` | 哪一年 |
| iso_year | `{"pub_date__iso_year" : "2023"}` | `WHERE EXTRACT('ISOYEAR' FROM pub_date) = '2023'` | ISO 8601 一年中的周数 |
| month | `{"pub_date__month" : "12"}` | `WHERE EXTRACT('MONTH' FROM pub_date) = '12'` | 月份1-12 |
| day | `{"pub_date__day" : "3"}` | `WHERE EXTRACT('DAY' FROM pub_date) = '3'` | 该月的某天(1-31) |
| week | `{"pub_date__week" : "7"}` | `WHERE EXTRACT('WEEK' FROM pub_date) = '7'` | ISO 8601 周编号 一年中的周数 |
| week_day | `{"pub_date__week_day" : "tom"}` | `` | 星期几 |
| iso_week_day | `{"pub_date__iso_week_day" : "tom"}` | `` | |
| quarter | `{"pub_date__quarter" : "1"}` | `WHERE EXTRACT('QUARTER' FROM pub_date) = '1'` | 一年中的季度 |
| time | `{"pub_date__time" : "12:59:59"}` | `` | |
| hour | `{"pub_date__hour" : "12"}` | `WHERE EXTRACT('HOUR' FROM pub_date) = '12'` | 小时(0-23) |
| minute | `{"pub_date__minute" : "59"}` | `WHERE EXTRACT('MINUTE' FROM pub_date) = '59'` | 分钟 (0-59) |
| second | `{"pub_date__second" : "59"}` | `WHERE EXTRACT('SECOND' FROM pub_date) = '59'` | 秒 (0-59) |
### 自定义字符串格式
在这套规则里面我们有5个分隔符
- **逗号** `,`:用于分隔多个`查询条件`,如果没有操作符则视作等于操作。
- **冒号** `:`:用于分隔`字段名+操作符` 和 `查询值`。
- **双下划线** `__`:用于分隔`字段名`和`操作符`。
- **竖线** `|`:用于分隔多个的`查询值`。
- **点号** `.`:用于分隔`字段名`和`JSON字段名`。
```text
{字段名}__{操作符} : {查询值}
{字段名1}__{操作符1} : {查询值1}, {字段名2}__{操作符2} : {查询值2}
{字段名}.{JSON字段名}__{操作符} : {查询值}
```
| 查找类型 | 示例 | SQL | 备注 |
|-------------|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| not | `name__not : tom` | `WHERE NOT ("name" = "tom")` | |
| in | `name__in" : tom \| jimm` | `WHERE name IN ("tom", "jimmy")` | |
| not_in | `name__not_in : tom \| jimm` | `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`,两个日期一致就会导致查询不到数据。 |
| 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%';` | |
| icontains | `name__icontains : L` | `WHERE name ILIKE '%L%';` | |
| startswith | `name__startswith : La` | `WHERE name LIKE 'La%';` | |
| istartswith | `name__istartswith : La` | `WHERE name ILIKE 'La%';` | |
| endswith | `name__endswith : a` | `WHERE name LIKE '%a';` | |
| iendswith | `name__iendswith : a` | `WHERE name ILIKE '%a';` | |
| exact | `name__exact : a` | `WHERE name LIKE 'a';` | |
| iexact | `name__iexact : a` | `WHERE name ILIKE 'a';` | |
| regex | `title__regex : ^(An?\|The) +` | MySQL: `WHERE title REGEXP BINARY '^(An?\|The) +'` <br> Oracle: `WHERE REGEXP_LIKE(title, '^(An?\|The) +', 'c');` <br> PostgreSQL: `WHERE title ~ '^(An?\|The) +';` <br> SQLite: `WHERE title REGEXP '^(An?\|The) +';` | |
| iregex | `title__iregex : ^(an?\|the) +` | MySQL: `WHERE title REGEXP '^(an?\|the) +'` <br> Oracle: `WHERE REGEXP_LIKE(title, '^(an?\|the) +', 'i');` <br> PostgreSQL: `WHERE title ~* '^(an?\|the) +';` <br> SQLite: `WHERE title REGEXP '(?i)^(an?\|the) +';` | |
| search | | | |
以及将日期提取出来的查找类型:
| 查找类型 | 示例 | SQL | 备注 |
|--------------|--------------------------------|---------------------------------------------------|----------------------|
| date | `pub_date__date : 2023-01-01` | `WHERE DATE(pub_date) = '2023-01-01'` | |
| year | `pub_date__year : 2023` | `WHERE EXTRACT('YEAR' FROM pub_date) = '2023'` | 哪一年 |
| iso_year | `pub_date__iso_year : 2023` | `WHERE EXTRACT('ISOYEAR' FROM pub_date) = '2023'` | ISO 8601 一年中的周数 |
| month | `pub_date__month : 12` | `WHERE EXTRACT('MONTH' FROM pub_date) = '12'` | 月份1-12 |
| day | `pub_date__day : 3` | `WHERE EXTRACT('DAY' FROM pub_date) = '3'` | 该月的某天(1-31) |
| week | `pub_date__week : 7` | `WHERE EXTRACT('WEEK' FROM pub_date) = '7'` | ISO 8601 周编号 一年中的周数 |
| week_day | `pub_date__week_day : tom` | `` | 星期几 |
| iso_week_day | `pub_date__iso_week_day : tom` | `` | |
| quarter | `pub_date__quarter : 1` | `WHERE EXTRACT('QUARTER' FROM pub_date) = '1'` | 一年中的季度 |
| time | `pub_date__time : 12:59:59` | `` | |
| hour | `pub_date__hour : 12` | `WHERE EXTRACT('HOUR' FROM pub_date) = '12'` | 小时(0-23) |
| minute | `pub_date__minute : 59` | `WHERE EXTRACT('MINUTE' FROM pub_date) = '59'` | 分钟 (0-59) |
| second | `pub_date__second : 59` | `WHERE EXTRACT('SECOND' FROM pub_date) = '59'` | 秒 (0-59) |
## 参考资料
- [Tortoise ORM Filtering][1]
- [Django Field lookups][2]
- [PostgreSQL Date/Time Functions and Operators][3]
- [PostgreSQL Regular Expressions][4]
- [PostgreSQL Date/Time Types][5]
[1]: https://tortoise.github.io/query.html#filtering
[2]: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups
[3]: https://www.postgresql.org/docs/current/functions-datetime.html
[4]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-REGEXP-TABLE
[5]: https://www.postgresql.org/docs/current/datatype-datetime.html

180
query_parser/filter.go Normal file
View File

@@ -0,0 +1,180 @@
package query_parser
import (
"strings"
"github.com/go-kratos/kratos/v2/encoding"
_ "github.com/go-kratos/kratos/v2/encoding/json"
"github.com/tx7do/go-utils/stringcase"
)
type FilterOperator string
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" // 全文搜索
)
const (
DatePartDate = "date" // 日期
DatePartYear = "year" // 年
DatePartISOYear = "iso_year" // ISO8601 一年中的周数
DatePartQuarter = "quarter" // 季度
DatePartMonth = "month" // 月
DatePartWeek = "week" // ISO8601 周编号 一年中的周数
DatePartWeekDay = "week_day" // 星期几
DatePartISOWeekDay = "iso_week_day" // ISO8601 星期几
DatePartDay = "day" // 日
DatePartTime = "time" // 小时:分钟:秒
DatePartHour = "hour" // 小时
DatePartMinute = "minute" // 分钟
DatePartSecond = "second" // 秒
DatePartMicrosecond = "microsecond" // 微秒
)
const (
JSONFilterFieldOperatorDelimiter = "__" // JSON过滤器 - 字段名和操作符的分隔符
QueryFilterFieldOperatorDelimiter = ":" // 自定义查询字符串过滤器 - 字段名和操作符的分隔符
QueryFilterQueriesDelimiter = "," // 自定义查询字符串过滤器 - 多个键值对的分隔符
QueryFilterValuesDelimiter = "|" // 自定义查询字符串过滤器 - 多个值的分隔符
JsonFieldDelimiter = "." // JSON字段分隔符
)
type FilterHandler func(field, operator, value string)
// ParseFilterJSONString 解析过滤条件的JSON字符串调用处理函数
func ParseFilterJSONString(query string, handler FilterHandler) error {
if query == "" {
return nil
}
codec := encoding.GetCodec("json")
var err error
queryMap := make(map[string]string)
if err = codec.Unmarshal([]byte(query), &queryMap); err == nil {
for k, v := range queryMap {
ParseFilterField(k, v, handler)
}
return nil
}
var queryMapArray []map[string]string
if err = codec.Unmarshal([]byte(query), &queryMapArray); err == nil {
for _, item := range queryMapArray {
for k, v := range item {
ParseFilterField(k, v, handler)
}
}
return nil
}
return err
}
// ParseFilterQueryString 解析过滤条件的查询字符串,调用处理函数
func ParseFilterQueryString(query string, handler FilterHandler) error {
if query == "" {
return nil // 如果查询字符串为空,直接返回
}
// 按逗号分割查询字符串,得到多个键值对
pairs := SplitQueryQueries(query)
for _, pair := range pairs {
// 按冒号分割键值对,提取字段名和值
parts := SplitQueryFieldAndOperator(pair)
if len(parts) != 2 {
continue // 跳过无效的键值对
}
// 解码字段名
key, err := DecodeSpecialCharacters(strings.TrimSpace(parts[0]))
if err != nil {
continue // 跳过解码失败的键值对
}
// 解码字段值
value, err := DecodeSpecialCharacters(strings.TrimSpace(parts[1]))
if err != nil {
continue // 跳过解码失败的键值对
}
// 调用 ParseFilterField 解析字段和操作符
ParseFilterField(key, value, handler)
}
return nil
}
// ParseFilterField 解析过滤条件字符串,调用处理函数
func ParseFilterField(key, value string, handler FilterHandler) {
if key == "" || value == "" {
return // 没有过滤条件
}
// 处理字段和操作符
parts := SplitJsonFieldAndOperator(key)
if len(parts) < 1 {
return // 无效的字段格式
}
field := strings.TrimSpace(parts[0])
if field == "" {
return
}
field = stringcase.ToSnakeCase(parts[0])
op := ""
if len(parts) > 1 {
op = parts[1]
}
handler(field, op, value)
}
// SplitJsonFieldAndOperator JSON过滤器 - 分割“字段名”和“操作符”
func SplitJsonFieldAndOperator(field string) []string {
return strings.Split(field, JSONFilterFieldOperatorDelimiter)
}
// SplitQueryFieldAndOperator 自定义查询字符串过滤器 - 分割“字段名”和“操作符”
func SplitQueryFieldAndOperator(field string) []string {
return strings.Split(field, QueryFilterFieldOperatorDelimiter)
}
// SplitQueryQueries 自定义查询字符串过滤器 - 分割多个键值对
func SplitQueryQueries(field string) []string {
return strings.Split(field, QueryFilterQueriesDelimiter)
}
// SplitQueryValues 自定义查询字符串过滤器 - 分割多个值
func SplitQueryValues(field string) []string {
return strings.Split(field, QueryFilterValuesDelimiter)
}
// SplitJSONField 将JSONB字段字符串按分隔符分割成多个字段
func SplitJSONField(field string) []string {
return strings.Split(field, JsonFieldDelimiter)
}

401
query_parser/filter_test.go Normal file
View File

@@ -0,0 +1,401 @@
package query_parser
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestParseFilterJSONString(t *testing.T) {
var results []struct {
Field string
Operator string
Value string
}
handler := func(field, operator, value string) {
results = append(results, struct {
Field string
Operator string
Value string
}{Field: field, Operator: operator, Value: value})
}
// 测试解析单个过滤条件
results = nil
err := ParseFilterJSONString(`{"name__exact":"John"}`, handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "name", results[0].Field)
assert.Equal(t, "exact", results[0].Operator)
assert.Equal(t, "John", results[0].Value)
// 测试解析多个过滤条件
results = nil
err = ParseFilterJSONString(`[{"age__gte":"30"},{"status__exact":"active"}]`, handler)
assert.NoError(t, err)
assert.Equal(t, 2, len(results))
assert.Equal(t, "age", results[0].Field)
assert.Equal(t, "gte", results[0].Operator)
assert.Equal(t, "30", results[0].Value)
assert.Equal(t, "status", results[1].Field)
assert.Equal(t, "exact", results[1].Operator)
assert.Equal(t, "active", results[1].Value)
// 测试空字符串
results = nil
err = ParseFilterJSONString("", handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
// 测试无效的JSON字符串
results = nil
err = ParseFilterJSONString(`invalid_json`, handler)
assert.Error(t, err)
assert.Equal(t, 0, len(results))
// 测试包含特殊字符的字段和值
results = nil
err = ParseFilterJSONString(`{"na!me__exact":"Jo@hn"}`, handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "na_me", results[0].Field)
assert.Equal(t, "exact", results[0].Operator)
assert.Equal(t, "Jo@hn", results[0].Value)
// 测试包含特殊字符的多个过滤条件
results = nil
err = ParseFilterJSONString(`[{"ag#e__gte":"30"},{"sta$tus__exact":"ac^tive"}]`, handler)
assert.NoError(t, err)
assert.Equal(t, 2, len(results))
assert.Equal(t, "ag_e", results[0].Field)
assert.Equal(t, "gte", results[0].Operator)
assert.Equal(t, "30", results[0].Value)
assert.Equal(t, "sta_tus", results[1].Field)
assert.Equal(t, "exact", results[1].Operator)
assert.Equal(t, "ac^tive", results[1].Value)
// 测试包含特殊字符的无效 JSON 字符串
results = nil
err = ParseFilterJSONString(`{"na!me__exact":Jo@hn}`, handler)
assert.Error(t, err)
assert.Equal(t, 0, len(results))
}
func TestParseFilterQueryString(t *testing.T) {
var results []struct {
Field string
Operator string
Value string
}
handler := func(field, operator, value string) {
results = append(results, struct {
Field string
Operator string
Value string
}{Field: field, Operator: operator, Value: value})
}
// 测试解析单个过滤条件
results = nil
err := ParseFilterQueryString("name__exact:John", handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "name", results[0].Field)
assert.Equal(t, "exact", results[0].Operator)
assert.Equal(t, "John", results[0].Value)
// 测试解析多个过滤条件
results = nil
err = ParseFilterQueryString("age__gte:30,status__exact:active", handler)
assert.NoError(t, err)
assert.Equal(t, 2, len(results))
assert.Equal(t, "age", results[0].Field)
assert.Equal(t, "gte", results[0].Operator)
assert.Equal(t, "30", results[0].Value)
assert.Equal(t, "status", results[1].Field)
assert.Equal(t, "exact", results[1].Operator)
assert.Equal(t, "active", results[1].Value)
// 测试空字符串
results = nil
err = ParseFilterQueryString("", handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
// 测试无效的查询字符串
results = nil
err = ParseFilterQueryString("invalid_query", handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
// 测试包含特殊字符的字段和值
results = nil
err = ParseFilterQueryString("na!me__exact:Jo@hn", handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "na_me", results[0].Field)
assert.Equal(t, "exact", results[0].Operator)
assert.Equal(t, "Jo@hn", results[0].Value)
// 测试包含特殊字符的多个过滤条件
results = nil
err = ParseFilterQueryString("ag#e__gte:30,sta$tus__exact:ac^tive", handler)
assert.NoError(t, err)
assert.Equal(t, 2, len(results))
assert.Equal(t, "ag_e", results[0].Field)
assert.Equal(t, "gte", results[0].Operator)
assert.Equal(t, "30", results[0].Value)
assert.Equal(t, "sta_tus", results[1].Field)
assert.Equal(t, "exact", results[1].Operator)
assert.Equal(t, "ac^tive", results[1].Value)
// 测试 Field 中包含分隔符
results = nil
err = ParseFilterQueryString("na:me__exact:John", handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
// 测试 Operator 中包含分隔符
results = nil
err = ParseFilterQueryString("name__ex:act:John", handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
// 测试 Value 中包含分隔符
results = nil
err = ParseFilterQueryString("name__exact:Jo|hn", handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "name", results[0].Field)
assert.Equal(t, "exact", results[0].Operator)
assert.Equal(t, "Jo|hn", results[0].Value)
// 测试多个过滤条件中包含分隔符
results = nil
err = ParseFilterQueryString("ag:e__gte:30,sta|tus__exact:ac|tive", handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
// 测试 Field 中包含编码后的分隔符
results = nil
encodedField := EncodeSpecialCharacters("na:me")
err = ParseFilterQueryString(encodedField+"__exact:John", handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "na_me", results[0].Field) // 注意:这里的字段名会被转换为 snake_case
assert.Equal(t, "exact", results[0].Operator)
assert.Equal(t, "John", results[0].Value)
// 测试 Operator 中包含编码后的分隔符
results = nil
encodedOperator := EncodeSpecialCharacters("ex:act")
err = ParseFilterQueryString("name__"+encodedOperator+":John", handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "name", results[0].Field)
assert.Equal(t, "ex:act", results[0].Operator)
assert.Equal(t, "John", results[0].Value)
// 测试 Value 中包含编码后的分隔符
results = nil
encodedValue := EncodeSpecialCharacters("Jo|hn")
err = ParseFilterQueryString("name__exact:"+encodedValue, handler)
assert.NoError(t, err)
assert.Equal(t, 1, len(results))
assert.Equal(t, "name", results[0].Field)
assert.Equal(t, "exact", results[0].Operator)
assert.Equal(t, "Jo|hn", results[0].Value)
// 测试多个过滤条件中包含编码后的分隔符
results = nil
encodedField1 := EncodeSpecialCharacters("ag:e")
encodedValue1 := EncodeSpecialCharacters("30")
encodedField2 := EncodeSpecialCharacters("sta|tus")
encodedValue2 := EncodeSpecialCharacters("ac|tive")
err = ParseFilterQueryString(encodedField1+"__gte:"+encodedValue1+","+encodedField2+"__exact:"+encodedValue2, handler)
assert.NoError(t, err)
assert.Equal(t, 2, len(results))
assert.Equal(t, "ag_e", results[0].Field) // 注意:这里的字段名会被转换为 snake_case
assert.Equal(t, "gte", results[0].Operator)
assert.Equal(t, "30", results[0].Value)
assert.Equal(t, "sta_tus", results[1].Field) // 注意:这里的字段名会被转换为 snake_case
assert.Equal(t, "exact", results[1].Operator)
assert.Equal(t, "ac|tive", results[1].Value)
}
func TestParseFilterField(t *testing.T) {
var results []struct {
Field string
Operator string
Value string
}
handler := func(field, operator, value string) {
results = append(results, struct {
Field string
Operator string
Value string
}{Field: field, Operator: operator, Value: value})
}
// 测试正常解析
results = nil
ParseFilterField("name__exact", "John", handler)
assert.Equal(t, 1, len(results))
assert.Equal(t, "name", results[0].Field)
assert.Equal(t, "exact", results[0].Operator)
assert.Equal(t, "John", results[0].Value)
// 测试无操作符解析
results = nil
ParseFilterField("name", "John", handler)
assert.Equal(t, 1, len(results))
assert.Equal(t, "name", results[0].Field)
assert.Equal(t, "", results[0].Operator)
assert.Equal(t, "John", results[0].Value)
// 测试空字段
results = nil
ParseFilterField("", "John", handler)
assert.Equal(t, 0, len(results))
// 测试空值
results = nil
ParseFilterField("name__exact", "", handler)
assert.Equal(t, 0, len(results))
// 测试无效字段格式
results = nil
ParseFilterField("__exact", "John", handler)
assert.Equal(t, 0, len(results))
}
func TestSplitJsonFieldAndOperator(t *testing.T) {
// 测试正常分割
result := SplitJsonFieldAndOperator("name__exact")
assert.Equal(t, 2, len(result))
assert.Equal(t, "name", result[0])
assert.Equal(t, "exact", result[1])
// 测试无操作符
result = SplitJsonFieldAndOperator("name")
assert.Equal(t, 1, len(result))
assert.Equal(t, "name", result[0])
// 测试空字符串
result = SplitJsonFieldAndOperator("")
assert.Equal(t, 1, len(result))
assert.Equal(t, "", result[0])
// 测试多个分隔符
result = SplitJsonFieldAndOperator("name__exact__extra")
assert.Equal(t, 3, len(result))
assert.Equal(t, "name", result[0])
assert.Equal(t, "exact", result[1])
assert.Equal(t, "extra", result[2])
}
func TestSplitJSONField(t *testing.T) {
// 测试正常分割
result := SplitJSONField("user.address.city")
assert.Equal(t, 3, len(result))
assert.Equal(t, "user", result[0])
assert.Equal(t, "address", result[1])
assert.Equal(t, "city", result[2])
// 测试单个字段
result = SplitJSONField("user")
assert.Equal(t, 1, len(result))
assert.Equal(t, "user", result[0])
// 测试空字符串
result = SplitJSONField("")
assert.Equal(t, 1, len(result))
assert.Equal(t, "", result[0])
// 测试多个分隔符
result = SplitJSONField("user..address.city")
assert.Equal(t, 4, len(result))
assert.Equal(t, "user", result[0])
assert.Equal(t, "", result[1])
assert.Equal(t, "address", result[2])
assert.Equal(t, "city", result[3])
}
func TestSplitQueryFieldAndOperator(t *testing.T) {
// 测试正常分割
result := SplitQueryFieldAndOperator("name:exact")
assert.Equal(t, 2, len(result))
assert.Equal(t, "name", result[0])
assert.Equal(t, "exact", result[1])
// 测试无操作符
result = SplitQueryFieldAndOperator("name")
assert.Equal(t, 1, len(result))
assert.Equal(t, "name", result[0])
// 测试空字符串
result = SplitQueryFieldAndOperator("")
assert.Equal(t, 1, len(result))
assert.Equal(t, "", result[0])
// 测试多个分隔符
result = SplitQueryFieldAndOperator("name:exact:extra")
assert.Equal(t, 3, len(result))
assert.Equal(t, "name", result[0])
assert.Equal(t, "exact", result[1])
assert.Equal(t, "extra", result[2])
}
func TestSplitQueryQueries(t *testing.T) {
// 测试正常分割多个键值对
result := SplitQueryQueries("name:John,age:30,status:active")
assert.Equal(t, 3, len(result))
assert.Equal(t, "name:John", result[0])
assert.Equal(t, "age:30", result[1])
assert.Equal(t, "status:active", result[2])
// 测试单个键值对
result = SplitQueryQueries("name:John")
assert.Equal(t, 1, len(result))
assert.Equal(t, "name:John", result[0])
// 测试空字符串
result = SplitQueryQueries("")
assert.Equal(t, 1, len(result))
assert.Equal(t, "", result[0])
// 测试多个分隔符
result = SplitQueryQueries("name:John,,age:30")
assert.Equal(t, 3, len(result))
assert.Equal(t, "name:John", result[0])
assert.Equal(t, "", result[1])
assert.Equal(t, "age:30", result[2])
}
func TestSplitQueryValues(t *testing.T) {
// 测试正常分割多个值
result := SplitQueryValues("value1|value2|value3")
assert.Equal(t, 3, len(result))
assert.Equal(t, "value1", result[0])
assert.Equal(t, "value2", result[1])
assert.Equal(t, "value3", result[2])
// 测试单个值
result = SplitQueryValues("value1")
assert.Equal(t, 1, len(result))
assert.Equal(t, "value1", result[0])
// 测试空字符串
result = SplitQueryValues("")
assert.Equal(t, 1, len(result))
assert.Equal(t, "", result[0])
// 测试多个分隔符
result = SplitQueryValues("value1||value2")
assert.Equal(t, 3, len(result))
assert.Equal(t, "value1", result[0])
assert.Equal(t, "", result[1])
assert.Equal(t, "value2", result[2])
}

21
query_parser/go.mod Normal file
View File

@@ -0,0 +1,21 @@
module github.com/tx7do/go-utils/query_parser
go 1.23.0
toolchain go1.23.2
require (
github.com/go-kratos/kratos/v2 v2.8.4
github.com/stretchr/testify v1.10.0
github.com/tx7do/go-utils v1.1.29
)
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.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/tx7do/go-utils => ../

26
query_parser/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/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/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.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=

50
query_parser/orderby.go Normal file
View File

@@ -0,0 +1,50 @@
package query_parser
import (
"strings"
)
type OrderByHandler func(field string, desc bool)
// ParseOrderByString 解析排序字符串,调用处理函数。
func ParseOrderByString(orderBy string, handler OrderByHandler) error {
if orderBy == "" {
return nil
}
parts := strings.Split(orderBy, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
ParseOrderByField(part, handler)
}
return nil
}
// ParseOrderByStrings 解析多个排序字符串,调用处理函数。
func ParseOrderByStrings(orderBys []string, handler OrderByHandler) error {
for _, v := range orderBys {
if v == "" {
continue
}
ParseOrderByField(v, handler)
}
return nil
}
// ParseOrderByField 解析单个排序字段,调用处理函数。
func ParseOrderByField(orderBy string, handler OrderByHandler) {
orderBy = strings.TrimSpace(orderBy)
if orderBy == "" {
return // 没有排序条件
}
if strings.HasPrefix(orderBy, "-") {
handler(orderBy[1:], true) // 降序
} else if strings.HasPrefix(orderBy, "+") {
handler(orderBy[1:], false) // 升序
} else {
handler(orderBy, false) // 升序
}
}

View File

@@ -0,0 +1,126 @@
package query_parser
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseOrderByString(t *testing.T) {
var results []struct {
Field string
Desc bool
}
handler := func(field string, desc bool) {
results = append(results, struct {
Field string
Desc bool
}{Field: field, Desc: desc})
}
// 测试正常解析
err := ParseOrderByString("name,-age,+created_at", handler)
assert.NoError(t, err)
assert.Equal(t, 3, len(results))
assert.Equal(t, "name", results[0].Field)
assert.False(t, results[0].Desc)
assert.Equal(t, "age", results[1].Field)
assert.True(t, results[1].Desc)
assert.Equal(t, "created_at", results[2].Field)
assert.False(t, results[2].Desc)
// 测试空字符串
results = nil
err = ParseOrderByString("", handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
// 测试只有空格的字符串
results = nil
err = ParseOrderByString(" ", handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
}
func TestParseOrderByStrings(t *testing.T) {
var results []struct {
Field string
Desc bool
}
handler := func(field string, desc bool) {
results = append(results, struct {
Field string
Desc bool
}{Field: field, Desc: desc})
}
// 测试正常解析
err := ParseOrderByStrings([]string{"name", "-age", "+created_at"}, handler)
assert.NoError(t, err)
assert.Equal(t, 3, len(results))
assert.Equal(t, "name", results[0].Field)
assert.False(t, results[0].Desc)
assert.Equal(t, "age", results[1].Field)
assert.True(t, results[1].Desc)
assert.Equal(t, "created_at", results[2].Field)
assert.False(t, results[2].Desc)
// 测试空字符串数组
results = nil
err = ParseOrderByStrings([]string{}, handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
// 测试包含空字符串的数组
results = nil
err = ParseOrderByStrings([]string{"", " "}, handler)
assert.NoError(t, err)
assert.Equal(t, 0, len(results))
}
func TestParseOrderByField(t *testing.T) {
var results []struct {
Field string
Desc bool
}
handler := func(field string, desc bool) {
results = append(results, struct {
Field string
Desc bool
}{Field: field, Desc: desc})
}
// 测试升序解析
results = nil
ParseOrderByField("name", handler)
assert.Equal(t, 1, len(results))
assert.Equal(t, "name", results[0].Field)
assert.False(t, results[0].Desc)
// 测试降序解析
results = nil
ParseOrderByField("-age", handler)
assert.Equal(t, 1, len(results))
assert.Equal(t, "age", results[0].Field)
assert.True(t, results[0].Desc)
// 测试带+的升序解析
results = nil
ParseOrderByField("+created_at", handler)
assert.Equal(t, 1, len(results))
assert.Equal(t, "created_at", results[0].Field)
assert.False(t, results[0].Desc)
// 测试空字符串
results = nil
ParseOrderByField("", handler)
assert.Equal(t, 0, len(results))
// 测试只有空格的字符串
results = nil
ParseOrderByField(" ", handler)
assert.Equal(t, 0, len(results))
}

13
query_parser/utils.go Normal file
View File

@@ -0,0 +1,13 @@
package query_parser
import "net/url"
// EncodeSpecialCharacters 对字符串进行编码
func EncodeSpecialCharacters(input string) string {
return url.QueryEscape(input)
}
// DecodeSpecialCharacters 对字符串进行解码
func DecodeSpecialCharacters(input string) (string, error) {
return url.QueryUnescape(input)
}

80
stringcase/README.md Normal file
View File

@@ -0,0 +1,80 @@
# 命名法转换
- camelCase 驼峰式命名法(大驼峰)
- PascalCase 帕斯卡命名法(小驼峰)
- snake_case 蛇形命名法
- kebab-case 烤肉串命名法
## camelCase 驼峰式命名法(大驼峰)
驼峰式命名法Camel case是一种不使用空格将多个单词连起来形成一个标识符的命名方式其中每个单词的首字母除了第一个单词如果使用小驼峰式命名法都大写就像骆驼的驼峰一样。
驼峰式命名法分为两种首字母小写的“小驼峰式”lowerCamelCase和首字母大写的“大驼峰式”UpperCamelCase也称为帕斯卡命名法PascalCase
- **小驼峰式(lowerCamelCase)**: 第一个单词的首字母小写,后续单词的首字母大写。例如:`myVariableName`
- **大驼峰式(UpperCamelCase)**: 每个单词的首字母都大写。例如MyVariableName也称为帕斯卡命名法PascalCase
在 JavaScript、Java和C#中,驼峰式大小写常用于变量和函数的命名。
```javascript
let firstName = "John";
let lastName = "Doe";
function printFullName(firstName, lastName) {
let fullName = firstName + " " + lastName;
console.log(fullName);
}
```
## PascalCase 帕斯卡命名法(小驼峰)
`PascalCase`,也称为`UpperCamelCase`,是一种在编程中使用的命名约定。它要求每个单词(包括第一个单词)的首字母都大写,并且单词之间没有空格或分隔符(如
`_`)。例如,`ThisIsPascalCase``MyClassName` 都是使用PascalCase的例子。
PascalCase 通常用于在 C#、Java 和TypeScript等语言中命名类、接口和其他类型。
```typescript
class Person {
firstName: string;
lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
printFullName(): void {
let fullName = this.firstName + " " + this.lastName;
console.log(fullName);
}
}
```
## snake_case 蛇形命名法
蛇形命名法是一种使用下划线 (`_`) 分隔单词的命名方式。之所以叫蛇形命名法,是因为"`snake_case`"的
下划线的形状类似于蛇腹上的鳞片。蛇形命名法通常用于Python、Ruby 和JavaScript等语言的变量名和函数名。
```python
first_name = "John"
last_name = "Doe"
def print_full_name(first_name, last_name):
full_name = first_name + " " + last_name
print(full_name)
```
## kebab-case 烤肉串命名法
`kebab-case烤肉串命名法`,也被称作 `kebab case``dash-case破折号式``hyphen-case连字符式``lisp-caseLisp 式)`
kebab-case 要求短语内的各个单词或缩写之间以`-`(连字符)做间隔。 例如:"`kebab-case`"。
短横线命名法通常用于 URL、文件名和 HTML/CSS 类名。
```html
<div class="user-profile">
<p>This is a user profile.</p>
</div>
```

65
stringcase/camel_case.go Normal file
View File

@@ -0,0 +1,65 @@
package stringcase
import (
"strings"
"unicode"
)
func UpperCamelCase(input string) string {
return camelCase(input, true)
}
func LowerCamelCase(input string) string {
return camelCase(input, false)
}
// ToPascalCase 把字符转换为 帕斯卡命名/大驼峰命名法CamelCase
func ToPascalCase(input string) string {
return camelCase(input, true)
}
func camelCase(input string, upper bool) string {
input = strings.TrimSpace(input)
if input == "" {
return input
}
// 分割字符串
words := Split(input)
if len(words) == 0 {
return ""
}
filteredWords := make([]string, 0, len(words))
for _, word := range words {
if strings.TrimSpace(word) != "" {
filteredWords = append(filteredWords, word)
}
}
words = filteredWords
if len(words) == 0 {
return ""
}
for i, word := range words {
if word == "" {
continue
}
runes := []rune(word)
if len(runes) > 0 {
if i == 0 && !upper {
runes[0] = unicode.ToLower(runes[0]) // LowerCamelCase首单词首字母小写
} else {
runes[0] = unicode.ToUpper(runes[0]) // UpperCamelCase或后续单词首字母大写
}
for j := 1; j < len(runes); j++ {
runes[j] = unicode.ToLower(runes[j]) // 其余字母统一小写
}
words[i] = string(runes)
}
}
// 合并结果
return strings.Join(words, "")
}

View File

@@ -0,0 +1,111 @@
package stringcase
import (
"testing"
)
func TestUpperCamelCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello world", "HelloWorld"},
{"hello_world", "HelloWorld"},
{"hello-world", "HelloWorld"},
{"hello.world", "HelloWorld"},
{"helloWorld", "HelloWorld"},
{"HelloWorld", "HelloWorld"},
{"HTTPStatusCode", "HttpStatusCode"},
{"ParseURL.DoParse", "ParseUrlDoParse"},
{"ParseUrl.DoParse", "ParseUrlDoParse"},
{"parse_url.do_parse", "ParseUrlDoParse"},
{"convert space", "ConvertSpace"},
{"convert-dash", "ConvertDash"},
{"skip___multiple_underscores", "SkipMultipleUnderscores"},
{"skip multiple spaces", "SkipMultipleSpaces"},
{"skip---multiple-dashes", "SkipMultipleDashes"},
{"", ""},
{"a", "A"},
{"Z", "Z"},
{"special-characters_test", "SpecialCharactersTest"},
{"numbers123test", "Numbers123Test"},
{"hello world!", "HelloWorld"},
{"test@with#symbols", "TestWithSymbols"},
{"complexCase123!@#", "ComplexCase123"},
{"snake_case_string", "SnakeCaseString"},
{"kebab-case-string", "KebabCaseString"},
{"PascalCaseString", "PascalCaseString"},
{"camelCaseString", "CamelCaseString"},
{"HTTPRequest", "HttpRequest"},
{"user ID", "UserId"},
{"UserId", "UserId"},
{"userID", "UserId"},
{"UserID", "UserId"},
{"123NumberPrefix", "123NumberPrefix"},
{"__leading_underscores", "LeadingUnderscores"},
{"trailing_underscores__", "TrailingUnderscores"},
{"multiple___underscores", "MultipleUnderscores"},
{" spaces around ", "SpacesAround"},
}
for _, test := range tests {
result := UpperCamelCase(test.input)
if result != test.expected {
t.Errorf("UpperCamelCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}
func TestLowerCamelCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello world", "helloWorld"},
{"hello_world", "helloWorld"},
{"hello-world", "helloWorld"},
{"hello.world", "helloWorld"},
{"helloWorld", "helloWorld"},
{"HelloWorld", "helloWorld"},
{"HTTPStatusCode", "httpStatusCode"},
{"ParseURL.DoParse", "parseUrlDoParse"},
{"ParseUrl.DoParse", "parseUrlDoParse"},
{"parse_url.do_parse", "parseUrlDoParse"},
{"convert space", "convertSpace"},
{"convert-dash", "convertDash"},
{"skip___multiple_underscores", "skipMultipleUnderscores"},
{"skip multiple spaces", "skipMultipleSpaces"},
{"skip---multiple-dashes", "skipMultipleDashes"},
{"", ""},
{"a", "a"},
{"Z", "z"},
{"special-characters_test", "specialCharactersTest"},
{"numbers123test", "numbers123Test"},
{"hello world!", "helloWorld"},
{"test@with#symbols", "testWithSymbols"},
{"complexCase123!@#", "complexCase123"},
{"snake_case_string", "snakeCaseString"},
{"kebab-case-string", "kebabCaseString"},
{"PascalCaseString", "pascalCaseString"},
{"camelCaseString", "camelCaseString"},
{"HTTPRequest", "httpRequest"},
{"user ID", "userId"},
{"UserId", "userId"},
{"userID", "userId"},
{"UserID", "userId"},
{"123NumberPrefix", "123NumberPrefix"},
{"__leading_underscores", "leadingUnderscores"},
{"trailing_underscores__", "trailingUnderscores"},
{"multiple___underscores", "multipleUnderscores"},
{" spaces around ", "spacesAround"},
}
for _, test := range tests {
result := LowerCamelCase(test.input)
if result != test.expected {
t.Errorf("LowerCamelCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}

9
stringcase/kebab_case.go Normal file
View File

@@ -0,0 +1,9 @@
package stringcase
func KebabCase(s string) string {
return delimiterCase(s, '-', false)
}
func UpperKebabCase(s string) string {
return delimiterCase(s, '-', true)
}

View File

@@ -0,0 +1,49 @@
package stringcase
import "testing"
func TestKebabCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"HelloWorld", "hello-world"},
{"helloWorld", "hello-world"},
{"Hello World", "hello-world"},
{"hello world!", "hello-world"},
{"Numbers123Test", "numbers-123-test"},
{"", ""},
{"_", ""},
{"__Hello__World__", "hello-world"},
}
for _, test := range tests {
result := KebabCase(test.input)
if result != test.expected {
t.Errorf("KebabCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}
func TestUpperKebabCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"HelloWorld", "HELLO-WORLD"},
{"helloWorld", "HELLO-WORLD"},
{"Hello World", "HELLO-WORLD"},
{"hello world!", "HELLO-WORLD"},
{"Numbers123Test", "NUMBERS-123-TEST"},
{"", ""},
{"_", ""},
{"__Hello__World__", "HELLO-WORLD"},
}
for _, test := range tests {
result := UpperKebabCase(test.input)
if result != test.expected {
t.Errorf("UpperKebabCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}

50
stringcase/snake_case.go Normal file
View File

@@ -0,0 +1,50 @@
package stringcase
import (
"strings"
)
// ToSnakeCase 把字符转换为 蛇形命名法snake_case
func ToSnakeCase(input string) string {
return SnakeCase(input)
}
func SnakeCase(s string) string {
return delimiterCase(s, '_', false)
}
func UpperSnakeCase(s string) string {
return delimiterCase(s, '_', true)
}
func delimiterCase(input string, delimiter rune, upperCase bool) string {
input = strings.TrimSpace(input)
if input == "" {
return input
}
// 使用 Split 分割字符串
words := Split(input)
filteredWords := make([]string, 0, len(words))
for _, word := range words {
if strings.TrimSpace(word) != "" {
filteredWords = append(filteredWords, word)
}
}
adjustCase := toLower
if upperCase {
adjustCase = toUpper
}
for i, word := range filteredWords {
runes := []rune(word)
for j := 0; j < len(runes); j++ {
runes[j] = adjustCase(runes[j])
}
filteredWords[i] = string(runes)
}
// 使用分隔符连接结果
return strings.Join(filteredWords, string(delimiter))
}

View File

@@ -0,0 +1,85 @@
package stringcase
import (
"testing"
)
func TestToSnakeCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"snake_case", "snake_case"},
{"CamelCase", "camel_case"},
{"lowerCamelCase", "lower_camel_case"},
{"F", "f"},
{"Foo", "foo"},
{"FooB", "foo_b"},
{"FooID", "foo_id"},
{" FooBar\t", "foo_bar"},
{"HTTPStatusCode", "http_status_code"},
{"ParseURL.DoParse", "parse_url_do_parse"},
{"Convert Space", "convert_space"},
{"Convert-dash", "convert_dash"},
{"Skip___MultipleUnderscores", "skip_multiple_underscores"},
{"Skip MultipleSpaces", "skip_multiple_spaces"},
{"Skip---MultipleDashes", "skip_multiple_dashes"},
{"Hello World", "hello_world"},
{"Multiple Words Example", "multiple_words_example"},
{"", ""},
{"A", "a"},
{"z", "z"},
{"Special-Characters_Test", "special_characters_test"},
{"Numbers123Test", "numbers_123_test"},
{"Hello World!", "hello_world"},
{"Test@With#Symbols", "test_with_symbols"},
{"ComplexCase123!@#", "complex_case_123"},
}
for _, test := range tests {
result := ToSnakeCase(test.input)
if result != test.expected {
t.Errorf("ToSnakeCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}
func TestUpperSnakeCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"snake_case", "SNAKE_CASE"},
{"CamelCase", "CAMEL_CASE"},
{"lowerCamelCase", "LOWER_CAMEL_CASE"},
{"F", "F"},
{"Foo", "FOO"},
{"FooB", "FOO_B"},
{"FooID", "FOO_ID"},
{" FooBar\t", "FOO_BAR"},
{"HTTPStatusCode", "HTTP_STATUS_CODE"},
{"ParseURL.DoParse", "PARSE_URL_DO_PARSE"},
{"Convert Space", "CONVERT_SPACE"},
{"Convert-dash", "CONVERT_DASH"},
{"Skip___MultipleUnderscores", "SKIP_MULTIPLE_UNDERSCORES"},
{"Skip MultipleSpaces", "SKIP_MULTIPLE_SPACES"},
{"Skip---MultipleDashes", "SKIP_MULTIPLE_DASHES"},
{"Hello World", "HELLO_WORLD"},
{"Multiple Words Example", "MULTIPLE_WORDS_EXAMPLE"},
{"", ""},
{"A", "A"},
{"z", "Z"},
{"Special-Characters_Test", "SPECIAL_CHARACTERS_TEST"},
{"Numbers123Test", "NUMBERS_123_TEST"},
{"Hello World!", "HELLO_WORLD"},
{"Test@With#Symbols", "TEST_WITH_SYMBOLS"},
{"ComplexCase123!@#", "COMPLEX_CASE_123"},
}
for _, test := range tests {
result := UpperSnakeCase(test.input)
if result != test.expected {
t.Errorf("UpperSnakeCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}

144
stringcase/split.go Normal file
View File

@@ -0,0 +1,144 @@
package stringcase
import (
"strings"
"unicode"
"unicode/utf8"
)
type runeInfo struct {
r rune
}
// Checks whether or not the rune represented by rInfo is a digit.
func (rInfo *runeInfo) isDigit() bool {
return unicode.IsDigit(rInfo.r)
}
// Checks whether or not the rune represented by rInfo is an uppercase rune.
func (rInfo *runeInfo) isUppercase() bool {
return unicode.IsUpper(rInfo.r)
}
// A reader designed for reading "CamelCase" strings.
type rdr struct {
input string // The data this reader operates on.
pos int // The position of this reader.
hasNextRune bool // A flag indicating if there's a next rune.
rdRune runeInfo // Information about the last rune that was read.
nxtRune runeInfo // Information about the next rune that's about to be read.
}
// Read the next rune from r.
func (r *rdr) readRune() {
r.rdRune = runeInfo{rune(r.input[r.pos])}
r.pos = r.pos + 1
r.hasNextRune = r.pos < len(r.input)
if r.hasNextRune {
r.nxtRune = runeInfo{rune(r.input[r.pos])}
}
}
// Undo the last rune from r.
func (r *rdr) unreadRune() {
r.pos = r.pos - 1
r.nxtRune = r.rdRune
r.rdRune = runeInfo{rune(r.input[r.pos])}
r.hasNextRune = true // NOTE: An undo operation means that there will be always a next rune.
}
// Verify if the word that's currently read by r is a word that should NOT be split.
// If noSplit contains a word that starts with the word that's currently read by r, this function returns true, false
// otherwise.
func (r *rdr) isNoSplitWord(sIdx int, noSplit []string) bool {
return ContainsFn(noSplit, r.input[sIdx:r.pos+1], func(got, want string) bool {
return strings.HasPrefix(got, want)
})
}
// Read the next part from r.
// Each word in noSplit (if provided) is treated as a word that shouldn't be split.
func (r *rdr) readNextPart(noSplit []string) string {
sIdx := r.pos
r.readRune()
if r.rdRune.isDigit() {
return r.readNumber(sIdx, noSplit)
}
return r.readWord(sIdx, noSplit)
}
// Read and return a number from r.
func (r *rdr) readNumber(sIdx int, noSplit []string) string {
if r.hasNextRune && r.nxtRune.isDigit() {
for r.hasNextRune && (r.nxtRune.isDigit() || r.isNoSplitWord(sIdx, noSplit)) {
r.readRune()
}
return r.input[sIdx:r.pos]
}
return r.input[sIdx:r.pos]
}
// Read and return a word from r.
func (r *rdr) readWord(sIdx int, noSplit []string) string {
if r.hasNextRune && r.nxtRune.isUppercase() {
for r.hasNextRune && (r.nxtRune.isUppercase() || r.isNoSplitWord(sIdx, noSplit)) {
r.readRune()
}
if r.hasNextRune && (!r.nxtRune.isUppercase() && !r.nxtRune.isDigit()) {
r.unreadRune()
}
return r.input[sIdx:r.pos]
}
for r.hasNextRune && (r.isNoSplitWord(sIdx, noSplit) || (!r.nxtRune.isUppercase() && !r.nxtRune.isDigit())) {
r.readRune()
}
return r.input[sIdx:r.pos]
}
// Split reads v treating it as a "CamelCase" and returns the different words.
// If v isn't a valid UTF-8 string, or when v is an empty string, a slice with one element (v) is returned.
// Each word in noSplit (if provided) is treated as a word that shouldn't be split.
func Split(input string, noSplit ...string) []string {
if !utf8.ValidString(input) || len(input) == 0 {
return []string{input}
}
output := make([]string, 0)
inputs := SplitByNonAlphanumeric(input)
for _, v := range inputs {
v = strings.TrimSpace(v)
if v == "" {
continue
}
output = append(output, split(v, noSplit...)...)
}
return output
}
func split(input string, noSplit ...string) []string {
if !utf8.ValidString(input) || len(input) == 0 {
return []string{input}
}
vRdr := &rdr{input: input}
output := make([]string, 0)
for vRdr.pos < len(input) {
part := vRdr.readNextPart(noSplit)
output = append(output, part)
}
return output
}

82
stringcase/split_test.go Normal file
View File

@@ -0,0 +1,82 @@
package stringcase
import (
"testing"
)
func TestSplitSingle(t *testing.T) {
input := "URL.DoParse"
result := Split(input)
t.Logf("Split(%q) = %q;", input, result)
t.Log(input[:7])
}
func TestSplit(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{"hello world", []string{"hello", "world"}},
{"hello_world", []string{"hello", "world"}},
{"hello-world", []string{"hello", "world"}},
{"hello.world", []string{"hello", "world"}},
{"helloWorld", []string{"hello", "World"}},
{"HelloWorld", []string{"Hello", "World"}},
{"HTTPStatusCode", []string{"HTTP", "Status", "Code"}},
{"ParseURLDoParse", []string{"Parse", "URL", "Do", "Parse"}},
{"ParseUrlDoParse", []string{"Parse", "Url", "Do", "Parse"}},
{"ParseUrl.DoParse", []string{"Parse", "Url", "Do", "Parse"}},
{"ParseURL.DoParse", []string{"Parse", "URL", "Do", "Parse"}},
{"ParseURL", []string{"Parse", "URL"}},
{"ParseURL.", []string{"Parse", "URL"}},
{"parse_url.do_parse", []string{"parse", "url", "do", "parse"}},
{"convert space", []string{"convert", "space"}},
{"convert-dash", []string{"convert", "dash"}},
{"skip___multiple_underscores", []string{"skip", "multiple", "underscores"}},
{"skip multiple spaces", []string{"skip", "multiple", "spaces"}},
{"skip---multiple-dashes", []string{"skip", "multiple", "dashes"}},
{"", []string{""}},
{"a", []string{"a"}},
{"Z", []string{"Z"}},
{"special-characters_test", []string{"special", "characters", "test"}},
{"numbers123test", []string{"numbers", "123", "test"}},
{"hello world!", []string{"hello", "world"}},
{"test@with#symbols", []string{"test", "with", "symbols"}},
{"complexCase123!@#", []string{"complex", "Case", "123"}},
{"snake_case_string", []string{"snake", "case", "string"}},
{"kebab-case-string", []string{"kebab", "case", "string"}},
{"PascalCaseString", []string{"Pascal", "Case", "String"}},
{"camelCaseString", []string{"camel", "Case", "String"}},
{"HTTPRequest", []string{"HTTP", "Request"}},
{"user ID", []string{"user", "ID"}},
{"UserId", []string{"User", "Id"}},
{"userID", []string{"user", "ID"}},
{"UserID", []string{"User", "ID"}},
{"123NumberPrefix", []string{"123", "Number", "Prefix"}},
{"__leading_underscores", []string{"leading", "underscores"}},
{"trailing_underscores__", []string{"trailing", "underscores"}},
{"multiple___underscores", []string{"multiple", "underscores"}},
{" spaces around ", []string{"spaces", "around"}},
}
for _, test := range tests {
result := Split(test.input)
if !compareStringSlices(result, test.expected) {
t.Errorf("Split(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}
func compareStringSlices(slice1, slice2 []string) bool {
if len(slice1) != len(slice2) {
return false
}
for i := range slice1 {
if slice1[i] != slice2[i] {
return false
}
}
return true
}

View File

@@ -1,119 +0,0 @@
package stringcase
import (
"strings"
"unicode"
)
// ToSnakeCase 把字符转换为 蛇形命名法snake_case
func ToSnakeCase(input string) string {
if input == "" {
return input
}
if len(input) == 1 {
return strings.ToLower(input)
}
input = strings.Replace(input, " ", "", -1)
source := []rune(input)
dist := strings.Builder{}
dist.Grow(len(input) + len(input)/3) // avoid reallocation memory, 33% ~ 50% is recommended
skipNext := false
for i := 0; i < len(source); i++ {
cur := source[i]
switch cur {
case '-', '_':
dist.WriteRune('_')
skipNext = true
continue
}
if unicode.IsLower(cur) || unicode.IsDigit(cur) {
dist.WriteRune(cur)
continue
}
if i == 0 {
dist.WriteRune(unicode.ToLower(cur))
continue
}
last := source[i-1]
if (!unicode.IsLetter(last)) || unicode.IsLower(last) {
if skipNext {
skipNext = false
} else {
dist.WriteRune('_')
}
dist.WriteRune(unicode.ToLower(cur))
continue
}
// last is upper case
if i < len(source)-1 {
next := source[i+1]
if unicode.IsLower(next) {
if skipNext {
skipNext = false
} else {
dist.WriteRune('_')
}
dist.WriteRune(unicode.ToLower(cur))
continue
}
}
dist.WriteRune(unicode.ToLower(cur))
}
return dist.String()
}
// ToPascalCase 把字符转换为 帕斯卡命名/大驼峰命名法CamelCase
func ToPascalCase(input string) string {
return toCamelCase(input, true)
}
// ToLowCamelCase 把字符转换为 小驼峰命名法lowerCamelCase
func ToLowCamelCase(input string) string {
return toCamelCase(input, false)
}
func toCamelCase(s string, initCase bool) string {
s = strings.TrimSpace(s)
if s == "" {
return s
}
var uppercaseAcronym = map[string]string{}
if a, ok := uppercaseAcronym[s]; ok {
s = a
}
n := strings.Builder{}
n.Grow(len(s))
capNext := initCase
for i, v := range []byte(s) {
vIsCap := v >= 'A' && v <= 'Z'
vIsLow := v >= 'a' && v <= 'z'
if capNext {
if vIsLow {
v += 'A'
v -= 'a'
}
} else if i == 0 {
if vIsCap {
v += 'a'
v -= 'A'
}
}
if vIsCap || vIsLow {
n.WriteByte(v)
capNext = false
} else if vIsNum := v >= '0' && v <= '9'; vIsNum {
n.WriteByte(v)
capNext = true
} else {
capNext = v == '_' || v == ' ' || v == '-' || v == '.'
}
}
return n.String()
}

View File

@@ -1,24 +0,0 @@
package stringcase
import (
"fmt"
"testing"
)
func TestToLowCamelCase(t *testing.T) {
fmt.Println(ToLowCamelCase("snake_case"))
fmt.Println(ToLowCamelCase("CamelCase"))
fmt.Println(ToLowCamelCase("lowerCamelCase"))
}
func TestToPascalCase(t *testing.T) {
fmt.Println(ToPascalCase("snake_case"))
fmt.Println(ToPascalCase("CamelCase"))
fmt.Println(ToPascalCase("lowerCamelCase"))
}
func TestToSnakeCase(t *testing.T) {
fmt.Println(ToSnakeCase("snake_case"))
fmt.Println(ToSnakeCase("CamelCase"))
fmt.Println(ToSnakeCase("lowerCamelCase"))
}

132
stringcase/utils.go Normal file
View File

@@ -0,0 +1,132 @@
package stringcase
import (
"regexp"
"strings"
"unicode"
)
func isLower(ch rune) bool {
return ch >= 'a' && ch <= 'z'
}
func toLower(ch rune) rune {
if ch >= 'A' && ch <= 'Z' {
return ch + 32
}
return ch
}
func isUpper(ch rune) bool {
return ch >= 'A' && ch <= 'Z'
}
func toUpper(ch rune) rune {
if ch >= 'a' && ch <= 'z' {
return ch - 32
}
return ch
}
func isSpace(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}
func isDigit(ch rune) bool {
return ch >= '0' && ch <= '9'
}
func isDelimiter(ch rune) bool {
return ch == '-' || ch == '_' || isSpace(ch)
}
type iterFunc func(prev, curr, next rune)
func stringIter(s string, callback iterFunc) {
var prev rune
var curr rune
for _, next := range s {
if curr == 0 {
prev = curr
curr = next
continue
}
callback(prev, curr, next)
prev = curr
curr = next
}
if len(s) > 0 {
callback(prev, curr, 0)
}
}
func isAlpha(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}
func ReplaceNonAlphanumeric(s string, replacement string) string {
if replacement == "" {
replacement = "_"
}
// 使用正则表达式匹配非英文字母和数字的字符
re := regexp.MustCompile("[^a-zA-Z0-9]+")
// 替换为指定字符
return re.ReplaceAllString(s, replacement)
}
func SplitByNonAlphanumeric(input string) []string {
var builder strings.Builder
for _, r := range input {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
builder.WriteRune(r)
} else {
builder.WriteRune(' ') // 将非英文字符和数字的字符替换为空格
}
}
processedInput := builder.String()
return strings.Fields(processedInput) // 使用空格分割字符串
}
func SplitAndKeepDelimiters(input string) []string {
var result []string
var builder strings.Builder
for _, r := range input {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
builder.WriteRune(r)
} else {
if builder.Len() > 0 {
result = append(result, builder.String())
builder.Reset()
}
result = append(result, string(r)) // 保留分隔符
}
}
if builder.Len() > 0 {
result = append(result, builder.String())
}
return result
}
func ContainsFn[T any](slice []T, value T, predicate func(got, want T) bool) bool {
for _, item := range slice {
if predicate(item, value) {
return true
}
}
return false
}
func isUpperCaseWord(word string) bool {
for _, r := range word {
if !unicode.IsUpper(r) {
return false
}
}
return true
}

52
stringcase/utils_test.go Normal file
View File

@@ -0,0 +1,52 @@
package stringcase
import (
"reflect"
"testing"
)
func TestSplitByNonAlphanumeric(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{"hello-world", []string{"hello", "world"}},
{"hello_world", []string{"hello", "world"}},
{"hello.world", []string{"hello", "world"}},
{"hello world", []string{"hello", "world"}},
{"hello123world", []string{"hello123world"}},
{"hello123 world", []string{"hello123", "world"}},
{"hello-world_123", []string{"hello", "world", "123"}},
{"!hello@world#", []string{"hello", "world"}},
}
for _, test := range tests {
result := SplitByNonAlphanumeric(test.input)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("SplitByNonAlphanumeric(%q) = %v; expected %v", test.input, result, test.expected)
}
}
}
func TestSplitAndKeepDelimiters(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{"hello-world", []string{"hello", "-", "world"}},
{"hello_world", []string{"hello", "_", "world"}},
{"hello.world", []string{"hello", ".", "world"}},
{"hello world", []string{"hello", " ", "world"}},
{"hello123world", []string{"hello123world"}},
{"hello123 world", []string{"hello123", " ", "world"}},
{"hello-world_123", []string{"hello", "-", "world", "_", "123"}},
{"!hello@world#", []string{"!", "hello", "@", "world", "#"}},
}
for _, test := range tests {
result := SplitAndKeepDelimiters(test.input)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("SplitAndKeepDelimiters(%q) = %v; expected %v", test.input, result, test.expected)
}
}
}

View File

@@ -1,4 +1,4 @@
git tag v1.1.28
git tag v1.1.29
git tag bank_card/v1.1.5
git tag geoip/v1.1.5
@@ -8,8 +8,11 @@ git tag jwtutil/v0.0.2
git tag id/v0.0.2
git tag slug/v0.0.1
git tag name_generator/v0.0.1
git tag mapper/v0.0.3
git tag password/v0.0.1
git tag query_parser/v0.0.2
git tag entgo/v1.1.31
git tag entgo/v1.1.32
git tag gorm/v1.1.6
git push origin --tags

View File

@@ -296,3 +296,67 @@ func DurationpbToString(in *durationpb.Duration) *string {
return trans.Ptr(in.AsDuration().String())
}
// TimestampToSeconds 将 timestamppb.Timestamp 转换为秒
func TimestampToSeconds(ts *timestamppb.Timestamp) int64 {
if ts == nil {
return 0
}
return ts.AsTime().Unix()
}
// SecondsToTimestamp 将秒转换为 timestamppb.Timestamp
func SecondsToTimestamp(seconds *int64) *timestamppb.Timestamp {
if seconds == nil {
return nil
}
return timestamppb.New(time.Unix(*seconds, 0))
}
func TimestampToMilliseconds(ts *timestamppb.Timestamp) int64 {
if ts == nil {
return 0
}
return ts.AsTime().UnixMilli()
}
func MillisecondsToTimestamp(milliseconds *int64) *timestamppb.Timestamp {
if milliseconds == nil {
return nil
}
return timestamppb.New(time.UnixMilli(*milliseconds))
}
func TimestampToMicroseconds(ts *timestamppb.Timestamp) int64 {
if ts == nil {
return 0
}
return ts.AsTime().UnixMicro()
}
func MicrosecondsToTimestamp(microseconds *int64) *timestamppb.Timestamp {
if microseconds == nil {
return nil
}
return timestamppb.New(time.UnixMicro(*microseconds))
}
// TimestampToNanoseconds 将 timestamppb.Timestamp 转换为纳秒
func TimestampToNanoseconds(ts *timestamppb.Timestamp) int64 {
if ts == nil {
return 0
}
return ts.AsTime().UnixNano()
}
// NanosecondsToTimestamp 将纳秒转换为 timestamppb.Timestamp
func NanosecondsToTimestamp(nanoseconds *int64) *timestamppb.Timestamp {
if nanoseconds == nil {
return nil
}
return timestamppb.New(time.Unix(0, *nanoseconds))
}

View File

@@ -442,3 +442,29 @@ func TestDurationpbToString(t *testing.T) {
result = DurationpbToString(nil)
assert.Nil(t, result)
}
func TestTimestampConversions(t *testing.T) {
// 测试秒转换
seconds := int64(1672531200)
ts := SecondsToTimestamp(&seconds)
assert.NotNil(t, ts)
assert.Equal(t, seconds, TimestampToSeconds(ts))
// 测试毫秒转换
milliseconds := int64(1672531200123)
ts = MillisecondsToTimestamp(&milliseconds)
assert.NotNil(t, ts)
assert.Equal(t, milliseconds, TimestampToMilliseconds(ts))
// 测试微秒转换
microseconds := int64(1672531200123456)
ts = MicrosecondsToTimestamp(&microseconds)
assert.NotNil(t, ts)
assert.Equal(t, microseconds, TimestampToMicroseconds(ts))
// 测试纳秒转换
nanoseconds := int64(1672531200123456789)
ts = NanosecondsToTimestamp(&nanoseconds)
assert.NotNil(t, ts)
assert.Equal(t, nanoseconds, TimestampToNanoseconds(ts))
}