tada(*): 🎉 基础库功能

main
熊二 1 year ago
commit 815fbd568f
  1. 40
      .env.example
  2. 7
      .gitignore
  3. 47
      README.md
  4. 9
      TODOs.md
  5. 60
      go.mod
  6. 230
      go.sum
  7. 24
      internal/entities/company.go
  8. 37
      internal/entities/company_department.go
  9. 26
      internal/entities/company_staff.go
  10. 24
      internal/entities/config.go
  11. 21
      internal/entities/config_group.go
  12. 30
      internal/entities/feature.go
  13. 24
      internal/entities/feature_category.go
  14. 21
      internal/entities/feature_config.go
  15. 26
      internal/entities/feature_content.go
  16. 24
      internal/entities/feature_content_chapter.go
  17. 28
      internal/entities/feature_content_detail.go
  18. 25
      internal/entities/resource.go
  19. 23
      internal/entities/resource_category.go
  20. 19
      internal/entities/system_log.go
  21. 23
      internal/entities/system_menu.go
  22. 19
      internal/entities/system_permission.go
  23. 20
      internal/entities/system_role.go
  24. 12
      internal/entities/system_role_power.go
  25. 20
      internal/entities/system_user.go
  26. 53
      internal/init.go
  27. 134
      internal/middleware/cors.go
  28. 140
      internal/middleware/jwt.go
  29. 74
      internal/middleware/key_auth.go
  30. 63
      internal/middleware/logger.go
  31. 24
      internal/middleware/middleware.go
  32. 268
      internal/middleware/rate_limiter.go
  33. 135
      internal/middleware/recover.go
  34. 19
      internal/repositories/company.go
  35. 19
      internal/repositories/company_department.go
  36. 19
      internal/repositories/company_staff.go
  37. 19
      internal/repositories/config.go
  38. 19
      internal/repositories/config_group.go
  39. 19
      internal/repositories/feature.go
  40. 19
      internal/repositories/feature_category.go
  41. 19
      internal/repositories/feature_config.go
  42. 19
      internal/repositories/feature_content.go
  43. 19
      internal/repositories/feature_content_chapter.go
  44. 19
      internal/repositories/feature_content_detail.go
  45. 27
      internal/repositories/ioc.go
  46. 19
      internal/repositories/resource.go
  47. 19
      internal/repositories/resource_category.go
  48. 19
      internal/repositories/system_log.go
  49. 19
      internal/repositories/system_menu.go
  50. 19
      internal/repositories/system_permission.go
  51. 19
      internal/repositories/system_role.go
  52. 19
      internal/repositories/system_role_power.go
  53. 19
      internal/repositories/system_user.go
  54. 7
      internal/util/echo_context.go
  55. 201
      internal/util/echo_logger.go
  56. 88
      main.go
  57. 17
      pkg/bus/bus.go
  58. 141
      pkg/bus/emitter.go
  59. 17
      pkg/bus/event.go
  60. 33
      pkg/bus/listener.go
  61. 174
      pkg/cast/cast.go
  62. 311
      pkg/db/db.go
  63. 18
      pkg/db/delete_builder.go
  64. 174
      pkg/db/expr.go
  65. 57
      pkg/db/log.go
  66. 67
      pkg/db/logger.go
  67. 1
      pkg/db/pager.go
  68. 191
      pkg/db/query_builder.go
  69. 1
      pkg/db/repo.go
  70. 98
      pkg/db/repository.go
  71. 67
      pkg/db/types.go
  72. 66
      pkg/db/update_builder.go
  73. 153
      pkg/env/env.go
  74. 186
      pkg/env/environ.go
  75. 228
      pkg/ioc/container.go
  76. 97
      pkg/ioc/ioc.go
  77. 19
      pkg/ioc/util.go
  78. 80
      pkg/log/attr.go
  79. 71
      pkg/log/colorer.go
  80. 164
      pkg/log/handler.go
  81. 62
      pkg/log/level.go
  82. 97
      pkg/log/log.go
  83. 273
      pkg/log/logger.go
  84. 77
      pkg/log/util.go
  85. 169
      pkg/log/writer.go
  86. 67
      pkg/logs/attr.go
  87. 44
      pkg/logs/color.go
  88. 1
      pkg/logs/handler.go
  89. 59
      pkg/logs/level.go
  90. 38
      pkg/logs/log.go
  91. 512
      pkg/logs/logger.go
  92. 13
      pkg/misc/clamp.go
  93. 14
      pkg/misc/fallback.go
  94. 26
      pkg/misc/key.go
  95. 45
      pkg/misc/result.go
  96. 19
      pkg/misc/zero.go
  97. 171
      pkg/rsp/accept.go
  98. 102
      pkg/rsp/error.go
  99. 255
      pkg/rsp/rsp.go
  100. 72
      pkg/rsp/sse.go
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,40 @@
################
# 应用基础配置
################
# 设置运行环境,开设置的值有 dev、test、prod
APP_ENV=dev
# 配置时区(预留配置,暂未生效)
APP_TIMEZONE=
################
# 数据库配置
################
# 数据库驱动,支持 sqlite3、mysql、sqlserver、pgsql
DB_DRIVER=
# 数据库连接配置
DB_DSN=
# 数据表前缀
DB_PREFIX=
# MySQL存储引擎
DB_STORE_ENGINE=
# 最大闲置连接数
DB_MAX_IDLE_CONNS=
# 最大打开连接数
DB_MAX_OPEN_CONNS=
# 连接最大有效时长
DB_CONN_MAX_LIFETIME=
# 是不是代码优先模式,只有开启此选项后才能够
# 使用自动迁移功能同步表结构到数据库
DB_CODE_FIRST=
################
# 授权认证
################
JWT_PRIVATE_KEY=
JWT_PUBLIC_KEY=

7
.gitignore vendored

@ -0,0 +1,7 @@
/.idea
/*.iml
*.db
.env
.env.*
!.env.example
/docs

@ -0,0 +1,47 @@
## 开发接口文档
> [swag 文档地址](https://github.com/swaggo/swag)
生成开发文档命令
```shell
swag init
```
注释文档格式化命令
```shell
swag fmt
```
## 关于 Api
参考 Restful 设计风格。
### 请求方法指南
> 五个常用的 HTTP 动词,用于表示对于资源的具体操作类型:
>
> * GET(SELECT):从服务器取出资源(一项或多项)。
> * PUT(CREATE):在服务器新建一个资源。
> * POST(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
> * PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
> * DELETE(DELETE):从服务器删除资源。
>
> 还有两个不常用的HTTP动词:
>
> * HEAD:获取资源的元数据。
> * OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
### 分页查询
接口应该提供如下参数过滤返回结果:
- **sort_by** 排序字段列表,可以为字段指定如下前缀实现排序:
- `+` 表示查询结果按该字段的升序进行排序,为默认前缀,可省略;
- `-` 表示查询结果按该字段的降序进行排序。
- **page** 页码,可选,默认值为 1
- **per_page** 页容量,可选,默认值为 30
客户端通过 QueryString 方式提交上述参数来分页查询。

@ -0,0 +1,9 @@
## 实现 env 包
https://github.com/golobby/env/blob/master/env.go
## 支持 HTTP2
https://www.modb.pro/db/87148
## API文档
https://golang2.eddycjy.com/posts/ch2/04-api-doc/

@ -0,0 +1,60 @@
module sorbet
go 1.21
require (
github.com/go-resty/resty/v2 v2.7.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/joho/godotenv v1.5.1
github.com/labstack/echo-jwt/v4 v4.2.0
github.com/labstack/echo/v4 v4.11.1
github.com/labstack/gommon v0.4.0
github.com/mattn/go-isatty v0.0.19
github.com/mattn/go-sqlite3 v1.14.17
github.com/mitchellh/mapstructure v1.5.0
github.com/rs/xid v1.5.0
github.com/swaggo/echo-swagger v1.4.1
github.com/swaggo/swag v1.16.2
golang.org/x/time v0.3.0
gorm.io/driver/mysql v1.5.1
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.5.3
gorm.io/driver/sqlserver v1.5.1
gorm.io/gorm v1.25.4
gorm.io/plugin/optimisticlock v1.1.1
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/microsoft/go-mssqldb v1.1.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.7.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

230
go.sum

@ -0,0 +1,230 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/microsoft/go-mssqldb v1.1.0 h1:jsV+tpvcPTbNNKW0o3kiCD69kOHICsfjZ2VcVu2lKYc=
github.com/microsoft/go-mssqldb v1.1.0/go.mod h1:LzkFdl4z2Ck+Hi+ycGOTbL56VEfgoyA2DvYejrNGbRk=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
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/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/driver/sqlserver v1.5.1 h1:wpyW/pR26U94uaujltiFGXY7fd2Jw5hC9PB1ZF/Y5s4=
gorm.io/driver/sqlserver v1.5.1/go.mod h1:AYHzzte2msKTmYBYsSIq8ZUsznLJwBdkB2wpI+kt0nM=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/plugin/optimisticlock v1.1.1 h1:REWF26BNTIcLpgzp34EW1Mi9bPZpthBcwjBkOYINn5Q=
gorm.io/plugin/optimisticlock v1.1.1/go.mod h1:wFWgM/KsGEg+IoxgZAAVBP4OmaPfj337L/+T4AR6/hI=

@ -0,0 +1,24 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// Company 公司表
type Company struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:公司编号"`
PrincipalID *uint `json:"principal_id" xml:"principal_id" gorm:"comment:负责人编号(员工)"`
Name string `json:"name" xml:"name" gorm:"size:25;not null;uniqueIndex;comment:公司名称"`
Logo string `json:"logo" xml:"logo" gorm:"not null;comment:形象徽标"`
Status bool `json:"status" xml:"status" gorm:"comment:状态"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Principal *CompanyStaff `json:"principal" xml:"principal"`
Staffs []*CompanyStaff `json:"staffs" xml:"staffs"`
Departments []*CompanyDepartment `json:"departments" xml:"departments"`
}

@ -0,0 +1,37 @@
package entities
import (
"errors"
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
"unicode/utf8"
)
// CompanyDepartment 公司部门表
type CompanyDepartment struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:部门编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级部门编号"`
CompanyID uint `json:"company_id" xml:"company_id" gorm:"comment:所属公司编号"`
PrincipalID *uint `json:"principal_id" xml:"principal_id" gorm:"comment:负责人编号(员工)"`
Name string `json:"name" xml:"name" gorm:"size:50;not null;comment:部门名称"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:展示排序"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Staffs []*CompanyStaff `json:"staffs" xml:"staffs" gorm:"many2many:company_staff_to_department_relations"`
Courses []*FeatureContent `json:"courses" xml:"courses" gorm:"many2many:company_course_to_department_relations"`
Children []*CompanyDepartment `json:"children" xml:"children" gorm:"foreignKey:PID"`
}
func (c *CompanyDepartment) BeforeCreate(tx *gorm.DB) error {
if c.CompanyID == 0 {
return errors.New("缺少所属公司编号")
}
if utf8.RuneCountInString(c.Name) < 6 {
return errors.New("部门名称至少两个字")
}
return nil
}

@ -0,0 +1,26 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// CompanyStaff 公司员工表
type CompanyStaff struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:员工编号"`
CompanyID uint `json:"company_id" xml:"company_id" gorm:"comment:所属公司编号"`
Name string `json:"name" xml:"name" gorm:"size:20;not null;comment:员工姓名"`
Gender string `json:"gender" xml:"gender" gorm:"not null;default:'unknown';check:gender IN('female','male','unknown');comment:员工性别"`
Position string `json:"position" xml:"position" gorm:"size:100;not null;comment:员工职务"`
PhoneNumber string `json:"phone_number" xml:"phone_number" gorm:"size:11;not null;comment:手机号码"`
WechatOpenid string `json:"wechat_openid" xml:"wechat_openid" gorm:"size:100;not null;comment:微信号"`
WithoutStudy bool `json:"without_study" xml:"without_study" gorm:"comment:是否可以不用学习"`
IsAdmin bool `json:"is_admin" xml:"is_admin" gorm:"comment:是否管理员"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Departments []*CompanyDepartment `json:"departments" xml:"departments" gorm:"many2many:company_staff_to_department_relations"`
}

@ -0,0 +1,24 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// Config 配置表
type Config struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:配置编号"`
GroupID uint `json:"group_id" xml:"group_id" gorm:"comment:所属配置组编号"`
Name string `json:"name" xml:"name" gorm:"size:30;not null;comment:配置名称"`
Title string `json:"title" xml:"title" gorm:"size:50;not null;comment:配置标题"`
Description string `json:"description" xml:"description" gorm:"size:100;not null;comment:配置描述"`
DataType string `json:"data_type" xml:"data_type" gorm:"size:10;not null;comment:数据类型"`
Attributes map[string]any `json:"attributes" xml:"attributes" gorm:"serializer:json;comment:相关属性值"`
Value any `json:"value" xml:"value" gorm:"serializer:json;not null;comment:配置值"`
Sort int32 `json:"sort" xml:"sort" gorm:"size:4;default:0;comment:排序"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
}

@ -0,0 +1,21 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// ConfigGroup 配置组表
type ConfigGroup struct {
ID int64 `json:"id" xml:"id" gorm:"primaryKey;not null;comment:配置组编号"`
Name string `json:"name" xml:"name" gorm:"size:25;not null;uniqueIndex;comment:配置组名称"`
Description string `json:"description" xml:"description" gorm:"comment:配置组描述"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Configs []*Config `json:"configs" xml:"configs" gorm:"foreignKey:GroupID"`
}

@ -0,0 +1,30 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// Feature 栏目表
type Feature struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:栏目编号"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;uniqueIndex;comment:栏目名称"`
Intro string `json:"intro" xml:"intro" gorm:"comment:栏目简介"`
Icon string `json:"icon" xml:"icon" gorm:"comment:栏目图标"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Config *FeatureConfig `json:"config" xml:"config"`
Categories []*FeatureCategory `json:"categories" xml:"categories"`
Contents []*FeatureContent `json:"contents" xml:"contents"`
}
// AfterDelete 将在对应的条件数据成功从数据库删除之后执行
// todo(hupeh): 是否支持软删除
func (f *Feature) AfterDelete(tx *gorm.DB) error {
return nil
}

@ -0,0 +1,24 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// FeatureCategory 栏目分类表
type FeatureCategory struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:栏目分类编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级分类编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"index:,unique,composite:idx_title_with_feature;comment:所属栏目编号"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;index:,unique,composite:idx_title_with_feature;comment:栏目分类标题"`
Description string `json:"description" xml:"description" gorm:"size:250;comment:栏目分类描述"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"`
Status bool `json:"status" xml:"status" gorm:"comment:栏目分类功能"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Children []*FeatureCategory `json:"children" xml:"children" gorm:"foreignKey:PID"`
}

@ -0,0 +1,21 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// FeatureConfig 栏目配置表
type FeatureConfig struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:配置编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
Status bool `json:"status" xml:"status" gorm:"comment:是否启用分类功能"`
Categorizable bool `json:"categorizable" xml:"categorizable" gorm:"comment:是否启用分类功能"`
CategoryDepth int64 `json:"category_depth" xml:"category_depth" gorm:"comment:最大分类层级数"`
ContentTypes []string `json:"content_types" xml:"content_types" gorm:"serializer:json;comment:支持的内容类型"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
}

@ -0,0 +1,26 @@
package entities
import (
"gorm.io/gorm"
"time"
)
// FeatureContent 栏目内容表(文章、视频、课程)
type FeatureContent struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:内容编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
CategoryID *uint `json:"category_id" xml:"category_id" gorm:"default:null;comment:所属分类编号"`
Type string `json:"type" xml:"type" gorm:"not null;comment:内容类型"`
Title string `json:"title" xml:"title" gorm:"size:100;not null;comment:内容标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:内容简介"`
Version int `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Chapters []*FeatureContentChapter `json:"chapters" xml:"chapters" gorm:"foreignKey:ContentID"`
Details []*FeatureContentDetail `json:"details" xml:"details" gorm:"foreignKey:ContentID"`
// 只有当该记录作为课程时才有效,所以这里需要 check
Departments []*FeatureContent `json:"departments" xml:"departments" gorm:"many2many:company_course_to_department_relations"`
}

@ -0,0 +1,24 @@
package entities
import (
"gorm.io/gorm"
"time"
)
// FeatureContentChapter 栏目内容章回表
type FeatureContentChapter struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:章回编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级章回编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
ContentID uint `json:"content_id" xml:"content_id" gorm:"comment:所属内容编号"`
Title string `json:"title" xml:"title" gorm:"size:100;not null;comment:章回标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:章回描述"`
Sort int32 `json:"sort" xml:"sort" gorm:"size:4;default:0;comment:排序"`
Version int `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Details []FeatureContentDetail `json:"details" xml:"details" gorm:"foreignKey:ChapterID"`
Children []*FeatureContentChapter `json:"children" xml:"children" gorm:"foreignKey:PID"`
}

@ -0,0 +1,28 @@
package entities
import (
"gorm.io/gorm"
"time"
)
// FeatureContentDetail 内容详情表
type FeatureContentDetail struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:内容详情编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
ChapterID *uint `json:"chapter_id" xml:"chapter_id" gorm:"comment:所属章回编号"`
ContentID uint `json:"content_id" xml:"content_id" gorm:"comment:所属内容编号"`
Type string `json:"type" xml:"type" gorm:"not null;comment:内容类型"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;comment:标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:简介"`
PosterUrl string `json:"poster_url" xml:"poster_url" gorm:"size:250;comment:封面链接"`
VideoUrl string `json:"video_url" xml:"video_url" gorm:"size:250;comment:视频描述"`
Text string `json:"text" xml:"text" gorm:"type:longtext;comment:具体内容"`
Attributes map[string]any `json:"attributes" xml:"attributes" gorm:"serializer:json;type:text;comment:相关属性值"`
Version int `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Chapter *FeatureContentChapter `json:"chapter" xml:"chapter" gorm:"foreignKey:ChapterID"`
Content *FeatureContent `json:"content" xml:"content" gorm:"foreignKey:ContentID"`
}

@ -0,0 +1,25 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// Resource 资源表
type Resource struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:资源编号"`
CategoryID uint `json:"category_id" xml:"category_id" gorm:"not null;comment:所属分类编号"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;comment:资源名称"`
Path string `json:"path" xml:"path" gorm:"comment:资源访问路径"`
Width int32 `json:"width" xml:"width" gorm:"comment:资源宽度"`
Height int32 `json:"height" xml:"height" gorm:"comment:资源高度"`
Duration int32 `json:"duration" xml:"duration" gorm:"comment:播放时长(视频、gif动画)"`
MimeType string `json:"mime_type" xml:"mime_type" gorm:"comment:资源媒体类型"`
Extension string `json:"extension" xml:"extension" gorm:"comment:资源文件扩展名"`
Size int64 `json:"size" xml:"size" gorm:"comment:资源大小"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
}

@ -0,0 +1,23 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// ResourceCategory 资源分类表
type ResourceCategory struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:资源分类编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级分类编号"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;uniqueIndex;comment:资源分类名称"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"`
Status bool `json:"status" xml:"status" gorm:"comment:状态"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Resources []*Resource `json:"resources" xml:"resources" gorm:"foreignKey:CategoryID"`
Children []*ResourceCategory `json:"children" xml:"children" gorm:"foreignKey:PID"`
}

@ -0,0 +1,19 @@
package entities
import "time"
// SystemLog 系统日志表
type SystemLog struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:系统用户编号"`
Table string `json:"table" xml:"table" gorm:"comment:被操作表名"`
RowID uint `json:"row_id" xml:"row_id" gorm:"comment:被操作的数据编号"`
Operation string `json:"operation" xml:"operation" gorm:"comment:操作类型,1查询、2新增、3编辑、4删除"`
IP string `json:"ip" xml:"IP" gorm:"comment:用户IP"`
Comment string `json:"comment" xml:"comment" gorm:"comment:操作描述"`
RequestID string `json:"request_id" xml:"request_id" gorm:"comment:请求编号"`
RequestInfo string `json:"request_info" xml:"request_info" gorm:"comment:请求信息"`
ColumnInfo string `json:"column_info" xml:"column_info" gorm:"comment:列变更信息"`
UserID int64 `json:"user_id" xml:"user_id" gorm:"comment:用户编号"`
UserType int64 `json:"user_type" xml:"user_type" gorm:"comment:用户类型"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
}

@ -0,0 +1,23 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// SystemMenu 系统菜单表
type SystemMenu struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:菜单编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级菜单编号"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;comment:菜单标题"`
Icon string `json:"icon" xml:"icon" gorm:"comment:菜单图标"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"`
Path string `json:"path" xml:"path" gorm:"comment:跳转链接"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Children []*SystemMenu `json:"children" xml:"children" gorm:"foreignKey:PID"`
}

@ -0,0 +1,19 @@
package entities
import (
"gorm.io/gorm"
"time"
)
type SystemPermission struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:权限编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级权限编号"`
Name string `json:"name" xml:"name" gorm:"size:25;not null;comment:权限名称"`
Type string `json:"type" xml:"type" gorm:"size:25;not null;index;comment:权限类型"`
Identifier string `json:"identifier" xml:"identifier" gorm:"size:25;not null;uniqueIndex;comment:权限标识"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Children []*SystemPermission `json:"children" xml:"children" gorm:"foreignKey:PID"`
}

@ -0,0 +1,20 @@
package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
// SystemRole 系统用户角色表
type SystemRole struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:角色编号"`
Name string `json:"name" xml:"name" gorm:"size:25;not null;uniqueIndex;comment:角色名称"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Powers []*SystemRolePower `json:"powers" xml:"powers" gorm:"foreignKey:RoleID"`
Users []*SystemUser `json:"users" xml:"users" gorm:"many2many:system_user_to_role_relations"`
}

@ -0,0 +1,12 @@
package entities
import "time"
// SystemRolePower 角色授权表
type SystemRolePower struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:能力编号"`
RoleID uint `json:"role_id" xml:"role_id" gorm:"comment:关联角色编号"`
WithType string `json:"with_type" xml:"with_type" gorm:"size:25;not null;comment:关联类型,如:perm权限、menu菜单、data数据"`
WithID uint `json:"with_id" xml:"with_id" gorm:"size:25;not null;comment:关联编号"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
}

@ -0,0 +1,20 @@
package entities
import (
"gorm.io/gorm"
"time"
)
// SystemUser 系统用户表
type SystemUser struct {
ID int64 `json:"id" xml:"id" gorm:"primaryKey;not null;comment:系统用户编号"`
Username string `json:"username" xml:"username" gorm:"size:25;not null;uniqueIndex;comment:用户名"`
Password string `json:"password" xml:"password" gorm:"size:25;not null;comment:登录密码"`
Status bool `json:"status" xml:"status" gorm:"comment:状态"`
Version int `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
Roles []*SystemRole `json:"roles" xml:"roles" gorm:"many2many:system_user_to_role_relations"`
}

@ -0,0 +1,53 @@
package internal
import (
"errors"
"sorbet/internal/entities"
"sorbet/internal/repositories"
"sorbet/pkg/db"
"sorbet/pkg/env"
"sorbet/pkg/ioc"
"sorbet/pkg/log"
)
func Init() error {
ioc.Bind(db.DB()) // 注入数据库操作
ioc.Bind(log.Default()) // 注入日志操作
repositories.Init() // 注入数据仓库操作
// 同步数据库结构
if err := syncEntities(); err != nil {
if !errors.Is(err, db.ErrNoCodeFirst) {
return err
}
if !env.IsEnv("prod") {
log.Error("同步数据表结构需要开启 [DB_CODE_FIRST],在生产模式下请务必关闭。")
}
}
return nil
}
func syncEntities() error {
return db.Sync(
&entities.Company{},
&entities.CompanyDepartment{},
&entities.CompanyStaff{},
&entities.Config{},
&entities.ConfigGroup{},
&entities.Feature{},
&entities.FeatureCategory{},
&entities.FeatureConfig{},
&entities.FeatureContent{},
&entities.FeatureContentChapter{},
&entities.FeatureContentDetail{},
&entities.Resource{},
&entities.ResourceCategory{},
&entities.SystemLog{},
&entities.SystemMenu{},
&entities.SystemPermission{},
&entities.SystemRole{},
&entities.SystemRolePower{},
&entities.SystemUser{},
)
}

@ -0,0 +1,134 @@
package middleware
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
type CORSConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// AllowOrigins determines the value of the Access-Control-Allow-Origin
// response header. This header defines a list of origins that may access the
// resource. The wildcard characters '*' and '?' are supported and are
// converted to regex fragments '.*' and '.' accordingly.
//
// Security: use extreme caution when handling the origin, and carefully
// validate any logic. Remember that attackers may register hostile domain names.
// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
//
// Optional. Default value []string{"*"}.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
AllowOrigins []string
// AllowOriginFunc is a custom function to validate the origin. It takes the
// origin as an argument and returns true if allowed or false otherwise. If
// an error is returned, it is returned by the handler. If this option is
// set, AllowOrigins is ignored.
//
// Security: use extreme caution when handling the origin, and carefully
// validate any logic. Remember that attackers may register hostile domain names.
// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
//
// Optional.
AllowOriginFunc func(origin string) (bool, error)
// AllowMethods determines the value of the Access-Control-Allow-Methods
// response header. This header specified the list of methods allowed when
// accessing the resource. This is used in response to a preflight request.
//
// Optional. Default value DefaultCORSConfig.AllowMethods.
// If `allowMethods` is left empty, this middleware will fill for preflight
// request `Access-Control-Allow-Methods` header value
// from `Allow` header that echo.Router set into context.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
AllowMethods []string
// AllowHeaders determines the value of the Access-Control-Allow-Headers
// response header. This header is used in response to a preflight request to
// indicate which HTTP headers can be used when making the actual request.
//
// Optional. Default value []string{}.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
AllowHeaders []string
// AllowCredentials determines the value of the
// Access-Control-Allow-Credentials response header. This header indicates
// whether or not the response to the request can be exposed when the
// credentials mode (Request.credentials) is true. When used as part of a
// response to a preflight request, this indicates whether or not the actual
// request can be made using credentials. See also
// [MDN: Access-Control-Allow-Credentials].
//
// Optional. Default value false, in which case the header is not set.
//
// Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`.
// See "Exploiting CORS misconfigurations for Bitcoins and bounties",
// https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
AllowCredentials bool
// UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials
// flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header.
//
// This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties)
// attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject.
//
// Optional. Default value is false.
UnsafeWildcardOriginWithAllowCredentials bool
// ExposeHeaders determines the value of Access-Control-Expose-Headers, which
// defines a list of headers that clients are allowed to access.
//
// Optional. Default value []string{}, in which case the header is not set.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header
ExposeHeaders []string
// MaxAge determines the value of the Access-Control-Max-Age response header.
// This header indicates how long (in seconds) the results of a preflight
// request can be cached.
//
// Optional. Default value 0. The header is set only if MaxAge > 0.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
MaxAge int
}
// DefaultCORSConfig is the default CORS middleware config.
var DefaultCORSConfig = CORSConfig{
Skipper: middleware.DefaultSkipper,
AllowOrigins: []string{"*"},
AllowMethods: []string{
http.MethodGet,
http.MethodHead,
http.MethodPut,
http.MethodPatch,
http.MethodPost,
http.MethodDelete,
},
}
func (c *CORSConfig) ToMiddleware() echo.MiddlewareFunc {
return middleware.CORSWithConfig(middleware.CORSConfig{
Skipper: c.Skipper,
AllowOrigins: c.AllowOrigins,
AllowOriginFunc: c.AllowOriginFunc,
AllowMethods: c.AllowMethods,
AllowHeaders: c.AllowHeaders,
AllowCredentials: c.AllowCredentials,
UnsafeWildcardOriginWithAllowCredentials: c.UnsafeWildcardOriginWithAllowCredentials,
ExposeHeaders: c.ExposeHeaders,
MaxAge: c.MaxAge,
})
}
func CORS() echo.MiddlewareFunc {
return DefaultCORSConfig.ToMiddleware()
}

@ -0,0 +1,140 @@
package middleware
import (
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
)
// JWTConfig defines the config for JWT middleware.
type JWTConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// BeforeFunc defines a function which is executed just before the middleware.
BeforeFunc BeforeFunc
// SuccessHandler defines a function which is executed for a valid token.
SuccessHandler func(c echo.Context)
// ErrorHandler defines a function which is executed when all lookups have been done and none of them passed Validator
// function. ErrorHandler is executed with last missing (ErrExtractionValueMissing) or an invalid key.
// It may be used to define a custom JWT error.
//
// Note: when error handler swallows the error (returns nil) middleware continues handler chain execution towards handler.
// This is useful in cases when portion of your site/api is publicly accessible and has extra features for authorized users
// In that case you can use ErrorHandler to set default public JWT token value to request and continue with handler chain.
ErrorHandler func(c echo.Context, err error) error
// ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to
// ignore the error (by returning `nil`).
// This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality.
// In that case you can use ErrorHandler to set a default public JWT token value in the request context
// and continue. Some logic down the remaining execution chain needs to check that (public) token value then.
ContinueOnIgnoredError bool
// Context key to store user information from the token into context.
// Optional. Default value "user".
ContextKey string
// Signing key to validate token.
// This is one of the three options to provide a token validation key.
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
// Required if neither user-defined KeyFunc nor SigningKeys is provided.
SigningKey any
// Map of signing keys to validate token with kid field usage.
// This is one of the three options to provide a token validation key.
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
// Required if neither user-defined KeyFunc nor SigningKey is provided.
SigningKeys map[string]any
// Signing method used to check the token's signing algorithm.
// Optional. Default value HS256.
SigningMethod string
// KeyFunc defines a user-defined function that supplies the public key for a token validation.
// The function shall take care of verifying the signing algorithm and selecting the proper key.
// A user-defined KeyFunc can be useful if tokens are issued by an external party.
// Used by default ParseTokenFunc implementation.
//
// When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored.
// This is one of the three options to provide a token validation key.
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
// Required if neither SigningKeys nor SigningKey is provided.
// Not used if custom ParseTokenFunc is set.
// Default to an internal implementation verifying the signing algorithm and selecting the proper key.
KeyFunc jwt.Keyfunc
// TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>" or "header:<name>:<cut-prefix>"
// `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header
// value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we
// want to cut is `<auth-scheme> ` note the space at the end.
// In case of JWT tokens `Authorization: Bearer <token>` prefix we cut is `Bearer `.
// If prefix is left empty the whole value is returned.
// - "query:<name>"
// - "param:<name>"
// - "cookie:<name>"
// - "form:<name>"
// Multiple sources example:
// - "header:Authorization:Bearer ,cookie:myowncookie"
TokenLookup string
// TokenLookupFuncs defines a list of user-defined functions that extract JWT token from the given context.
// This is one of the two options to provide a token extractor.
// The order of precedence is user-defined TokenLookupFuncs, and TokenLookup.
// You can also provide both if you want.
TokenLookupFuncs []ValuesExtractor
// ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token
// parsing fails or parsed token is invalid.
// Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library
ParseTokenFunc func(c echo.Context, auth string) (any, error)
// Claims are extendable claims data defining token content. Used by default ParseTokenFunc implementation.
// Not used if custom ParseTokenFunc is set.
// Optional. Defaults to function returning jwt.MapClaims
NewClaimsFunc func(c echo.Context) jwt.Claims
}
// Errors
var (
ErrJWTMissing = echojwt.ErrJWTMissing
ErrJWTInvalid = echojwt.ErrJWTInvalid
)
func (config *JWTConfig) ToMiddleware() echo.MiddlewareFunc {
return echojwt.WithConfig(echojwt.Config{
Skipper: config.Skipper,
BeforeFunc: config.BeforeFunc,
SuccessHandler: config.SuccessHandler,
ErrorHandler: config.ErrorHandler,
ContinueOnIgnoredError: config.ContinueOnIgnoredError,
ContextKey: config.ContextKey,
SigningKey: config.SigningKey,
SigningKeys: config.SigningKeys,
SigningMethod: config.SigningMethod,
KeyFunc: config.KeyFunc,
TokenLookup: config.TokenLookup,
TokenLookupFuncs: config.TokenLookupFuncs,
ParseTokenFunc: config.ParseTokenFunc,
NewClaimsFunc: nil,
})
}
// JWT returns a JSON Web Token (JWT) auth middleware.
//
// For valid token, it sets the user in context and calls next handler.
// For invalid token, it returns "401 - Unauthorized" error.
// For missing token, it returns "400 - Bad Request" error.
//
// See: https://jwt.io/introduction
// See `JWTConfig.TokenLookup`
// See https://github.com/labstack/echo-jwt
func JWT(signingKey any) echo.MiddlewareFunc {
return echojwt.JWT(signingKey)
}

@ -0,0 +1,74 @@
package middleware
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// KeyAuthValidator defines a function to validate KeyAuth credentials.
type KeyAuthValidator = middleware.KeyAuthValidator
// KeyAuthErrorHandler defines a function which is executed for an invalid key.
type KeyAuthErrorHandler = middleware.KeyAuthErrorHandler
// KeyAuthConfig defines the config for KeyAuth middleware.
type KeyAuthConfig struct {
Skipper Skipper
// KeyLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
// to extract key from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>" or "header:<name>:<cut-prefix>"
// `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header
// value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we
// want to cut is `<auth-scheme> ` note the space at the end.
// In case of basic authentication `Authorization: Basic <credentials>` prefix we want to remove is `Basic `.
// - "query:<name>"
// - "form:<name>"
// - "cookie:<name>"
// Multiple sources example:
// - "header:Authorization,header:X-Api-Key"
KeyLookup string
// AuthScheme to be used in the Authorization header.
// Optional. Default value "Bearer".
AuthScheme string
// Validator is a function to validate key.
// Required.
Validator KeyAuthValidator
// ErrorHandler defines a function which is executed for an invalid key.
// It may be used to define a custom error.
ErrorHandler KeyAuthErrorHandler
// ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to
// ignore the error (by returning `nil`).
// This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality.
// In that case you can use ErrorHandler to set a default public key auth value in the request context
// and continue. Some logic down the remaining execution chain needs to check that (public) key auth value then.
ContinueOnIgnoredError bool
}
// DefaultKeyAuthConfig is the default KeyAuth middleware config.
var DefaultKeyAuthConfig = KeyAuthConfig{
Skipper: DefaultSkipper,
KeyLookup: "header:" + echo.HeaderAuthorization,
AuthScheme: "Bearer",
}
func (a *KeyAuthConfig) ToMiddleware() echo.MiddlewareFunc {
return middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
Skipper: a.Skipper,
KeyLookup: a.KeyLookup,
AuthScheme: a.AuthScheme,
Validator: a.Validator,
ErrorHandler: a.ErrorHandler,
ContinueOnIgnoredError: a.ContinueOnIgnoredError,
})
}
func KeyAuth() echo.MiddlewareFunc {
return DefaultKeyAuthConfig.ToMiddleware()
}

@ -0,0 +1,63 @@
package middleware
import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/rs/xid"
"net/http"
"sorbet/internal/util"
"sorbet/pkg/log"
"time"
)
var color = log.NewColorer()
func requestId(req *http.Request, res *echo.Response) string {
id := req.Header.Get(echo.HeaderXRequestID)
if id == "" {
id = xid.New().String()
res.Header().Set(echo.HeaderXRequestID, id)
}
return id
}
// Logger 该日志中间件会自动获取或设置 RequestID
func Logger(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
req := c.Request()
res := c.Response()
start := time.Now()
id := requestId(req, res)
l := log.With(log.String("id", id))
l.Info(
"Started %s %s for %s",
req.Method, req.RequestURI, c.RealIP(),
log.RawTime(start),
)
c.SetLogger(util.NewCustomLogger(l))
if err = next(c); err != nil {
c.Error(err)
}
stop := time.Now()
content := fmt.Sprintf(
"Completed %s %s %v %s in %v",
req.Method, req.RequestURI, res.Status,
http.StatusText(res.Status), stop.Sub(start),
)
if res.Status >= 500 {
content = color.Cyan(content)
} else if res.Status >= 400 {
content = color.Red(content)
} else if res.Status >= 300 {
if res.Status == 304 {
content = color.Yellow(content)
} else {
content = color.White(content)
}
} else if res.Status >= 200 {
content = color.Green(content)
}
l.Info(content, log.RawTime(stop))
return
}
}

@ -0,0 +1,24 @@
package middleware
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// Skipper defines a function to skip middleware. Returning true skips processing
// the middleware.
type Skipper = middleware.Skipper
// BeforeFunc defines a function which is executed just before the middleware.
type BeforeFunc = middleware.BeforeFunc
type ValuesExtractor = middleware.ValuesExtractor
type ToMiddleware interface {
ToMiddleware() echo.MiddlewareFunc
}
// DefaultSkipper returns false which processes the middleware.
func DefaultSkipper(echo.Context) bool {
return false
}

@ -0,0 +1,268 @@
package middleware
import (
"github.com/labstack/echo/v4/middleware"
"net/http"
"sync"
"time"
"github.com/labstack/echo/v4"
"golang.org/x/time/rate"
)
// RateLimiterStore is the interface to be implemented by custom stores.
type RateLimiterStore interface {
// Allow Stores for the rate limiter have to implement the Allow method
Allow(identifier string) (bool, error)
}
type (
// RateLimiterConfig defines the configuration for the rate limiter
RateLimiterConfig struct {
Skipper Skipper
BeforeFunc middleware.BeforeFunc
// IdentifierExtractor uses echo.Context to extract the identifier for a visitor
IdentifierExtractor Extractor
// Store defines a store for the rate limiter
Store RateLimiterStore
// ErrorHandler provides a handler to be called when IdentifierExtractor returns an error
ErrorHandler func(context echo.Context, err error) error
// DenyHandler provides a handler to be called when RateLimiter denies access
DenyHandler func(context echo.Context, identifier string, err error) error
}
// Extractor is used to extract data from echo.Context
Extractor func(context echo.Context) (string, error)
)
// errors
var (
// ErrRateLimitExceeded denotes an error raised when rate limit is exceeded
ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded")
// ErrExtractorError denotes an error raised when extractor function is unsuccessful
ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier")
)
// DefaultRateLimiterConfig defines default values for RateLimiterConfig
var DefaultRateLimiterConfig = RateLimiterConfig{
Skipper: middleware.DefaultSkipper,
IdentifierExtractor: func(ctx echo.Context) (string, error) {
id := ctx.RealIP()
return id, nil
},
ErrorHandler: func(context echo.Context, err error) error {
return &echo.HTTPError{
Code: ErrExtractorError.Code,
Message: ErrExtractorError.Message,
Internal: err,
}
},
DenyHandler: func(context echo.Context, identifier string, err error) error {
return &echo.HTTPError{
Code: ErrRateLimitExceeded.Code,
Message: ErrRateLimitExceeded.Message,
Internal: err,
}
},
}
/*
RateLimiter returns a rate limiting middleware
e := echo.New()
limiterStore := middleware.NewRateLimiterMemoryStore(20)
e.GET("/rate-limited", func(c echo.Context) error {
return c.String(http.StatusOK, "test")
}, RateLimiter(limiterStore))
*/
func RateLimiter(store RateLimiterStore) echo.MiddlewareFunc {
config := DefaultRateLimiterConfig
config.Store = store
return config.ToMiddleware()
}
/*
ToMiddleware returns a rate limiting middleware
e := echo.New()
config := middleware.RateLimiterConfig{
Skipper: DefaultSkipper,
Store: middleware.NewRateLimiterMemoryStore(
middleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute}
)
IdentifierExtractor: func(ctx echo.Context) (string, error) {
id := ctx.RealIP()
return id, nil
},
ErrorHandler: func(context echo.Context, err error) error {
return context.JSON(http.StatusTooManyRequests, nil)
},
DenyHandler: func(context echo.Context, identifier string) error {
return context.JSON(http.StatusForbidden, nil)
},
}
e.GET("/rate-limited", func(c echo.Context) error {
return c.String(http.StatusOK, "test")
}, middleware.RateLimiterWithConfig(config))
*/
func (config *RateLimiterConfig) ToMiddleware() echo.MiddlewareFunc {
if config.Skipper == nil {
config.Skipper = DefaultSkipper
}
if config.IdentifierExtractor == nil {
config.IdentifierExtractor = DefaultRateLimiterConfig.IdentifierExtractor
}
if config.ErrorHandler == nil {
config.ErrorHandler = DefaultRateLimiterConfig.ErrorHandler
}
if config.DenyHandler == nil {
config.DenyHandler = DefaultRateLimiterConfig.DenyHandler
}
if config.Store == nil {
panic("Store configuration must be provided")
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
if config.BeforeFunc != nil {
config.BeforeFunc(c)
}
identifier, err := config.IdentifierExtractor(c)
if err != nil {
c.Error(config.ErrorHandler(c, err))
return nil
}
if allow, err := config.Store.Allow(identifier); !allow {
c.Error(config.DenyHandler(c, identifier, err))
return nil
}
return next(c)
}
}
}
type (
// RateLimiterMemoryStore is the built-in store implementation for RateLimiter
RateLimiterMemoryStore struct {
visitors map[string]*Visitor
mutex sync.Mutex
rate rate.Limit // for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
burst int
expiresIn time.Duration
lastCleanup time.Time
timeNow func() time.Time
}
// Visitor signifies a unique user's limiter details
Visitor struct {
*rate.Limiter
lastSeen time.Time
}
)
/*
NewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with
the provided rate (as req/s).
for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
Burst and ExpiresIn will be set to default values.
Note that if the provided rate is a float number and Burst is zero, Burst will be treated as the rounded down value of the rate.
Example (with 20 requests/sec):
limiterStore := middleware.NewRateLimiterMemoryStore(20)
*/
func NewRateLimiterMemoryStore(rate rate.Limit) (store *RateLimiterMemoryStore) {
return NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{
Rate: rate,
})
}
/*
NewRateLimiterMemoryStoreWithConfig returns an instance of RateLimiterMemoryStore
with the provided configuration. Rate must be provided. Burst will be set to the rounded down value of
the configured rate if not provided or set to 0.
The build-in memory store is usually capable for modest loads. For higher loads other
store implementations should be considered.
Characteristics:
* Concurrency above 100 parallel requests may causes measurable lock contention
* A high number of different IP addresses (above 16000) may be impacted by the internally used Go map
* A high number of requests from a single IP address may cause lock contention
Example:
limiterStore := middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{Rate: 50, Burst: 200, ExpiresIn: 5 * time.Minute},
)
*/
func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (store *RateLimiterMemoryStore) {
store = &RateLimiterMemoryStore{}
store.rate = config.Rate
store.burst = config.Burst
store.expiresIn = config.ExpiresIn
if config.ExpiresIn == 0 {
store.expiresIn = DefaultRateLimiterMemoryStoreConfig.ExpiresIn
}
if config.Burst == 0 {
store.burst = int(config.Rate)
}
store.visitors = make(map[string]*Visitor)
store.timeNow = time.Now
store.lastCleanup = store.timeNow()
return
}
// RateLimiterMemoryStoreConfig represents configuration for RateLimiterMemoryStore
type RateLimiterMemoryStoreConfig struct {
Rate rate.Limit // Rate of requests allowed to pass as req/s. For more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
Burst int // Burst is maximum number of requests to pass at the same moment. It additionally allows a number of requests to pass when rate limit is reached.
ExpiresIn time.Duration // ExpiresIn is the duration after that a rate limiter is cleaned up
}
// DefaultRateLimiterMemoryStoreConfig provides default configuration values for RateLimiterMemoryStore
var DefaultRateLimiterMemoryStoreConfig = RateLimiterMemoryStoreConfig{
ExpiresIn: 3 * time.Minute,
}
// Allow implements RateLimiterStore.Allow
func (store *RateLimiterMemoryStore) Allow(identifier string) (bool, error) {
store.mutex.Lock()
limiter, exists := store.visitors[identifier]
if !exists {
limiter = new(Visitor)
limiter.Limiter = rate.NewLimiter(store.rate, store.burst)
store.visitors[identifier] = limiter
}
now := store.timeNow()
limiter.lastSeen = now
if now.Sub(store.lastCleanup) > store.expiresIn {
store.cleanupStaleVisitors()
}
store.mutex.Unlock()
return limiter.AllowN(store.timeNow(), 1), nil
}
/*
cleanupStaleVisitors helps manage the size of the visitors map by removing stale records
of users who haven't visited again after the configured expiry time has elapsed
*/
func (store *RateLimiterMemoryStore) cleanupStaleVisitors() {
for id, visitor := range store.visitors {
if store.timeNow().Sub(visitor.lastSeen) > store.expiresIn {
delete(store.visitors, id)
}
}
store.lastCleanup = store.timeNow()
}

@ -0,0 +1,135 @@
package middleware
import (
"fmt"
"net/http"
"runtime"
"sorbet/internal/util"
"sorbet/pkg/log"
"github.com/labstack/echo/v4"
)
// LogErrorFunc defines a function for custom logging in the middleware.
type LogErrorFunc func(c echo.Context, err error, stack []byte) error
// RecoverConfig defines the config for Recover middleware.
type RecoverConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Size of the stack to be printed.
// Optional. Default value 4KB.
StackSize int
// DisableStackAll disables formatting stack traces of all other goroutines
// into buffer after the trace for the current goroutine.
// Optional. Default value false.
DisableStackAll bool
// DisablePrintStack disables printing stack trace.
// Optional. Default value as false.
DisablePrintStack bool
// LogLevel is log level to printing stack trace.
// Optional. Default value 0 (Print).
LogLevel log.Level
// LogErrorFunc defines a function for custom logging in the middleware.
// If it's set you don't need to provide LogLevel for config.
// If this function returns nil, the centralized HTTPErrorHandler will not be called.
LogErrorFunc LogErrorFunc
// DisableErrorHandler disables the call to centralized HTTPErrorHandler.
// The recovered error is then passed back to upstream middleware, instead of swallowing the error.
// Optional. Default value false.
DisableErrorHandler bool
}
// DefaultRecoverConfig is the default Recover middleware config.
var DefaultRecoverConfig = RecoverConfig{
Skipper: DefaultSkipper,
StackSize: 4 << 10, // 4 KB
DisableStackAll: false,
DisablePrintStack: false,
LogLevel: log.LevelDebug,
LogErrorFunc: nil,
DisableErrorHandler: false,
}
func (config *RecoverConfig) ToMiddleware() echo.MiddlewareFunc {
if config.Skipper == nil {
config.Skipper = DefaultRecoverConfig.Skipper
}
if config.StackSize == 0 {
config.StackSize = DefaultRecoverConfig.StackSize
}
switch config.LogLevel {
case log.LevelTrace, log.LevelFatal, log.LevelPanic:
panic("不应该将 LevelTrace、LevelFatal 和 LevelPanic 这三个日志作用在错误恢复中间件上")
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (returnErr error) {
if config.Skipper(c) {
return next(c)
}
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
}
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
var stack []byte
var length int
if !config.DisablePrintStack {
stack = make([]byte, config.StackSize)
length = runtime.Stack(stack, !config.DisableStackAll)
stack = stack[:length]
}
if config.LogErrorFunc != nil {
err = config.LogErrorFunc(c, err, stack)
} else if !config.DisablePrintStack {
var i []any
if _, ok := c.Logger().(*util.EchoLogger); ok {
i = append(i,
fmt.Sprintf("%v %s\n", err, stack[:length]),
log.RawLevel("PANIC RECOVER"),
)
} else {
i = append(i, fmt.Sprintf("[PANIC RECOVER] %v %s\n", err, stack[:length]))
}
switch config.LogLevel {
case log.LevelDebug:
c.Logger().Debug(i...)
case log.LevelInfo:
c.Logger().Info(i...)
case log.LevelWarn:
c.Logger().Warn(i...)
case log.LevelError:
c.Logger().Error(i...)
case log.LevelOff:
// None.
default:
c.Logger().Print(i...)
}
}
if err != nil && !config.DisableErrorHandler {
c.Error(err)
} else {
returnErr = err
}
}
}()
return next(c)
}
}
}
// Recover returns a middleware which recovers from panics anywhere in the chain
// and handles the control to the centralized HTTPErrorHandler.
func Recover() echo.MiddlewareFunc {
return DefaultRecoverConfig.ToMiddleware()
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type CompanyRepository struct {
*db.Repository[entities.Company]
}
// NewCompanyRepository 创建公司仓库
func NewCompanyRepository(orm *gorm.DB) *CompanyRepository {
return &CompanyRepository{
db.NewRepositoryWith[entities.Company](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type CompanyDepartmentRepository struct {
*db.Repository[entities.CompanyDepartment]
}
// NewCompanyDepartmentRepository 创建公司部门仓库
func NewCompanyDepartmentRepository(orm *gorm.DB) *CompanyDepartmentRepository {
return &CompanyDepartmentRepository{
db.NewRepositoryWith[entities.CompanyDepartment](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type CompanyStaffRepository struct {
*db.Repository[entities.CompanyStaff]
}
// NewCompanyStaffRepository 创建公司员工仓库
func NewCompanyStaffRepository(orm *gorm.DB) *CompanyStaffRepository {
return &CompanyStaffRepository{
db.NewRepositoryWith[entities.CompanyStaff](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type ConfigRepository struct {
*db.Repository[entities.Config]
}
// NewConfigRepository 创建配置仓库
func NewConfigRepository(orm *gorm.DB) *ConfigRepository {
return &ConfigRepository{
db.NewRepositoryWith[entities.Config](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type ConfigGroupRepository struct {
*db.Repository[entities.ConfigGroup]
}
// NewConfigGroupRepository 创建配置组仓库
func NewConfigGroupRepository(orm *gorm.DB) *ConfigGroupRepository {
return &ConfigGroupRepository{
db.NewRepositoryWith[entities.ConfigGroup](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type FeatureRepository struct {
*db.Repository[entities.Feature]
}
// NewFeatureRepository 创建栏目仓库
func NewFeatureRepository(orm *gorm.DB) *FeatureRepository {
return &FeatureRepository{
db.NewRepositoryWith[entities.Feature](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type FeatureCategoryRepository struct {
*db.Repository[entities.FeatureCategory]
}
// NewFeatureCategoryRepository 创建栏目分类仓库
func NewFeatureCategoryRepository(orm *gorm.DB) *FeatureCategoryRepository {
return &FeatureCategoryRepository{
db.NewRepositoryWith[entities.FeatureCategory](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type FeatureConfigRepository struct {
*db.Repository[entities.FeatureConfig]
}
// NewFeatureConfigRepository 创建栏目配置仓库
func NewFeatureConfigRepository(orm *gorm.DB) *FeatureConfigRepository {
return &FeatureConfigRepository{
db.NewRepositoryWith[entities.FeatureConfig](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type FeatureContentRepository struct {
*db.Repository[entities.FeatureContent]
}
// NewFeatureContentRepository 创建栏目内容仓库
func NewFeatureContentRepository(orm *gorm.DB) *FeatureContentRepository {
return &FeatureContentRepository{
db.NewRepositoryWith[entities.FeatureContent](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type FeatureContentChapterRepository struct {
*db.Repository[entities.FeatureContentChapter]
}
// NewFeatureContentChapterRepository 创建栏目内容章回仓库
func NewFeatureContentChapterRepository(orm *gorm.DB) *FeatureContentChapterRepository {
return &FeatureContentChapterRepository{
db.NewRepositoryWith[entities.FeatureContentChapter](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type FeatureContentDetailRepository struct {
*db.Repository[entities.FeatureContentDetail]
}
// NewFeatureContentDetailRepository 创建栏目内容详情仓库
func NewFeatureContentDetailRepository(orm *gorm.DB) *FeatureContentDetailRepository {
return &FeatureContentDetailRepository{
db.NewRepositoryWith[entities.FeatureContentDetail](orm, "id"),
}
}

@ -0,0 +1,27 @@
package repositories
import (
"sorbet/pkg/ioc"
)
func Init() {
ioc.MustFactory(NewCompanyRepository)
ioc.MustFactory(NewCompanyDepartmentRepository)
ioc.MustFactory(NewCompanyStaffRepository)
ioc.MustFactory(NewConfigRepository)
ioc.MustFactory(NewConfigGroupRepository)
ioc.MustFactory(NewFeatureRepository)
ioc.MustFactory(NewFeatureCategoryRepository)
ioc.MustFactory(NewFeatureConfigRepository)
ioc.MustFactory(NewFeatureContentRepository)
ioc.MustFactory(NewFeatureContentChapterRepository)
ioc.MustFactory(NewFeatureContentDetailRepository)
ioc.MustFactory(NewResourceRepository)
ioc.MustFactory(NewResourceCategoryRepository)
ioc.MustFactory(NewSystemLogRepository)
ioc.MustFactory(NewSystemMenuRepository)
ioc.MustFactory(NewSystemPermissionRepository)
ioc.MustFactory(NewSystemRoleRepository)
ioc.MustFactory(NewSystemRolePowerRepository)
ioc.MustFactory(NewSystemUserRepository)
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type ResourceRepository struct {
*db.Repository[entities.Resource]
}
// NewResourceRepository 创建资源仓库
func NewResourceRepository(orm *gorm.DB) *ResourceRepository {
return &ResourceRepository{
db.NewRepositoryWith[entities.Resource](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type ResourceCategoryRepository struct {
*db.Repository[entities.ResourceCategory]
}
// NewResourceCategoryRepository 创建资源分类仓库
func NewResourceCategoryRepository(orm *gorm.DB) *ResourceCategoryRepository {
return &ResourceCategoryRepository{
db.NewRepositoryWith[entities.ResourceCategory](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type SystemLogRepository struct {
*db.Repository[entities.SystemLog]
}
// NewSystemLogRepository 创建系统日志仓库
func NewSystemLogRepository(orm *gorm.DB) *SystemLogRepository {
return &SystemLogRepository{
db.NewRepositoryWith[entities.SystemLog](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type SystemMenuRepository struct {
*db.Repository[entities.SystemMenu]
}
// NewSystemMenuRepository 创建系统菜单仓库
func NewSystemMenuRepository(orm *gorm.DB) *SystemMenuRepository {
return &SystemMenuRepository{
db.NewRepositoryWith[entities.SystemMenu](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type SystemPermissionRepository struct {
*db.Repository[entities.SystemPermission]
}
// NewSystemPermissionRepository 创建系统权限仓库
func NewSystemPermissionRepository(orm *gorm.DB) *SystemPermissionRepository {
return &SystemPermissionRepository{
db.NewRepositoryWith[entities.SystemPermission](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type SystemRoleRepository struct {
*db.Repository[entities.SystemRole]
}
// NewSystemRoleRepository 创建系统用户角色仓库
func NewSystemRoleRepository(orm *gorm.DB) *SystemRoleRepository {
return &SystemRoleRepository{
db.NewRepositoryWith[entities.SystemRole](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type SystemRolePowerRepository struct {
*db.Repository[entities.SystemRolePower]
}
// NewSystemRolePowerRepository 创建角色授权仓库
func NewSystemRolePowerRepository(orm *gorm.DB) *SystemRolePowerRepository {
return &SystemRolePowerRepository{
db.NewRepositoryWith[entities.SystemRolePower](orm, "id"),
}
}

@ -0,0 +1,19 @@
package repositories
import (
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
)
type SystemUserRepository struct {
*db.Repository[entities.SystemUser]
}
// NewSystemUserRepository 创建系统用户仓库
func NewSystemUserRepository(orm *gorm.DB) *SystemUserRepository {
return &SystemUserRepository{
db.NewRepositoryWith[entities.SystemUser](orm, "id"),
}
}

@ -0,0 +1,7 @@
package util
import "github.com/labstack/echo/v4"
type EchoContext struct {
echo.Context
}

@ -0,0 +1,201 @@
package util
import (
"encoding/json"
"fmt"
"github.com/labstack/gommon/log"
"io"
"os"
sorbetLog "sorbet/pkg/log"
)
type EchoLogger struct {
l sorbetLog.Logger
}
func NewLogger() *EchoLogger {
return NewCustomLogger(sorbetLog.Default())
}
func NewCustomLogger(l sorbetLog.Logger) *EchoLogger {
return &EchoLogger{l}
}
func (l *EchoLogger) Output() io.Writer {
return l.l.Writer()
}
func (l *EchoLogger) SetOutput(w io.Writer) {
l.l.SetWriter(w)
}
func (l *EchoLogger) Prefix() string {
return ""
}
func (l *EchoLogger) SetPrefix(_ string) {
// FIXME(hupeh): 能否使用 WithGroup 或者 Attr 实现?
fmt.Println("cannot set a prefix into logging")
}
func (l *EchoLogger) Level() log.Lvl {
switch v := l.l.Level(); v {
case sorbetLog.LevelDebug, sorbetLog.LevelTrace:
return log.DEBUG
case sorbetLog.LevelInfo:
return log.INFO
case sorbetLog.LevelWarn:
return log.WARN
case sorbetLog.LevelError:
return log.ERROR
case sorbetLog.LevelOff:
return log.OFF
case sorbetLog.LevelFatal:
return log.Lvl(7)
default:
if v < sorbetLog.LevelTrace {
return log.DEBUG
} else {
return log.Lvl(7)
}
}
}
func (l *EchoLogger) SetLevel(v log.Lvl) {
switch v {
case log.DEBUG:
l.l.SetLevel(sorbetLog.LevelDebug)
case log.INFO:
l.l.SetLevel(sorbetLog.LevelInfo)
case log.WARN:
l.l.SetLevel(sorbetLog.LevelWarn)
case log.ERROR:
l.l.SetLevel(sorbetLog.LevelError)
case log.OFF:
l.l.SetLevel(sorbetLog.LevelOff)
}
}
func (l *EchoLogger) SetHeader(_ string) {
fmt.Println("cannot set a header into logging")
}
func (l *EchoLogger) Print(i ...interface{}) {
l.log(l.l.Level(), i...)
}
func (l *EchoLogger) Printf(format string, args ...interface{}) {
l.logf(l.l.Level(), format, args...)
}
func (l *EchoLogger) Printj(j log.JSON) {
l.logj(l.l.Level(), j)
}
func (l *EchoLogger) Debug(i ...interface{}) {
l.log(sorbetLog.LevelDebug, i...)
}
func (l *EchoLogger) Debugf(format string, args ...interface{}) {
l.logf(sorbetLog.LevelDebug, format, args...)
}
func (l *EchoLogger) Debugj(j log.JSON) {
l.logj(sorbetLog.LevelDebug, j)
}
func (l *EchoLogger) Info(i ...interface{}) {
l.log(sorbetLog.LevelInfo, i...)
}
func (l *EchoLogger) Infof(format string, args ...interface{}) {
l.logf(sorbetLog.LevelInfo, format, args...)
}
func (l *EchoLogger) Infoj(j log.JSON) {
l.logj(sorbetLog.LevelInfo, j)
}
func (l *EchoLogger) Warn(i ...interface{}) {
l.log(sorbetLog.LevelWarn, i...)
}
func (l *EchoLogger) Warnf(format string, args ...interface{}) {
l.logf(sorbetLog.LevelInfo, format, args...)
}
func (l *EchoLogger) Warnj(j log.JSON) {
l.logj(sorbetLog.LevelWarn, j)
}
func (l *EchoLogger) Error(i ...interface{}) {
l.log(sorbetLog.LevelError, i...)
}
func (l *EchoLogger) Errorf(format string, args ...interface{}) {
l.logf(sorbetLog.LevelError, format, args...)
}
func (l *EchoLogger) Errorj(j log.JSON) {
l.logj(sorbetLog.LevelError, j)
}
func (l *EchoLogger) Fatal(i ...interface{}) {
l.log(sorbetLog.LevelFatal, i...)
os.Exit(1)
}
func (l *EchoLogger) Fatalj(j log.JSON) {
l.logj(sorbetLog.LevelFatal, j)
os.Exit(1)
}
func (l *EchoLogger) Fatalf(format string, args ...interface{}) {
l.logf(sorbetLog.LevelFatal, format, args...)
os.Exit(1)
}
func (l *EchoLogger) Panic(i ...interface{}) {
l.log(sorbetLog.LevelPanic, i...)
panic(fmt.Sprint(i...))
}
func (l *EchoLogger) Panicj(j log.JSON) {
l.logj(sorbetLog.LevelPanic, j)
panic(j)
}
func (l *EchoLogger) Panicf(format string, args ...interface{}) {
l.logf(sorbetLog.LevelPanic, format, args...)
panic(fmt.Sprintf(format, args...))
}
func (l *EchoLogger) log(level sorbetLog.Level, args ...any) {
var attrs []any
var formats []any
for _, arg := range args {
switch arg.(type) {
case sorbetLog.Attr:
attrs = append(attrs, attrs)
default:
formats = append(formats, args)
}
}
msg := ""
if len(formats) > 0 {
msg = fmt.Sprint(formats...)
}
l.l.Log(level, msg, attrs...)
}
func (l *EchoLogger) logf(level sorbetLog.Level, format string, args ...any) {
l.l.Log(level, format, args...)
}
func (l *EchoLogger) logj(level sorbetLog.Level, j log.JSON) {
b, err := json.Marshal(j)
if err != nil {
panic(err)
}
l.l.Log(level, string(b))
}

@ -0,0 +1,88 @@
package main
import (
"github.com/labstack/echo/v4"
"github.com/swaggo/echo-swagger"
"gorm.io/gorm"
"net/http"
_ "sorbet/docs" // 开发文档
"sorbet/internal"
"sorbet/internal/entities"
"sorbet/internal/middleware"
"sorbet/internal/repositories"
"sorbet/internal/util"
"sorbet/pkg/env"
"sorbet/pkg/ioc"
"sorbet/pkg/rsp"
)
// @title 博客系统
// @version 1.0
// @description 基于 Echo 框架的基本库
//
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
//
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
//
// @accept json
func main() {
if err := env.Init(); err != nil {
panic(err)
}
if err := internal.Init(); err != nil {
panic(err)
}
repositories.Init()
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.HTTPErrorHandler = func(err error, c echo.Context) {
if !c.Response().Committed {
http.Error(c.Response(), err.Error(), 500)
}
}
e.Logger = util.NewLogger()
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.Use(middleware.Logger)
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
db := ioc.MustGet[gorm.DB]().WithContext(c.Request().Context())
ci := ioc.Fork()
ci.Bind(db)
c.Set("db", db)
c.Set("ioc", ci)
return next(c)
}
})
e.GET("/swagger/*", echoSwagger.WrapHandler)
e.GET("/", func(c echo.Context) error {
repo := repositories.NewCompanyRepository(c.Get("db").(*gorm.DB))
//err := c.Get("ioc").(*ioc.Container).Resolve(&repo)
//if err != nil {
// return err
//}
//db := ioc.MustGet[gorm.DB]().WithContext(c.Request().Context())
//ioc.Fork().Bind(db)
//repo := ioc.MustGet[repositories.CompanyRepository]()
repo.Create(&entities.Company{Name: "海苔一诺"})
pager, err := repo.Paginate()
if err != nil {
return err
}
return rsp.Ok(c, pager)
})
e.Logger.Fatal(e.Start(":1323"))
}
func panicIf(e error) {
if e != nil {
panic(e)
}
}

@ -0,0 +1,17 @@
package bus
import "context"
var DefaultEmitter = New()
func Emit(ctx context.Context, topic string, data any) {
DefaultEmitter.Emit(ctx, topic, data)
}
func Listen(topic string, listener Listener, options ...ListenOption) {
DefaultEmitter.Listen(topic, listener, options...)
}
func Cancel(topic string, listeners ...Listener) {
DefaultEmitter.Cancel(topic, listeners...)
}

@ -0,0 +1,141 @@
package bus
import (
"context"
"github.com/rs/xid"
"reflect"
"sync"
"time"
)
type Emitter struct {
mu sync.RWMutex
topics map[string][]ListenerObject
}
func New() *Emitter {
return &Emitter{
mu: sync.RWMutex{},
topics: make(map[string][]ListenerObject),
}
}
func (e *Emitter) Emit(ctx context.Context, topic string, data any) {
e.mu.RLock()
listeners, ok := e.topics[topic]
e.mu.RUnlock()
if !ok {
return
}
stop := make(chan struct{})
txID, _ := ctx.Value("txid-key").(string)
source, _ := ctx.Value("source-key").(string)
ctx, cancel := context.WithCancel(ctx)
if txID == "" {
txID = xid.New().String()
}
event := Event{
ID: xid.New().String(),
TxID: txID,
Topic: topic,
Source: source,
OccurredAt: time.Now(),
Data: data,
stopPropagation: func() {
select {
case <-stop:
default:
close(stop)
cancel()
}
},
}
for _, listener := range listeners {
select {
case <-stop:
return
default:
if listener.once != nil {
listener.once.Do(func() {
if listener.async {
go listener.do(ctx, event)
} else {
listener.do(ctx, event)
}
e.Cancel(topic, listener.do)
})
} else if listener.async {
go listener.do(ctx, event)
} else {
listener.do(ctx, event)
}
}
}
}
func (e *Emitter) Listen(topic string, listener Listener, options ...ListenOption) {
if listener == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
if e.topics == nil {
e.topics = make(map[string][]ListenerObject)
}
listenerObject := ListenerObject{
async: false,
once: nil,
do: listener,
ptr: reflect.ValueOf(listener).Pointer(),
}
for _, option := range options {
option(&listenerObject)
}
if listeners, has := e.topics[topic]; !has {
e.topics[topic] = []ListenerObject{listenerObject}
} else {
e.topics[topic] = append(listeners, listenerObject)
}
}
func (e *Emitter) Cancel(topic string, listeners ...Listener) {
e.mu.Lock()
defer e.mu.Unlock()
if e.topics == nil {
return
}
ls, has := e.topics[topic]
if !has || len(ls) == 0 {
return
}
if len(listeners) == 0 {
delete(e.topics, topic)
return
}
for _, listener := range listeners {
for i, l := range ls {
if l.ptr == 0 {
l.ptr = reflect.ValueOf(l).Pointer()
}
if l.ptr == reflect.ValueOf(listener).Pointer() {
if i == 0 {
ls = ls[1:]
} else if i == len(ls)-1 {
ls = ls[:i]
} else {
ls = append(ls[:i], ls[i+1:]...)
}
}
}
}
if len(ls) == 0 {
delete(e.topics, topic)
} else {
e.topics[topic] = ls
}
}

@ -0,0 +1,17 @@
package bus
import "time"
type Event struct {
ID string // identifier
TxID string // transaction identifier
Topic string // topic name
Source string // source of the event
OccurredAt time.Time // creation time in nanoseconds
Data any // actual event data
stopPropagation func()
}
func (e *Event) StopPropagation() {
e.stopPropagation()
}

@ -0,0 +1,33 @@
package bus
import (
"context"
"sync"
)
type Listener func(ctx context.Context, event Event)
type ListenerObject struct {
async bool
once *sync.Once
do Listener
ptr uintptr
}
type ListenOption func(*ListenerObject)
func WithAsync(async bool) ListenOption {
return func(o *ListenerObject) {
o.async = async
}
}
func WithOnce(once bool) ListenOption {
return func(o *ListenerObject) {
if !once && o.once != nil {
o.once = nil
} else if once && o.once == nil {
o.once = new(sync.Once)
}
}
}

@ -0,0 +1,174 @@
// 将字符串转换成其它基本类型
package cast
import (
"fmt"
"reflect"
"strconv"
"strings"
)
const message = "cast: cannot cast `%v` to type `%v`"
func Uint(value string) (uint, error) {
v, err := strconv.ParseUint(value, 0, strconv.IntSize)
if err != nil {
return 0, err
}
return uint(v), nil
}
func Uint8(value string) (uint8, error) {
v, err := strconv.ParseUint(value, 0, 8)
if err != nil {
return 0, err
}
return uint8(v), nil
}
func Uint16(value string) (uint16, error) {
v, err := strconv.ParseUint(value, 0, 16)
if err != nil {
return 0, err
}
return uint16(v), nil
}
func Uint32(value string) (uint32, error) {
v, err := strconv.ParseUint(value, 0, 32)
if err != nil {
return 0, err
}
return uint32(v), nil
}
func AsUint64(value string) (uint64, error) {
v, err := strconv.ParseUint(value, 0, 64)
if err != nil {
return 0, err
}
return v, nil
}
func Int(value string) (int, error) {
v, err := strconv.ParseInt(value, 0, strconv.IntSize)
if err != nil {
return 0, fmt.Errorf(message, value, "int")
}
return int(v), nil
}
func Int8(value string) (int8, error) {
v, err := strconv.ParseInt(value, 0, 8)
if err != nil {
return 0, err
}
return int8(v), nil
}
func Int16(value string) (int16, error) {
v, err := strconv.ParseInt(value, 0, 16)
if err != nil {
return 0, err
}
return int16(v), nil
}
func Int32(value string) (int32, error) {
v, err := strconv.ParseInt(value, 0, 32)
if err != nil {
return 0, err
}
return int32(v), nil
}
func Int64(value string) (int64, error) {
v, err := strconv.ParseInt(value, 0, 64)
if err != nil {
return 0, err
}
return v, nil
}
func Float32(value string) (float32, error) {
v, err := strconv.ParseFloat(value, 64)
if err != nil {
return 0, err
}
return float32(v), nil
}
func Float64(value string) (float64, error) {
v, err := strconv.ParseFloat(value, 64)
if err != nil {
return 0, err
}
return v, nil
}
func Bool(value string) (bool, error) {
v, err := strconv.ParseBool(value)
if err != nil {
return false, err
}
return v, nil
}
// FromType casts a string value to the given reflected type.
func FromType(value string, targetType reflect.Type) (interface{}, error) {
var typeName = targetType.String()
if strings.HasPrefix(typeName, "[]") {
itemType := typeName[2:]
array := reflect.New(targetType).Elem()
for _, v := range strings.Split(value, ",") {
if item, err := FromString(strings.Trim(v, " \n\r"), itemType); err != nil {
return array.Interface(), err
} else {
array = reflect.Append(array, reflect.ValueOf(item))
}
}
return array.Interface(), nil
}
return FromString(value, typeName)
}
// FromString casts a string value to the given type name.
func FromString(value string, targetType string) (any, error) {
switch targetType {
case "int":
return Int(value)
case "int8":
return Int8(value)
case "int16":
return Int16(value)
case "int32":
return Int32(value)
case "int64":
return Int64(value)
case "uint":
return Uint(value)
case "uint8":
return Uint8(value)
case "uint16":
return Uint16(value)
case "uint32":
return Uint32(value)
case "uint64":
return AsUint64(value)
case "bool":
return Bool(value)
case "float32":
return Float32(value)
case "float64":
return Float64(value)
case "string":
return value, nil
}
return nil, fmt.Errorf("cast: type %v is not supported", targetType)
}

@ -0,0 +1,311 @@
package db
import (
"database/sql"
"errors"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gorm.io/plugin/optimisticlock"
"sorbet/pkg/env"
"sync"
"time"
)
var (
// TODO(hupeh): 使用原子性操作 atomic.Value
db *gorm.DB
lock sync.RWMutex
ErrNoCodeFirst = errors.New("no code first")
// 使用东八区时间
// https://cloud.tencent.com/developer/article/1805859
cstZone = time.FixedZone("CST", 8*3600)
)
type Version = optimisticlock.Version
type SessionConfig = gorm.Session
type BaseConfig struct {
TimeLocation *time.Location
NamingStrategy schema.Namer
Logger logger.Interface
Plugins map[string]gorm.Plugin
TablePrefix string
SingularTable bool
NameReplacer schema.Replacer
IdentifierMaxLength int
MaxIdleConns int
MaxOpenConns int
ConnMaxLifetime time.Duration
}
type Config struct {
BaseConfig
Driver string
StoreEngine string
DSN string
}
// DB 获取数据库操作实例
func DB() *gorm.DB {
lock.RLock()
if db != nil {
lock.RUnlock()
return db
}
lock.RUnlock()
lock.Lock()
db = New()
lock.Unlock()
return db
}
// SetDB 自定义操作引擎
func SetDB(engine *gorm.DB) {
lock.Lock()
defer lock.Unlock()
db = engine
}
// New 创建数据库操作引擎,初始化参数来自环境变量
func New() *gorm.DB {
engine, err := NewWithConfig(&Config{
BaseConfig: BaseConfig{
TimeLocation: cstZone,
TablePrefix: env.String("DB_PREFIX"),
SingularTable: env.Bool("DB_SINGULAR_TABLE", false),
IdentifierMaxLength: env.Int("DB_IDENTIFIER_MAX_LENGTH", 0),
Logger: &dbLogger{200 * time.Millisecond},
MaxIdleConns: env.Int("DB_MAX_IDLE_CONNS", 0),
MaxOpenConns: env.Int("DB_MAX_OPEN_CONNS", 0),
ConnMaxLifetime: env.Duration("DB_CONN_MAX_LIFETIME", 0),
},
Driver: env.String("DB_DRIVER", "sqlite3"),
StoreEngine: env.String("DB_STORE_ENGINE", "InnoDB"),
DSN: env.String("DB_DSN", "./app.db"),
})
if err != nil {
panic(err)
}
return engine
}
// NewWithConfig 通过配置创建数据库操作引擎
func NewWithConfig(config *Config) (*gorm.DB, error) {
var dialector gorm.Dialector
switch config.Driver {
case "mysql":
dialector = mysql.Open(config.DSN)
case "pgsql":
dialector = postgres.Open(config.DSN)
case "sqlite", "sqlite3":
dialector = sqlite.Open(config.DSN)
case "sqlserver":
dialector = sqlserver.Open(config.DSN)
default:
return nil, errors.New("不支持的数据库驱动:" + config.Driver)
}
engine, err := NewWithDialector(dialector, &config.BaseConfig)
if err != nil {
return nil, err
}
if config.Driver == "mysql" && config.StoreEngine != "" {
engine = engine.Set("gorm:table_options", "ENGINE="+config.StoreEngine)
}
return engine, nil
}
// NewWithDialector 通过指定的 dialector 创建数据库操作引擎
func NewWithDialector(dialector gorm.Dialector, config *BaseConfig) (*gorm.DB, error) {
engine, err := gorm.Open(dialector, &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: config.TablePrefix,
SingularTable: config.SingularTable,
NameReplacer: config.NameReplacer,
NoLowerCase: false,
IdentifierMaxLength: config.IdentifierMaxLength,
},
Logger: config.Logger,
NowFunc: func() time.Time {
if config.TimeLocation == nil {
return time.Now()
}
return time.Now().In(config.TimeLocation)
},
QueryFields: false,
})
if err != nil {
return nil, err
}
rawDB, err := engine.DB()
if err != nil {
return nil, err
}
if config.MaxIdleConns > 0 {
rawDB.SetMaxIdleConns(config.MaxIdleConns)
}
if config.MaxOpenConns > 0 {
rawDB.SetMaxOpenConns(config.MaxOpenConns)
}
if config.ConnMaxLifetime > 0 {
rawDB.SetConnMaxLifetime(config.ConnMaxLifetime)
}
return engine, nil
}
// Sync 同步数据库结构,属于代码优先模式。
//
// 在使用该方法之前需要在环境变量中开启 "DB_CODE_FIRST" 选项。
//
// 这是非常危险的操作,必须慎之又慎,因为函数将进行如下的同步操作:
// * 自动检测和创建表,这个检测是根据表的名字
// * 自动检测和新增表中的字段,这个检测是根据字段名,同时对表中多余的字段给出警告信息
// * 自动检测,创建和删除索引和唯一索引,这个检测是根据索引的一个或多个字段名,而不根据索引名称。因此这里需要注意,如果在一个有大量数据的表中引入新的索引,数据库可能需要一定的时间来建立索引。
// * 自动转换varchar字段类型到text字段类型,自动警告其它字段类型在模型和数据库之间不一致的情况。
// * 自动警告字段的默认值,是否为空信息在模型和数据库之间不匹配的情况
//
// 以上这些警告信息需要将日志的显示级别调整为Warn级别才会显示。
func Sync(beans ...any) error {
if env.Bool("DB_CODE_FIRST") {
return DB().AutoMigrate(beans...)
}
return ErrNoCodeFirst
}
// Ping ping 一下数据库连接
func Ping() error {
raw, err := DB().DB()
if err != nil {
return err
}
return raw.Ping()
}
// Stats 返回数据库统计信息
func Stats() (*sql.DBStats, error) {
raw, err := DB().DB()
if err != nil {
return nil, err
}
stats := raw.Stats()
return &stats, nil
}
// Now 这是个工具函数,返回当前时间
func Now() time.Time {
return DB().Config.NowFunc()
}
// Session 会话模式
//
// 在该模式下会创建并缓存预编译语句,从而提高后续的调用速度
func Session(config *SessionConfig) *gorm.DB {
return DB().Session(config)
}
// Model 通过模型进行下一步操作
func Model(value any) *gorm.DB {
return DB().Model(value)
}
// Table 通过数据表面进行下一步操作
func Table(name string, args ...any) *gorm.DB {
return DB().Table(name, args...)
}
// Create 通过模型创建记录
//
// 使用模型创建一条记录:
//
// user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
// ok, err := db.Create(&user) // 通过数据的指针来创建
//
// 我们还可以使用模型切边创建多项记录:
//
// users := []*User{
// User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
// User{Name: "Jackson", Age: 19, Birthday: time.Now()},
// }
// ok, err := db.Create(users) // 通过 slice 创建多条记录
func Create(value any) (bool, error) {
result := DB().Create(value)
if err := result.Error; err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}
// Save 保存模型数据,由以下两点需要注意:
//
// - 该函数会保存所有的字段,即使字段是零值。
// - 如果模型中的主键值是零值,将会创建该数据。
func Save(value any) (bool, error) {
result := DB().Save(value)
if err := result.Error; err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}
func Upsert(bean any, conflict clause.OnConflict) (bool, error) {
result := DB().Clauses(conflict).Create(bean)
if err := result.Error; err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}
// Transaction 自动事务管理
//
// 如果在 fc 中开启了新的事务,必须确保这个内嵌的事务被提交或被回滚。
func Transaction(fc func(tx *gorm.DB) error, opts ...*sql.TxOptions) error {
return DB().Transaction(fc, opts...)
}
// Begin 开启事务
//
// 使用示例
//
// tx := db.Begin() // 开始事务
// tx.Create() // 执行一些数据库操作
// tx.Rollback() // 遇到错误时回滚事务
// tx.Commit() // 否则,提交事务
//
// 事务一旦开始,就应该使用返回的 tx 对象处理数据
func Begin(opts ...*sql.TxOptions) (tx *gorm.DB) {
return DB().Begin(opts...)
}
// Raw 执行 SQL 查询语句
func Raw(sql string, values ...any) *gorm.DB {
return DB().Raw(sql, values...)
}
// Exec 执行Insert, Update, Delete 等命令的 SQL 语句,
// 如果需要查询数据请使用 Query 函数
func Exec(sql string, values ...any) *gorm.DB {
return DB().Exec(sql, values...)
}
func Unscoped() *gorm.DB {
return DB().Unscoped()
}
// Migrator 返回迁移接口
func Migrator() gorm.Migrator {
return DB().Migrator()
}

@ -0,0 +1,18 @@
package db
import "gorm.io/gorm"
type DeleteBuilder[T any] struct {
Expr
db *gorm.DB
}
func NewDeleteBuilder[T any](db *gorm.DB) *DeleteBuilder[T] {
return &DeleteBuilder[T]{Expr{}, db}
}
func (b *DeleteBuilder[T]) Commit() (int64, error) {
var t T
res := b.db.Scopes(b.Scopes).Delete(&t)
return res.RowsAffected, res.Error
}

@ -0,0 +1,174 @@
package db
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Expr struct {
clauses []clause.Expression
}
func (e *Expr) add(expr clause.Expression) *Expr {
e.clauses = append(e.clauses, expr)
return e
}
func (e *Expr) Eq(col string, val any) *Expr {
return e.add(clause.Eq{Column: col, Value: val})
}
func (e *Expr) Neq(col string, val any) *Expr {
return e.add(clause.Neq{Column: col, Value: val})
}
func (e *Expr) Lt(col string, val any) *Expr {
return e.add(clause.Lt{Column: col, Value: val})
}
func (e *Expr) Lte(col string, val any) *Expr {
return e.add(clause.Lte{Column: col, Value: val})
}
func (e *Expr) Gt(col string, val any) *Expr {
return e.add(clause.Gt{Column: col, Value: val})
}
func (e *Expr) Gte(col string, val any) *Expr {
return e.add(clause.Gte{Column: col, Value: val})
}
func (e *Expr) Between(col string, less, more any) *Expr {
return e.add(between{col, less, more})
}
func (e *Expr) NotBetween(col string, less, more any) *Expr {
return e.add(clause.Not(between{col, less, more}))
}
func (e *Expr) IsNull(col string) *Expr {
return e.add(null{col})
}
func (e *Expr) NotNull(col string) *Expr {
return e.add(clause.Not(null{col}))
}
func (e *Expr) Like(col, tpl string) *Expr {
return e.add(clause.Like{Column: col, Value: tpl})
}
func (e *Expr) NotLike(col, tpl string) *Expr {
return e.add(clause.Not(clause.Like{Column: col, Value: tpl}))
}
func (e *Expr) In(col string, values ...any) *Expr {
return e.add(clause.IN{Column: col, Values: values})
}
func (e *Expr) NotIn(col string, values ...any) *Expr {
return e.add(clause.Not(clause.IN{Column: col, Values: values}))
}
func (e *Expr) When(condition bool, then func(ex *Expr), elses ...func(ex *Expr)) *Expr {
if condition {
then(e)
} else {
for _, els := range elses {
els(e)
}
}
return e
}
func (e *Expr) Or(or func(ex *Expr)) *Expr {
other := &Expr{}
or(other)
if len(other.clauses) == 0 {
return e
}
if len(e.clauses) == 0 {
e.clauses = other.clauses[:]
return e
}
e.clauses = []clause.Expression{
clause.Or(
clause.And(e.clauses...),
clause.And(other.clauses...),
),
}
return e
}
func (e *Expr) And(and func(ex *Expr)) *Expr {
other := &Expr{}
and(other)
if len(other.clauses) == 0 {
return e
}
if len(e.clauses) == 0 {
e.clauses = other.clauses[:]
return e
}
e.clauses = []clause.Expression{
clause.And(
clause.And(e.clauses...),
clause.And(other.clauses...),
),
}
return e
}
func (e *Expr) Not(not func(ex *Expr)) *Expr {
other := &Expr{}
not(other)
if len(other.clauses) == 0 {
return e
}
return e.add(clause.Not(other.clauses...))
}
func (e *Expr) Scopes(tx *gorm.DB) *gorm.DB {
if e.clauses != nil {
for _, express := range e.clauses {
tx = tx.Where(express)
}
}
return tx
}
type null struct {
Column any
}
func (n null) Build(builder clause.Builder) {
builder.WriteQuoted(n.Column)
builder.WriteString(" IS NULL")
}
func (n null) NegationBuild(builder clause.Builder) {
builder.WriteQuoted(n.Column)
builder.WriteString(" IS NOT NULL")
}
type between struct {
Column any
Less any
More any
}
func (b between) Build(builder clause.Builder) {
b.build(builder, " BETWEEN ")
}
func (b between) NegationBuild(builder clause.Builder) {
b.build(builder, " NOT BETWEEN ")
}
func (b between) build(builder clause.Builder, op string) {
builder.WriteQuoted(b.Column)
builder.WriteString(op)
builder.AddVar(builder, b.Less)
builder.WriteString(" And ")
builder.AddVar(builder, b.More)
}

@ -0,0 +1,57 @@
package db
//
//import (
// "context"
// "gorm.io/gorm/dbLogger"
// "io"
// "log"
// "os"
// "time"
//)
//
//type dbLogger struct {
// console dbLogger.Interface
// persist dbLogger.Interface
//}
//
//func NewLogger(persistWriter io.Writer) dbLogger.Interface {
// return &dbLogger{
// console: dbLogger.New(log.New(os.Stdout, "", log.Ltime|log.Lmicroseconds), dbLogger.Config{
// SlowThreshold: 200 * time.Millisecond,
// Colorful: true,
// LogLevel: dbLogger.Info,
// }),
// persist: dbLogger.New(log.New(persistWriter, "\r\n", log.LstdFlags), dbLogger.Config{
// SlowThreshold: 200 * time.Millisecond,
// LogLevel: dbLogger.Info,
// }),
// }
//}
//
//func (l *dbLogger) LogMode(level dbLogger.LogLevel) dbLogger.Interface {
// c := *l
// c.console = c.console.LogMode(level)
// c.persist = c.persist.LogMode(level)
// return &c
//}
//
//func (l *dbLogger) Info(ctx context.Context, s string, i ...interface{}) {
// l.console.Info(ctx, s, i...)
// l.persist.Info(ctx, s, i...)
//}
//
//func (l *dbLogger) Warn(ctx context.Context, s string, i ...interface{}) {
// l.console.Warn(ctx, s, i...)
// l.persist.Warn(ctx, s, i...)
//}
//
//func (l *dbLogger) Error(ctx context.Context, s string, i ...interface{}) {
// l.console.Error(ctx, s, i...)
// l.persist.Error(ctx, s, i...)
//}
//
//func (l *dbLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
// l.console.Trace(ctx, begin, fc, err)
// l.persist.Trace(ctx, begin, fc, err)
//}

@ -0,0 +1,67 @@
package db
import (
"context"
"errors"
"fmt"
glog "gorm.io/gorm/logger"
"sorbet/pkg/log"
"time"
)
type dbLogger struct {
SlowThreshold time.Duration
}
// LogMode log mode
func (l *dbLogger) LogMode(level glog.LogLevel) glog.Interface {
return l
}
// Info print info
func (l dbLogger) Info(ctx context.Context, msg string, data ...any) {
log.Info(msg, data...)
}
// Warn print warn messages
func (l dbLogger) Warn(ctx context.Context, msg string, data ...any) {
log.Warn(msg, data...)
}
// Error print error messages
func (l dbLogger) Error(ctx context.Context, msg string, data ...any) {
log.Error(msg, data...)
}
// Trace print sql message
func (l dbLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
elapsed := time.Since(begin)
switch {
case err != nil && !errors.Is(err, glog.ErrRecordNotFound):
sql, rows := fc()
if rows == -1 {
log.Error("%s [rows:%v] %s [%.3fms]", err, "-", sql, float64(elapsed.Nanoseconds())/1e6)
} else {
log.Error("%s [rows:%v] %s [%.3fms]", err, rows, sql, float64(elapsed.Nanoseconds())/1e6)
}
case elapsed > l.SlowThreshold && l.SlowThreshold != 0:
sql, rows := fc()
slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)
if rows == -1 {
log.Warn("%s [rows:%v] %s [%.3fms]", slowLog, "-", sql, float64(elapsed.Nanoseconds())/1e6)
} else {
log.Warn("%s [rows:%v] %s [%.3fms]", slowLog, rows, sql, float64(elapsed.Nanoseconds())/1e6)
}
default:
sql, rows := fc()
if rows == -1 {
log.Trace("[rows:%v] %s [%.3fms]", "-", sql, float64(elapsed.Nanoseconds())/1e6, log.RawLevel("GORM"))
} else {
log.Trace("[rows:%v] %s [%.3fms]", rows, sql, float64(elapsed.Nanoseconds())/1e6, log.RawLevel("GORM"))
}
}
}
func (l dbLogger) ParamsFilter(ctx context.Context, sql string, params ...any) (string, []any) {
return sql, params
}

@ -0,0 +1 @@
package db

@ -0,0 +1,191 @@
package db
import (
"database/sql"
"gorm.io/gorm"
"math"
)
// QueryBuilder 查询构造器
// TODO(hupeh):实现 joins 和表别名
type QueryBuilder[T any] struct {
Expr
db *gorm.DB
selects []string
omits []string
orders []string
limit int
offset int
distinct []any
preloads []preload
}
func NewQueryBuilder[T any](db *gorm.DB) *QueryBuilder[T] {
return &QueryBuilder[T]{Expr: Expr{}, db: db}
}
type preload struct {
query string
args []any
}
type Pager[T any] struct {
Total int `json:"total" xml:"total"` // 数据总数
Page int `json:"page" xml:"page"` // 当前页码
Limit int `json:"limit" xml:"limit"` // 数据容量
Items []*T `json:"items" xml:"items"` // 数据列表
}
func (q *QueryBuilder[T]) Select(columns ...string) *QueryBuilder[T] {
q.selects = append(q.selects, columns...)
return q
}
func (q *QueryBuilder[T]) Omit(columns ...string) *QueryBuilder[T] {
q.omits = append(q.omits, columns...)
return q
}
func (q *QueryBuilder[T]) DescentBy(columns ...string) *QueryBuilder[T] {
for _, col := range columns {
q.orders = append(q.orders, col+" DESC")
}
return q
}
func (q *QueryBuilder[T]) AscentBy(columns ...string) *QueryBuilder[T] {
for _, col := range columns {
q.orders = append(q.orders, col)
}
return q
}
func (q *QueryBuilder[T]) Limit(limit int) *QueryBuilder[T] {
q.limit = limit
return q
}
func (q *QueryBuilder[T]) Offset(offset int) *QueryBuilder[T] {
q.offset = offset
return q
}
func (q *QueryBuilder[T]) Distinct(columns ...any) *QueryBuilder[T] {
q.distinct = append(q.distinct, columns...)
return q
}
func (q *QueryBuilder[T]) Preload(query string, args ...any) *QueryBuilder[T] {
q.preloads = append(q.preloads, preload{query, args})
return q
}
func (q *QueryBuilder[T]) Scopes(tx *gorm.DB) *gorm.DB {
tx = q.scopesWithoutEffect(tx)
if q.orders != nil {
for _, order := range q.orders {
tx = tx.Order(order)
}
}
if q.limit > 0 {
tx = tx.Limit(q.limit)
}
if q.offset > 0 {
tx = tx.Offset(q.offset)
}
if q.preloads != nil {
for _, pl := range q.preloads {
tx = tx.Preload(pl.query, pl.args...)
}
}
return tx
}
func (q *QueryBuilder[T]) scopesWithoutEffect(tx *gorm.DB) *gorm.DB {
var entity T
tx = tx.Model(&entity)
if q.selects != nil {
tx = tx.Select(q.selects)
}
if q.omits != nil {
tx = tx.Omit(q.omits...)
}
if len(q.distinct) > 0 {
tx = tx.Distinct(q.distinct...)
}
return q.Expr.Scopes(tx)
}
func (q *QueryBuilder[T]) Count() (int64, error) {
var count int64
err := q.db.Scopes(q.scopesWithoutEffect).Count(&count).Error
return count, err
}
func (q *QueryBuilder[T]) First(entity any) error {
return q.db.Scopes(q.Scopes).First(entity).Error
}
func (q *QueryBuilder[T]) Take(entity any) error {
return q.db.Scopes(q.Scopes).Take(entity).Error
}
func (q *QueryBuilder[T]) Last(entity any) error {
return q.db.Scopes(q.Scopes).Last(entity).Error
}
func (q *QueryBuilder[T]) Find(entity any) error {
return q.db.Scopes(q.Scopes).Find(entity).Error
}
func (q *QueryBuilder[T]) Paginate() (*Pager[T], error) {
if q.limit <= 0 {
q.limit = 30
}
if q.offset < 0 {
q.offset = 0
}
count, err := q.Count()
if err != nil {
return nil, err
}
var items []*T
err = q.Find(&items)
if err != nil {
return nil, err
}
return &Pager[T]{
Total: int(count),
Page: int(math.Ceil(float64(q.offset)/float64(q.limit))) + 1,
Limit: q.limit,
Items: items,
}, nil
}
// Rows 返回行数据迭代器
//
// 使用示例:
//
// rows, err := q.Eq("name", "jack").Rows()
// if err != nil {
// panic(err)
// }
// defer rows.Close()
// for rows.Next() {
// var user User
// db.ScanRows(rows, &user)
// // do something
// }
func (q *QueryBuilder[T]) Rows() (*sql.Rows, error) {
return q.db.Scopes(q.Scopes).Rows()
}
// Pluck 获取指定列的值
//
// 示例:
//
// var names []string
// q.Pluck("name", &names)
func (q *QueryBuilder[T]) Pluck(column string, dest any) error {
return q.db.Scopes(q.Scopes).Pluck(column, dest).Error
}

@ -0,0 +1 @@
package db

@ -0,0 +1,98 @@
package db
import (
"gorm.io/gorm"
)
type Repository[T any] struct {
db *gorm.DB
pk string // 默认 id
}
func NewRepository[T any](db ...*gorm.DB) *Repository[T] {
for _, d := range db {
return NewRepositoryWith[T](d)
}
return NewRepositoryWith[T](DB())
}
func NewRepositoryWith[T any](db *gorm.DB, pk ...string) *Repository[T] {
r := &Repository[T]{db: db, pk: "id"}
for _, s := range pk {
if s != "" {
r.pk = s
return r
}
}
return r
}
// Create 创建数据
func (r *Repository[T]) Create(entity *T) error {
return r.db.Model(&entity).Create(&entity).Error
}
func (r *Repository[T]) Delete(expr *Expr) (int64, error) {
var entity T
res := r.db.Model(&entity).Scopes(expr.Scopes).Delete(&entity)
return res.RowsAffected, res.Error
}
func (r *Repository[T]) DeleteByID(id any) error {
var entity T
return r.db.Delete(&entity, r.pk, id).Error
}
func (r *Repository[T]) Update(expr *Expr, values map[string]any) (int64, error) {
res := r.db.Scopes(expr.Scopes).Updates(values)
return res.RowsAffected, res.Error
}
func (r *Repository[T]) UpdateByID(id any, values map[string]any) error {
var entity T
return r.db.Model(&entity).Where(r.pk, id).Updates(values).Error
}
func (r *Repository[T]) GetByID(id any) (*T, error) {
var entity T
err := r.db.Model(&entity).Where(r.pk, id).First(&entity).Error
if err != nil {
return nil, err
}
return &entity, nil
}
func (r *Repository[T]) Find(expr ...*Expr) ([]*T, error) {
var entity T
var items []*T
err := r.db.Model(&entity).Scopes(func(tx *gorm.DB) *gorm.DB {
for _, e := range expr {
tx = e.Scopes(tx)
}
return tx
}).Find(&items).Error
if err != nil {
return nil, err
}
return items, nil
}
func (r *Repository[T]) Paginate(expr ...*Expr) (*Pager[T], error) {
qb := NewQueryBuilder[T](r.db)
for _, e := range expr {
qb.Expr = *e
}
return qb.Paginate()
}
func (r *Repository[T]) NewDeleteBuilder() *DeleteBuilder[T] {
return NewDeleteBuilder[T](r.db)
}
func (r *Repository[T]) NewUpdateBuilder() *UpdateBuilder[T] {
return NewUpdateBuilder[T](r.db)
}
func (r *Repository[T]) NewQueryBuilder() *QueryBuilder[T] {
return NewQueryBuilder[T](r.db)
}

@ -0,0 +1,67 @@
package db
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"strconv"
"time"
)
type NullTime struct {
sql.NullTime
}
func (v NullTime) MarshalJSON() ([]byte, error) {
if v.Valid {
return json.Marshal(v.Time)
} else {
return json.Marshal(nil)
}
}
func (v *NullTime) UnmarshalJSON(data []byte) error {
var s *time.Time
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s != nil {
v.Valid = true
v.Time = *s
} else {
v.Valid = false
}
return nil
}
type NullInt64 sql.NullInt64
func (v *NullInt64) Scan(value interface{}) error {
return (*sql.NullInt64)(v).Scan(value)
}
func (v NullInt64) Value() (driver.Value, error) {
if !v.Valid {
return nil, nil
}
return v.Int64, nil
}
func (v *NullInt64) UnmarshalJSON(bytes []byte) error {
if string(bytes) == "null" {
v.Valid = false
return nil
}
err := json.Unmarshal(bytes, &v.Int64)
if err == nil {
v.Valid = true
}
return err
}
func (v NullInt64) MarshalJSON() ([]byte, error) {
if v.Valid {
return strconv.AppendInt(nil, v.Int64, 10), nil
}
return []byte("null"), nil
}

@ -0,0 +1,66 @@
package db
import (
"errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type UpdateBuilder[T any] struct {
Expr
db *gorm.DB
selects []string
omits []string
onConflict *clause.OnConflict
}
func NewUpdateBuilder[T any](db *gorm.DB) *UpdateBuilder[T] {
return &UpdateBuilder[T]{Expr: Expr{}, db: db}
}
func (b *UpdateBuilder[T]) Select(columns ...string) *UpdateBuilder[T] {
b.selects = append(b.selects, columns...)
return b
}
func (b *UpdateBuilder[T]) Omit(columns ...string) *UpdateBuilder[T] {
b.omits = append(b.omits, columns...)
return b
}
func (b *UpdateBuilder[T]) OnConflict(conflict clause.OnConflict) *UpdateBuilder[T] {
if b.onConflict == nil {
b.onConflict = &conflict
} else {
b.onConflict.Columns = conflict.Columns
b.onConflict.Where = conflict.Where
b.onConflict.TargetWhere = conflict.TargetWhere
b.onConflict.OnConstraint = conflict.OnConstraint
b.onConflict.DoNothing = conflict.DoNothing
b.onConflict.DoUpdates = conflict.DoUpdates
b.onConflict.UpdateAll = conflict.UpdateAll
}
return b
}
func (b *UpdateBuilder[T]) Scopes(tx *gorm.DB) *gorm.DB {
if b.selects != nil {
tx = tx.Select(b.selects)
}
if b.omits != nil {
tx = tx.Omit(b.omits...)
}
return b.Expr.Scopes(tx)
}
func (b *UpdateBuilder[T]) Commit(values map[string]any) (int64, error) {
var entity T
res := b.db.Model(&entity).Scopes(b.Scopes).Updates(values)
if err := res.Error; err != nil {
return res.RowsAffected, err
}
if res.RowsAffected == 0 {
return 0, errors.New("no record updated")
}
return res.RowsAffected, nil
}

153
pkg/env/env.go vendored

@ -0,0 +1,153 @@
package env
import (
"errors"
"os"
"path/filepath"
"strings"
"time"
)
// 缓存的环境变量
var env = New()
// Init 加载运行目录下的 .env 文件
func Init() error {
if path, err := filepath.Abs("."); err != nil {
return err
} else {
return InitWithDir(path)
}
}
// InitWithDir 加载指定录下的 .env 文件
func InitWithDir(dir string) error {
// 重置缓存的环境变量
clear(env)
// 加载系统的环境变量
for _, value := range os.Environ() {
parts := strings.SplitN(value, "=", 2)
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
env[key] = val
}
// 将 windows 系统风格的目录分隔符替换为 Unix 风格,
// 并且排除尾部的目录分隔符。
dir = strings.TrimSuffix(strings.ReplaceAll(dir, "\\", "/"), "/")
// 加载 .env 文件
if err := loadEnv(dir, ""); err != nil {
return err
}
// 加载 .env.{APP_ENV} 文件
if appEnv := String("APP_ENV", "prod"); len(appEnv) > 0 {
if err := loadEnv(dir, "."+strings.ToLower(appEnv)); err != nil {
return err
}
}
return nil
}
func loadEnv(dir, env string) error {
if err := Load(dir + "/.env" + env); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
}
if err := Load(dir + "/.env" + env + ".local"); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
}
return nil
}
// Load 加载指定的环境变量文件
func Load(filenames ...string) error {
return env.Load(filenames...)
}
// Is 判断是不是期望的值
func Is(name string, value any) bool {
switch value.(type) {
case bool:
return Bool(name) == value
case int:
return Int(name) == value
case time.Duration:
return Duration(name) == value
default:
return String(name) == value
}
}
// IsEnv 判断应用环境是否与给出的一致
func IsEnv(env string) bool {
return Is("APP_ENV", env)
}
// Lookup 查看配置
func Lookup(name string) (string, bool) {
return env.Lookup(name)
}
// Exists 配置是否存在
func Exists(name string) bool {
return env.Exists(name)
}
// String 取字符串值
func String(name string, value ...string) string {
return env.String(name, value...)
}
// Bytes 取二进制值
func Bytes(name string, value ...[]byte) []byte {
return env.Bytes(name, value...)
}
// Int 取整型值
func Int(name string, value ...int) int {
return env.Int(name, value...)
}
func Duration(name string, value ...time.Duration) time.Duration {
return env.Duration(name, value...)
}
func Bool(name string, value ...bool) bool {
return env.Bool(name, value...)
}
// List 将值按 `,` 分割并返回
func List(name string, fallback ...[]string) []string {
return env.List(name, fallback...)
}
func Map(prefix string) map[string]string {
return env.Map(prefix)
}
func Where(filter func(name string, value string) bool) map[string]string {
return env.Where(filter)
}
// Fill 将环境变量填充到指定结构体
func Fill(structure any) error {
return env.Fill(structure)
}
// All 返回所有值
func All() map[string]string {
clone := make(map[string]string)
for key, value := range env {
clone[key] = value
}
return clone
}

186
pkg/env/environ.go vendored

@ -0,0 +1,186 @@
package env
import (
"errors"
"fmt"
"github.com/joho/godotenv"
"reflect"
"sorbet/pkg/cast"
"strconv"
"strings"
"time"
"unsafe"
)
type Environ map[string]string
func New() Environ {
return make(Environ)
}
// Load 加载环境变量文件
func (e Environ) Load(filenames ...string) error {
data, err := godotenv.Read(filenames...)
if err != nil {
return err
}
for key, value := range data {
e[key] = value
}
return nil
}
// Lookup 查看环境变量值,如果不存在或值为空,返回的第二个参数的值则为false。
func (e Environ) Lookup(key string) (string, bool) {
val, exists := e[key]
if exists && len(val) == 0 {
exists = false
}
return val, exists
}
// Exists 判断环境变量是否存在
func (e Environ) Exists(key string) bool {
_, exists := e[key]
return exists
}
// String 取字符串值
func (e Environ) String(key string, fallback ...string) string {
if value, exists := e.Lookup(key); exists {
return value
}
for _, value := range fallback {
return value
}
return ""
}
// Bytes 取二进制值
func (e Environ) Bytes(key string, fallback ...[]byte) []byte {
if value, exists := e.Lookup(key); exists {
return []byte(value)
}
for _, bytes := range fallback {
return bytes
}
return []byte{}
}
// Int 取整型值
func (e Environ) Int(key string, fallback ...int) int {
if val, exists := e.Lookup(key); exists {
if n, err := strconv.Atoi(val); err == nil {
return n
}
}
for _, value := range fallback {
return value
}
return 0
}
func (e Environ) Duration(key string, fallback ...time.Duration) time.Duration {
if val, ok := e.Lookup(key); ok {
n, err := strconv.Atoi(val)
if err == nil {
return time.Duration(n)
}
if d, err := time.ParseDuration(val); err == nil {
return d
}
}
for _, value := range fallback {
return value
}
return time.Duration(0)
}
func (e Environ) Bool(key string, fallback ...bool) bool {
if val, ok := e.Lookup(key); ok {
bl, err := strconv.ParseBool(val)
if err == nil {
return bl
}
}
for _, value := range fallback {
return value
}
return false
}
// List 将值按 `,` 分割并返回
func (e Environ) List(key string, fallback ...[]string) []string {
if value, ok := e.Lookup(key); ok {
parts := strings.Split(value, ",")
for i, part := range parts {
parts[i] = strings.Trim(part, " \n\r")
}
return parts
}
for _, value := range fallback {
return value
}
return []string{}
}
// Map 获取指定前缀的所有值
func (e Environ) Map(prefix string) map[string]string {
result := map[string]string{}
for k, v := range e {
if strings.HasPrefix(k, prefix) {
name := strings.TrimPrefix(k, prefix)
result[name] = strings.TrimSpace(v)
}
}
return result
}
// Where 获取符合过滤器的所有值
func (e Environ) Where(filter func(name, value string) bool) map[string]string {
result := map[string]string{}
for k, v := range e {
if filter(k, v) {
result[k] = v
}
}
return result
}
// Fill 将环境变量填充到指定结构体
func (e Environ) Fill(structure any) error {
inputType := reflect.TypeOf(structure)
if inputType != nil && inputType.Kind() == reflect.Ptr && inputType.Elem().Kind() == reflect.Struct {
return e.fillStruct(reflect.ValueOf(structure).Elem())
}
return errors.New("env: invalid structure")
}
func (e Environ) fillStruct(s reflect.Value) error {
for i := 0; i < s.NumField(); i++ {
if t, exist := s.Type().Field(i).Tag.Lookup("env"); exist {
if osv := e[t]; osv != "" {
v, err := cast.FromType(osv, s.Type().Field(i).Type)
if err != nil {
return fmt.Errorf("env: cannot set `%v` field; err: %v", s.Type().Field(i).Name, err)
}
ptr := reflect.NewAt(s.Field(i).Type(), unsafe.Pointer(s.Field(i).UnsafeAddr())).Elem()
ptr.Set(reflect.ValueOf(v))
}
} else if s.Type().Field(i).Type.Kind() == reflect.Struct {
if err := e.fillStruct(s.Field(i)); err != nil {
return err
}
} else if s.Type().Field(i).Type.Kind() == reflect.Ptr {
if s.Field(i).IsZero() == false && s.Field(i).Elem().Type().Kind() == reflect.Struct {
if err := e.fillStruct(s.Field(i).Elem()); err != nil {
return err
}
}
}
}
return nil
}

@ -0,0 +1,228 @@
package ioc
import (
"errors"
"fmt"
"reflect"
)
type binding struct {
name string
typ reflect.Type
resolver any
shared bool
}
func (b *binding) make(c *Container) (reflect.Value, error) {
if v, exists := c.instances[b.typ][b.name]; exists {
return v, nil
}
val, err := c.Invoke(b.resolver)
if err != nil {
return reflect.Value{}, err
}
rv := val[0]
if len(val) == 2 {
err = val[1].Interface().(error)
if err != nil {
return reflect.Value{}, err
}
}
if b.shared {
if _, exists := c.instances[b.typ]; !exists {
c.instances[b.typ] = make(map[string]reflect.Value)
}
c.instances[b.typ][b.name] = rv
}
return rv, nil
}
func (b *binding) mustMake(c *Container) reflect.Value {
val, err := b.make(c)
if err != nil {
panic(err)
}
return val
}
type Container struct {
// 注册的工厂函数
factories map[reflect.Type]map[string]*binding
// 注册的共享实例
instances map[reflect.Type]map[string]reflect.Value
parent *Container
}
func New() *Container {
return &Container{
factories: make(map[reflect.Type]map[string]*binding),
instances: make(map[reflect.Type]map[string]reflect.Value),
parent: nil,
}
}
// Fork 分支
func (c *Container) Fork() *Container {
ioc := New()
ioc.parent = c
return ioc
}
// Bind 绑定值到容器,有效类型:
// - 接口的具体实现值
// - 结构体的实例
// - 类型的值(尽量不要使用原始类型,而应该使用元素类型的变体)
func (c *Container) Bind(instance any) {
c.NamedBind("", instance)
}
// NamedBind 绑定具名值到容器
func (c *Container) NamedBind(name string, instance any) {
//typ := InterfaceOf(instance)
typ := reflect.TypeOf(instance)
if _, ok := c.instances[typ]; !ok {
c.instances[typ] = make(map[string]reflect.Value)
}
c.instances[typ][name] = reflect.ValueOf(instance)
}
// Factory 绑定工厂函数
func (c *Container) Factory(factory any, shared ...bool) error {
return c.NamedFactory("", factory, shared...)
}
// NamedFactory 绑定具名工厂函数
func (c *Container) NamedFactory(name string, factory any, shared ...bool) error {
reflectedFactory := reflect.TypeOf(factory)
if reflectedFactory.Kind() != reflect.Func {
return errors.New("container: the factory must be a function")
}
if returnCount := reflectedFactory.NumOut(); returnCount == 0 || returnCount > 2 {
return errors.New("container: factory function signature is invalid - it must return abstract, or abstract and error")
}
// TODO(hupeh): 验证第二个参数必须是 error 接口
concreteType := reflectedFactory.Out(0)
for i := 0; i < reflectedFactory.NumIn(); i++ {
// 循环依赖
if reflectedFactory.In(i) == concreteType {
return fmt.Errorf("container: factory function signature is invalid - depends on abstract it returns")
}
}
if _, exists := c.factories[concreteType]; !exists {
c.factories[concreteType] = make(map[string]*binding)
}
bd := &binding{
name: name,
typ: concreteType,
resolver: factory,
shared: false,
}
for _, b := range shared {
bd.shared = b
}
c.factories[concreteType][name] = bd
return nil
}
// Resolve 完成的注入
func (c *Container) Resolve(i any) error {
v := reflect.ValueOf(i)
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return errors.New("must given a struct")
}
t := v.Type()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
structField := t.Field(i)
inject, willInject := structField.Tag.Lookup("inject")
if !f.CanSet() {
if willInject {
return fmt.Errorf("container: cannot make %v field", t.Field(i).Name)
}
continue
}
ft := f.Type()
fv := c.NamedGet(inject, ft)
if !fv.IsValid() {
return fmt.Errorf("value not found for type %v", ft)
}
f.Set(fv)
}
return nil
}
// Get 获取指定类型的值
func (c *Container) Get(t reflect.Type) reflect.Value {
return c.NamedGet("", t)
}
// NamedGet 通过注入的名称获取指定类型的值
func (c *Container) NamedGet(name string, t reflect.Type) reflect.Value {
val, exists := c.instances[t][name]
if exists && val.IsValid() {
return val
}
if factory, exists := c.factories[t][name]; exists {
val = factory.mustMake(c)
}
if val.IsValid() || t.Kind() != reflect.Interface {
goto RESULT
}
// 使用共享值里面该接口的实现者
for k, v := range c.instances {
if k.Implements(t) {
for _, value := range v {
if value.IsValid() {
val = value
goto RESULT
}
}
break
}
}
// 使用工厂函数里面该接口的实现者
for k, v := range c.factories {
if k.Implements(t) {
for _, bd := range v {
if x := bd.mustMake(c); x.IsValid() {
val = x
goto RESULT
}
}
break
}
}
RESULT:
if !val.IsValid() && c.parent != nil {
val = c.parent.NamedGet(name, t)
}
return val
}
// Invoke 执行函数
func (c *Container) Invoke(f any) ([]reflect.Value, error) {
t := reflect.TypeOf(f)
if t.Kind() != reflect.Func {
return nil, errors.New("container: invalid function")
}
var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func
for i := 0; i < t.NumIn(); i++ {
argType := t.In(i)
val := c.Get(argType)
if !val.IsValid() {
return nil, fmt.Errorf("value not found for type %v", argType)
}
in[i] = val
}
return reflect.ValueOf(f).Call(in), nil
}

@ -0,0 +1,97 @@
package ioc
import (
"context"
"errors"
"reflect"
)
var (
ErrValueNotFound = errors.New("ioc: value not found")
)
var global = New()
// Fork 分支
func Fork() *Container {
return global.Fork()
}
// Bind 绑定值到容器,有效类型:
//
// - 接口的具体实现值
// - 结构体的实例
// - 类型的值(尽量不要使用原始类型,而应该使用元素类型的变体)
func Bind(instance any) {
global.Bind(instance)
}
// NamedBind 绑定具名值到容器
func NamedBind(name string, instance any) {
global.NamedBind(name, instance)
}
// Factory 绑定工厂函数
func Factory(factory any, shared ...bool) error {
return global.Factory(factory, shared...)
}
func MustFactory(factory any, shared ...bool) {
err := Factory(factory, shared...)
if err != nil {
panic(err)
}
}
// NamedFactory 绑定具名工厂函数
func NamedFactory(name string, factory any, shared ...bool) error {
return global.NamedFactory(name, factory, shared...)
}
func MustNamedFactory(name string, factory any, shared ...bool) {
err := NamedFactory(name, factory, shared...)
if err != nil {
panic(err)
}
}
// Resolve 完成的注入
func Resolve(i any) error {
return global.Resolve(i)
}
// Get 获取指定类型的值
func Get[T any](ctx context.Context) (*T, error) {
return NamedGet[T](ctx, "")
}
func MustGet[T any](ctx context.Context) *T {
return MustNamedGet[T](ctx, "")
}
// NamedGet 通过注入的名称获取指定类型的值
func NamedGet[T any](ctx context.Context, name string) (*T, error) {
var abs T
t := reflect.TypeOf(&abs)
v := global.NamedGet(name, t)
if !v.IsValid() {
return nil, ErrValueNotFound
}
if x, ok := v.Interface().(*T); ok {
return x, nil
}
return nil, ErrValueNotFound
}
func MustNamedGet[T any](ctx context.Context, name string) *T {
v, err := NamedGet[T](ctx, name)
if err != nil {
panic(err)
}
return v
}
// Invoke 执行函数
func Invoke(f any) ([]reflect.Value, error) {
return global.Invoke(f)
}

@ -0,0 +1,19 @@
package ioc
import "reflect"
// InterfaceOf dereferences a pointer to an Interface type.
// It panics if value is not a pointer to an interface.
func InterfaceOf(value interface{}) reflect.Type {
t := reflect.TypeOf(value)
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Interface {
panic("the value is not a pointer to an interface. (*MyInterface)(nil)")
}
return t
}

@ -0,0 +1,80 @@
package log
import (
"log/slog"
"time"
)
const (
rawTimeKey = "@rawTime"
rawLevelKey = "@rawLevel"
)
type Attr = slog.Attr
func RawTime(value time.Time) Attr {
return Time(rawTimeKey, value)
}
func RawLevel(value string) Attr {
return String(rawLevelKey, value)
}
// String returns an Attr for a string value.
func String(key, value string) Attr {
return slog.String(key, value)
}
// Int64 returns an Attr for an int64.
func Int64(key string, value int64) Attr {
return slog.Int64(key, value)
}
// Int converts an int to an int64 and returns
// an Attr with that value.
func Int(key string, value int) Attr {
return slog.Int(key, value)
}
// Uint64 returns an Attr for a uint64.
func Uint64(key string, v uint64) Attr {
return slog.Uint64(key, v)
}
// Float64 returns an Attr for a floating-point number.
func Float64(key string, v float64) Attr {
return slog.Float64(key, v)
}
// Bool returns an Attr for a bool.
func Bool(key string, v bool) Attr {
return slog.Bool(key, v)
}
// Time returns an Attr for a time.Time.
// It discards the monotonic portion.
func Time(key string, v time.Time) Attr {
return slog.Time(key, v)
}
// Duration returns an Attr for a time.Duration.
func Duration(key string, v time.Duration) Attr {
return slog.Duration(key, v)
}
// Group returns an Attr for a Group Instance.
// The first argument is the key; the remaining arguments
// are converted to Attrs as in [Logger.Log].
//
// Use Group to collect several key-value pairs under a single
// key on a log line, or as the result of LogValue
// in order to log a single value as multiple Attrs.
func Group(key string, args ...any) Attr {
return slog.Group(key, args...)
}
// Any returns an Attr for the supplied value.
// See [AnyValue] for how values are treated.
func Any(key string, value any) Attr {
return slog.Any(key, value)
}

@ -0,0 +1,71 @@
package log
import (
"github.com/mattn/go-isatty"
"os"
)
type Colorer interface {
Black(s string) string
Red(s string) string
Green(s string) string
Yellow(s string) string
Blue(s string) string
Magenta(s string) string
Cyan(s string) string
White(s string) string
Grey(s string) string
}
func NewColorer() Colorer {
return &colorer{}
}
type clr string
const (
black clr = "\x1b[30m"
red clr = "\x1b[31m"
green clr = "\x1b[32m"
yellow clr = "\x1b[33m"
blue clr = "\x1b[34m"
magenta clr = "\x1b[35m"
cyan clr = "\x1b[36m"
white clr = "\x1b[37m"
grey clr = "\x1b[90m"
)
var (
// NoColor defines if the output is colorized or not. It's dynamically set to
// false or true based on the stdout's file descriptor referring to a terminal
// or not. It's also set to true if the NO_COLOR environment variable is
// set (regardless of its value). This is a global option and affects all
// colors. For more control over each Color block use the methods
// DisableColor() individually.
noColor = noColorIsSet() || os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
)
// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string.
func noColorIsSet() bool {
return os.Getenv("NO_COLOR") != ""
}
func (c clr) f(s string) string {
if noColorIsSet() || noColor {
return s
}
return string(c) + s + "\x1b[0m"
}
type colorer struct{}
func (c *colorer) Black(s string) string { return black.f(s) }
func (c *colorer) Red(s string) string { return red.f(s) }
func (c *colorer) Green(s string) string { return green.f(s) }
func (c *colorer) Yellow(s string) string { return yellow.f(s) }
func (c *colorer) Blue(s string) string { return blue.f(s) }
func (c *colorer) Magenta(s string) string { return magenta.f(s) }
func (c *colorer) Cyan(s string) string { return cyan.f(s) }
func (c *colorer) White(s string) string { return white.f(s) }
func (c *colorer) Grey(s string) string { return grey.f(s) }

@ -0,0 +1,164 @@
package log
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"log/slog"
"runtime"
"strings"
"sync/atomic"
"time"
"unsafe"
)
type Handler struct {
ctx unsafe.Pointer // 结构体 logger
color Colorer
slog.Handler
attrs []Attr
l *log.Logger
}
func (h *Handler) WithAttrs(attrs []Attr) slog.Handler {
return &Handler{
ctx: h.ctx,
color: h.color,
Handler: h.Handler.WithAttrs(attrs),
attrs: append(h.attrs, attrs...),
l: h.l,
}
}
func (h *Handler) WithGroup(name string) slog.Handler {
return &Handler{
ctx: h.ctx,
color: h.color,
Handler: h.Handler.WithGroup(name),
l: h.l,
}
}
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
// 参考 atomic.Pointer#Load 实现
l := (*logger)(atomic.LoadPointer(&h.ctx))
if atomic.LoadInt32(&l.discard) != 1 {
if len(h.attrs) > 0 {
x := slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
r.Attrs(func(attr slog.Attr) bool {
x.AddAttrs(attr)
return true
})
x.AddAttrs(h.attrs...)
if err := h.Print(l, x); err != nil {
return err
}
} else if err := h.Print(l, r); err != nil {
return err
}
}
if atomic.LoadInt32(&l.unpersist) != 1 {
return h.Handler.Handle(ctx, r)
}
return nil
}
func (h *Handler) Print(l *logger, r slog.Record) error {
flags := l.Flags()
colorful := flags&Lcolor != 0
level := parseSlogLevel(r.Level)
levelStr := level.String()
var fields map[string]any
if numAttrs := r.NumAttrs(); numAttrs > 0 {
fields = make(map[string]any, numAttrs)
r.Attrs(func(a Attr) bool {
switch a.Key {
case rawTimeKey:
r.Time = a.Value.Any().(time.Time)
case rawLevelKey:
levelStr = a.Value.Any().(string)
default:
fields[a.Key] = a.Value.Any()
}
return true
})
}
var output string
var sep string
write := func(s string) {
if sep == "" {
sep = " "
} else {
output += sep
}
output += s
}
if flags&(Ldate|Ltime|Lmicroseconds) != 0 {
t := r.Time.In(l.Timezone()) // todo 原子操作
if flags&Ldate != 0 {
write(t.Format("2006/01/02"))
}
if flags&(Ltime|Lmicroseconds) != 0 {
if flags&Lmicroseconds != 0 {
write(t.Format("15:04:05.000"))
} else {
write(t.Format("15:04:05"))
}
}
}
if colorful {
// TODO(hupeh): 重新设计不同颜色
colorize := identify
switch level {
case LevelDebug:
colorize = h.color.Cyan
case LevelInfo:
colorize = h.color.Blue
case LevelWarn:
colorize = h.color.Yellow
case LevelError:
colorize = h.color.Red
case LevelFatal, LevelPanic:
colorize = h.color.Magenta
}
write(h.color.Grey("[") + colorize(levelStr) + h.color.Grey("]"))
write(colorize(r.Message))
} else {
write("[" + levelStr + "]")
write(r.Message)
}
if flags&(Lshortfile|Llongfile) != 0 && r.PC > 0 {
var fileStr string
fs := runtime.CallersFrames([]uintptr{r.PC})
f, _ := fs.Next()
file := f.File
if flags&Lshortfile != 0 {
i := strings.LastIndexAny(file, "\\/")
if i > -1 {
file = file[i+1:]
}
}
fileStr = fmt.Sprintf("%s:%s:%d", f.Function, file, f.Line)
if colorful {
fileStr = h.color.Green(fileStr)
}
write(fileStr)
}
if flags&Lfields != 0 && len(fields) > 0 {
b, err := json.Marshal(fields)
if err != nil {
return err
}
fieldsStr := string(bytes.TrimSpace(b))
if fieldsStr != "" {
if colorful {
fieldsStr = h.color.White(fieldsStr)
}
write(fieldsStr)
}
}
h.l.Println(strings.TrimSpace(output))
return nil
}

@ -0,0 +1,62 @@
package log
import (
"log/slog"
)
type Level int
const (
LevelTrace Level = iota
LevelDebug // 用于程序调试
LevelInfo // 用于程序运行
LevelWarn // 潜在错误或非预期结果
LevelError // 发生错误,但不影响系统的继续运行
LevelFatal
LevelPanic
LevelOff
)
// 越界取近值
func (l Level) real() Level {
return min(LevelOff, max(l, LevelTrace))
}
// Level 实现 slog.Leveler 接口
func (l Level) Level() slog.Level {
return slog.Level(20 - int(LevelOff-l.real())*4)
}
func (l Level) slog() slog.Leveler {
return l.Level()
}
func (l Level) String() string {
switch l {
case LevelTrace:
return "TRACE"
case LevelDebug:
return "DEBUG"
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
case LevelFatal:
return "FATAL"
case LevelPanic:
return "PANIC"
case LevelOff:
return "OFF"
}
if l < LevelTrace {
return "TRACE"
} else {
return "OFF"
}
}
func parseSlogLevel(level slog.Level) Level {
return Level(level/4 + 2).real()
}

@ -0,0 +1,97 @@
package log
import (
"io"
"time"
)
var std = New(&Options{Level: LevelTrace})
func Default() Logger { return std }
func Flags() int {
return std.Flags()
}
func SetFlags(flags int) {
std.SetFlags(flags)
}
func LevelMode() Level {
return std.Level()
}
func SetLevel(level Level) {
std.SetLevel(level)
}
func Timezone() *time.Location {
return std.Timezone()
}
func SetTimezone(loc *time.Location) {
std.SetTimezone(loc)
}
func IgnorePC() bool {
return std.IgnorePC()
}
func SetIgnorePC(ignore bool) {
std.SetIgnorePC(ignore)
}
func Enabled(level Level) bool {
return std.Enabled(level)
}
func SetWriter(w io.Writer) {
std.SetWriter(w)
}
func SetPersistWriter(w io.Writer) {
std.SetPersistWriter(w)
}
func With(attrs ...Attr) Logger {
return std.With(attrs...)
}
func WithGroup(name string) Logger {
return std.WithGroup(name)
}
// Log logs at level.
func Log(level Level, msg string, args ...any) {
std.Log(level, msg, args...)
}
// Trace logs at LevelTrace.
func Trace(msg string, args ...any) {
std.Trace(msg, args...)
}
// Debug logs at LevelDebug.
func Debug(msg string, args ...any) {
std.Debug(msg, args...)
}
// Info logs at LevelInfo.
func Info(msg string, args ...any) {
std.Info(msg, args...)
}
// Warn logs at LevelWarn.
func Warn(msg string, args ...any) {
std.Warn(msg, args...)
}
// Error logs at LevelError.
func Error(msg string, args ...any) {
std.Error(msg, args...)
}
// Fatal logs at LevelFatal.
func Fatal(msg string, args ...any) {
std.Fatal(msg, args...)
}

@ -0,0 +1,273 @@
package log
import (
"fmt"
"io"
"log"
"log/slog"
"os"
"runtime"
"sync"
"sync/atomic"
"time"
"unsafe"
)
const (
Ldate = 1 << iota
Ltime
Lmicroseconds
Llongfile
Lshortfile
Lfields
Lcolor
LstdFlags = Ltime | Lmicroseconds | Lfields | Lcolor
)
type Options struct {
Flags int
Level Level
IgnorePC bool
Timezone *time.Location
Colorer Colorer
PersistWriter io.Writer
Writer io.Writer
}
type Logger interface {
Flags() int
SetFlags(flags int)
Level() Level
SetLevel(Level)
Timezone() *time.Location
SetTimezone(loc *time.Location)
IgnorePC() bool
SetIgnorePC(ignore bool)
Enabled(level Level) bool
Writer() io.Writer
SetWriter(w io.Writer)
PersistWriter() io.Writer
SetPersistWriter(w io.Writer)
With(attrs ...Attr) Logger
WithGroup(name string) Logger
Log(level Level, msg string, args ...any)
Trace(msg string, args ...any)
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
Fatal(msg string, args ...any)
}
var _ Logger = &logger{}
type logger struct {
flags int32
level int32
ignorePC int32
unpersist int32 // 忽略持久化写入
discard int32 // 忽略控制台输出
timezone unsafe.Pointer
mu *sync.Mutex
persistWriter io.Writer
writer io.Writer
handler slog.Handler
}
func New(o *Options) Logger {
if o.Flags == 0 {
o.Flags = LstdFlags
}
if o.Timezone == nil {
o.Timezone = time.FixedZone("CST", 8*3600) // 使用东八区时间
}
if o.Writer == nil {
o.Writer = os.Stderr
}
if o.PersistWriter == nil {
o.PersistWriter = io.Discard
}
if o.Colorer == nil {
o.Colorer = NewColorer()
}
l := &logger{mu: &sync.Mutex{}}
l.handler = &Handler{
ctx: unsafe.Pointer(l),
color: o.Colorer,
Handler: slog.NewJSONHandler(
&mutablePersistWriter{l},
&slog.HandlerOptions{
AddSource: true,
Level: &mutableLevel{l},
ReplaceAttr: createAttrReplacer(l),
},
),
l: log.New(&mutableWriter{l}, "", 0),
}
l.SetFlags(o.Flags)
l.SetLevel(o.Level)
l.SetIgnorePC(o.IgnorePC)
l.SetTimezone(o.Timezone)
l.SetPersistWriter(o.PersistWriter)
l.SetWriter(o.Writer)
return l
}
func (l *logger) Flags() int {
return int(atomic.LoadInt32(&l.flags))
}
func (l *logger) SetFlags(flags int) {
atomic.StoreInt32(&l.flags, int32(flags))
}
func (l *logger) Level() Level {
return Level(int(atomic.LoadInt32(&l.level)))
}
func (l *logger) SetLevel(level Level) {
atomic.StoreInt32(&l.level, int32(level))
}
func (l *logger) Timezone() *time.Location {
// 参考 atomic.Pointer#Load 实现
return (*time.Location)(atomic.LoadPointer(&l.timezone))
}
func (l *logger) SetTimezone(loc *time.Location) {
// 参考 atomic.Pointer#Store 实现
atomic.StorePointer(&l.timezone, unsafe.Pointer(loc))
}
func (l *logger) IgnorePC() bool {
return atomic.LoadInt32(&l.ignorePC) == 1
}
func (l *logger) SetIgnorePC(ignore bool) {
atomic.StoreInt32(&l.ignorePC, bool2int32(ignore))
}
func (l *logger) Enabled(level Level) bool {
return l.handler.Enabled(nil, level.slog().Level())
}
func (l *logger) SetWriter(w io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.writer = w
atomic.StoreInt32(&l.discard, bool2int32(w == io.Discard))
}
func (l *logger) Writer() io.Writer {
l.mu.Lock()
defer l.mu.Unlock()
return l.writer
}
func (l *logger) SetPersistWriter(w io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.persistWriter = w
atomic.StoreInt32(&l.unpersist, bool2int32(w == io.Discard))
}
func (l *logger) PersistWriter() io.Writer {
l.mu.Lock()
defer l.mu.Unlock()
return l.persistWriter
}
func (l *logger) With(attrs ...Attr) Logger {
if len(attrs) == 0 {
return l
}
c := l.clone()
c.handler = l.handler.WithAttrs(attrs).(*Handler)
return c
}
func (l *logger) WithGroup(name string) Logger {
if name == "" {
return l
}
c := l.clone()
c.handler = l.handler.WithGroup(name).(*Handler)
return c
}
func (l *logger) clone() *logger {
c := *l
// TODO(hupeh): 测试 clone 是否报错
//c.writer = l.writer
//c.persistWriter = l.persistWriter
return &c
}
// Log logs at level.
func (l *logger) Log(level Level, msg string, args ...any) {
l.log(level, msg, args...)
}
// Trace logs at LevelTrace.
func (l *logger) Trace(msg string, args ...any) {
l.log(LevelTrace, msg, args...)
}
// Debug logs at LevelDebug.
func (l *logger) Debug(msg string, args ...any) {
l.log(LevelDebug, msg, args...)
}
// Info logs at LevelInfo.
func (l *logger) Info(msg string, args ...any) {
l.log(LevelInfo, msg, args...)
}
// Warn logs at LevelWarn.
func (l *logger) Warn(msg string, args ...any) {
l.log(LevelWarn, msg, args...)
}
// Error logs at LevelError.
func (l *logger) Error(msg string, args ...any) {
l.log(LevelError, msg, args...)
}
// Fatal logs at LevelFatal.
func (l *logger) Fatal(msg string, args ...any) {
l.log(LevelFatal, msg, args...)
}
func (l *logger) log(level Level, msg string, args ...any) {
if !l.Enabled(level) {
return
}
var pc uintptr
if atomic.LoadInt32(&l.ignorePC) != 1 {
var pcs [1]uintptr
// skip [runtime.Callers, this function, this function's caller]
runtime.Callers(3, pcs[:])
pc = pcs[0]
}
r := slog.NewRecord(time.Now(), level.slog().Level(), msg, pc)
if len(args) > 0 {
var sprintfArgs []any
for _, arg := range args {
switch v := arg.(type) {
case Attr:
r.AddAttrs(v)
default:
sprintfArgs = append(sprintfArgs, arg)
}
}
if len(sprintfArgs) > 0 {
msg = fmt.Sprintf(msg, sprintfArgs...)
}
r.Message = msg
}
_ = l.handler.Handle(nil, r)
}

@ -0,0 +1,77 @@
package log
import (
"log/slog"
"time"
)
type mutableLevel struct {
l *logger
}
func (l *mutableLevel) Level() slog.Level {
return l.l.Level().slog().Level()
}
type mutablePersistWriter struct {
l *logger
}
func (l *mutablePersistWriter) Write(b []byte) (int, error) {
l.l.mu.Lock()
defer l.l.mu.Unlock()
return l.l.persistWriter.Write(b)
}
type mutableWriter struct {
l *logger
}
func (l *mutableWriter) Write(b []byte) (int, error) {
l.l.mu.Lock()
defer l.l.mu.Unlock()
return l.l.writer.Write(b)
}
func bool2int32(v bool) int32 {
if v {
return 1
} else {
return 0
}
}
func createAttrReplacer(l *logger) func([]string, Attr) Attr {
return func(_ []string, a Attr) Attr {
if a.Key == slog.LevelKey {
level := a.Value.Any().(slog.Level)
levelLabel := parseSlogLevel(level).String()
a.Value = slog.StringValue(levelLabel)
} else if a.Key == slog.TimeKey {
t := a.Value.Any().(time.Time)
a.Value = slog.TimeValue(t.In(l.Timezone()))
} else if a.Key == slog.SourceKey {
s := a.Value.Any().(*slog.Source)
var as []Attr
if s.Function != "" {
as = append(as, String("func", s.Function))
}
if s.File != "" {
as = append(as, String("file", s.File))
}
if s.Line != 0 {
as = append(as, Int("line", s.Line))
}
a.Value = slog.GroupValue(as...)
} else if a.Key == rawLevelKey || a.Key == rawTimeKey {
// TODO(hupeh): 在 JSONHandler 中替换这两个值
a.Key = ""
a.Value = slog.AnyValue(nil)
}
return a
}
}
func identify(s string) string {
return s
}

@ -0,0 +1,169 @@
package log
import (
"errors"
"os"
"path"
"strings"
"sync"
"time"
)
const (
WhenSecond = iota
WhenMinute
WhenHour
WhenDay
)
type RotateWriter struct {
filename string // should be set to the actual filename
written int
interval time.Duration
rotateSize int
rotateTo func(time.Time) string
rolloverAt chan struct{}
timer *time.Timer
mu sync.Mutex
fp *os.File
closed chan struct{}
}
// Rotate Make a new RotateWriter. Return nil if error occurs during setup.
func Rotate(basename string, when int, rotateSize int) *RotateWriter {
if rotateSize <= 0 {
panic("invalid rotate size")
}
var interval time.Duration
var suffix string
switch when {
case WhenSecond:
interval = time.Second
suffix = "20060102150405"
case WhenMinute:
interval = time.Minute
suffix = "200601021504"
case WhenHour:
interval = time.Hour
suffix = "2006010215"
case WhenDay:
fallthrough
default:
interval = time.Hour * 24
suffix = "20060102"
}
// 解决 Windows 电脑路径问题
basename = strings.ReplaceAll(basename, "\\", "/")
filenameWithSuffix := path.Base(basename)
fileSuffix := path.Ext(filenameWithSuffix)
filename := strings.TrimSuffix(filenameWithSuffix, fileSuffix)
fileDir := path.Dir(basename)
// 创建日志文件目录
if err := os.MkdirAll(fileDir, 0777); err != nil {
panic(err)
}
w := &RotateWriter{
filename: basename,
interval: interval,
rotateSize: rotateSize,
rotateTo: func(t time.Time) string {
return fileDir + "/" + filename + "." + t.Format(suffix) + fileSuffix
},
rolloverAt: make(chan struct{}),
mu: sync.Mutex{},
}
err := w.Rotate()
if err != nil {
return nil
}
return w
}
func (w *RotateWriter) Write(b []byte) (int, error) {
select {
case <-w.closed:
return 0, errors.New("already closed")
case <-w.rolloverAt:
if err := w.Rotate(); err != nil {
return 0, err
}
return w.Write(b)
default:
w.mu.Lock()
defer w.mu.Unlock()
n, err := w.fp.Write(b)
w.written += n
if w.written >= w.rotateSize {
w.timer.Stop()
w.rolloverAt <- struct{}{}
}
return n, err
}
}
func (w *RotateWriter) Close() error {
select {
case <-w.closed:
default:
w.mu.Lock()
defer w.mu.Unlock()
if w.fp != nil {
return w.fp.Close()
}
w.timer.Stop()
}
return nil
}
func (w *RotateWriter) Rotate() error {
w.mu.Lock()
defer w.mu.Unlock()
// Close existing file if open
if w.fp != nil {
err := w.fp.Close()
w.fp = nil
if err != nil {
return err
}
}
// Rename dest file if it already exists
_, err := os.Stat(w.filename)
if err == nil {
err = os.Rename(w.filename, w.rotateTo(time.Now()))
if err != nil {
return err
}
}
// Create a file.
w.fp, err = os.Create(w.filename)
if err == nil {
return err
}
if w.timer != nil {
w.timer.Stop()
}
w.timer = time.NewTimer(w.interval)
go func() {
// todo 到底是 ok 还是 !ok
if _, ok := <-w.timer.C; !ok {
w.rolloverAt <- struct{}{}
}
}()
w.written = 0
return err
}

@ -0,0 +1,67 @@
package logs
import (
"log/slog"
"time"
)
type Attr = slog.Attr
// String returns an Attr for a string value.
func String(key, value string) Attr {
return slog.String(key, value)
}
// Int64 returns an Attr for an int64.
func Int64(key string, value int64) Attr {
return slog.Int64(key, value)
}
// Int converts an int to an int64 and returns
// an Attr with that value.
func Int(key string, value int) Attr {
return slog.Int(key, value)
}
// Uint64 returns an Attr for a uint64.
func Uint64(key string, v uint64) Attr {
return slog.Uint64(key, v)
}
// Float64 returns an Attr for a floating-point number.
func Float64(key string, v float64) Attr {
return slog.Float64(key, v)
}
// Bool returns an Attr for a bool.
func Bool(key string, v bool) Attr {
return slog.Bool(key, v)
}
// Time returns an Attr for a time.Time.
// It discards the monotonic portion.
func Time(key string, v time.Time) Attr {
return slog.Time(key, v)
}
// Duration returns an Attr for a time.Duration.
func Duration(key string, v time.Duration) Attr {
return slog.Duration(key, v)
}
// Group returns an Attr for a Group Instance.
// The first argument is the key; the remaining arguments
// are converted to Attrs as in [Logger.Log].
//
// Use Group to collect several key-value pairs under a single
// key on a log line, or as the result of LogValue
// in order to log a single value as multiple Attrs.
func Group(key string, args ...any) Attr {
return slog.Group(key, args...)
}
// Any returns an Attr for the supplied value.
// See [AnyValue] for how values are treated.
func Any(key string, value any) Attr {
return slog.Any(key, value)
}

@ -0,0 +1,44 @@
package logs
import (
"github.com/mattn/go-isatty"
"os"
)
type Color string
var (
//fgBlack Color = "\x1b[30m"
//fgWhiteItalic Color = "\x1b[37;3m"
FgRed Color = "\x1b[31m"
FgGreen Color = "\x1b[32m"
FgYellow Color = "\x1b[33m"
FgBlue Color = "\x1b[34m"
FgMagenta Color = "\x1b[35m"
FgCyan Color = "\x1b[36m"
FgWhite Color = "\x1b[37m"
FgHiBlack Color = "\x1b[90m"
fgGreenItalic Color = "\x1b[32;3m"
// NoColor defines if the output is colorized or not. It's dynamically set to
// false or true based on the stdout's file descriptor referring to a terminal
// or not. It's also set to true if the NO_COLOR environment variable is
// set (regardless of its value). This is a global option and affects all
// colors. For more control over each Color block use the methods
// DisableColor() individually.
noColor = noColorIsSet() || os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
)
// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string.
func noColorIsSet() bool {
return os.Getenv("NO_COLOR") != ""
}
func (c Color) Wrap(msg string) string {
if noColorIsSet() || noColor {
return msg
}
return string(c) + msg + "\x1b[0m"
}

@ -0,0 +1 @@
package logs

@ -0,0 +1,59 @@
package logs
import (
"log/slog"
)
type Level int
const (
LevelTrace Level = iota
LevelDebug // 用于程序调试
LevelInfo // 用于程序运行
LevelWarn // 潜在错误或非预期结果
LevelError // 发生错误,但不影响系统的继续运行
LevelFatal
LevelSilent
)
// 越界取近值
func (l Level) real() Level {
return min(LevelSilent, max(l, LevelTrace))
}
// Level 实现 slog.Leveler 接口
func (l Level) Level() slog.Level {
return slog.Level(16 - int(LevelSilent-l.real())*4)
}
func (l Level) slog() slog.Leveler {
return l.Level()
}
func (l Level) String() string {
switch l {
case LevelTrace:
return "TRACE"
case LevelDebug:
return "DEBUG"
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
case LevelFatal:
return "FATAL"
case LevelSilent:
return "OFF"
}
if l < LevelTrace {
return "TRACE"
} else {
return "OFF"
}
}
func parseSlogLevel(level slog.Level) Level {
return Level(level/4 + 2).real()
}

@ -0,0 +1,38 @@
package logs
import (
"io"
"time"
)
var std = New(&Options{Level: LevelInfo})
func Default() Logger { return std }
func SetFlags(flags int) { Default().SetFlags(flags) }
func Flags() int { return Default().Flags() }
func SetTimezone(loc *time.Location) { Default().SetTimezone(loc) }
func Timezone() *time.Location { return Default().Timezone() }
func SetLevel(level Level) { Default().SetLevel(level) }
func GetLevel() Level { return Default().Level() }
func SetPersistWriter(w io.Writer) { Default().SetPersistWriter(w) }
func SetWriter(w io.Writer) { Default().SetWriter(w) }
func With(args ...Attr) Logger { return Default().With(args...) }
func WithGroup(name string) Logger { return Default().WithGroup(name) }
func Enabled(level Level) bool { return Default().Enabled(level) }
func Log(level Level, msg string, args ...any) { Default().Log(level, msg, args...) }
func ForkLevel(level Level, msg string, args ...any) ChildLogger {
return Default().ForkLevel(level, msg, args...)
}
func Trace(msg string, args ...any) { Default().Trace(msg, args...) }
func ForkTrace(msg string, args ...any) ChildLogger { return Default().ForkTrace(msg, args...) }
func Debug(msg string, args ...any) { Default().Debug(msg, args...) }
func ForkDebug(msg string, args ...any) ChildLogger { return Default().ForkDebug(msg, args...) }
func Info(msg string, args ...any) { Default().Info(msg) }
func ForkInfo(msg string, args ...any) ChildLogger { return Default().ForkInfo(msg, args...) }
func Warn(msg string, args ...any) { Default().Warn(msg, args...) }
func ForkWarn(msg string, args ...any) ChildLogger { return Default().ForkWarn(msg, args...) }
func Error(msg string, args ...any) { Default().Error(msg, args...) }
func ForkError(msg string, args ...any) ChildLogger { return Default().ForkError(msg, args...) }
func Fatal(msg string, args ...any) { Default().Fatal(msg, args...) }
func ForkFatal(msg string, args ...any) ChildLogger { return Default().ForkFatal(msg, args...) }

@ -0,0 +1,512 @@
package logs
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"os"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"unsafe"
)
var (
// 使用东八区时间
// https://cloud.tencent.com/developer/article/1805859
cstZone = time.FixedZone("CST", 8*3600)
childLoggerKey = "sorbet/log:ChildLogger"
)
const (
Ldate = 1 << iota
Ltime
Lmicroseconds
Llongfile
Lshortfile
Lfields
Lcolor
LstdFlags = Ltime | Lmicroseconds | Lfields | Lcolor
)
type Logger interface {
SetFlags(flags int)
Flags() int
SetTimezone(loc *time.Location)
Timezone() *time.Location
SetLevel(level Level)
Level() Level
SetPersistWriter(w io.Writer)
SetWriter(w io.Writer)
With(args ...Attr) Logger
WithGroup(name string) Logger
Enabled(level Level) bool
Log(level Level, msg string, args ...any)
ForkLevel(level Level, msg string, args ...any) ChildLogger
Trace(msg string, args ...any)
ForkTrace(msg string, args ...any) ChildLogger
Debug(msg string, args ...any)
ForkDebug(msg string, args ...any) ChildLogger
Info(msg string, args ...any)
ForkInfo(msg string, args ...any) ChildLogger
Warn(msg string, args ...any)
ForkWarn(msg string, args ...any) ChildLogger
Error(msg string, args ...any)
ForkError(msg string, args ...any) ChildLogger
Fatal(msg string, args ...any)
ForkFatal(msg string, args ...any) ChildLogger
}
type ChildLogger interface {
Print(msg string, args ...any)
Finish()
}
type Options struct {
Flags int
Level Level
Timezone *time.Location
PersistWriter io.Writer
Writer io.Writer
}
type logger struct {
parent *logger
isChild int32
indent int32
level int32
flags int32
timezone unsafe.Pointer
outMu *sync.Mutex
isPersistDiscard int32
isDiscard int32
persistWriter io.Writer
writer io.Writer
handler slog.Handler
l *log.Logger
}
func New(opts *Options) Logger {
if opts.Flags == 0 {
opts.Flags = LstdFlags
}
if opts.Timezone == nil {
opts.Timezone = cstZone
}
if opts.PersistWriter == nil {
opts.PersistWriter = io.Discard
}
if opts.Writer == nil {
opts.Writer = os.Stderr
}
var l *logger
l = &logger{
outMu: &sync.Mutex{},
persistWriter: opts.PersistWriter,
writer: opts.Writer,
l: log.New(opts.Writer, "", 0),
}
l.SetLevel(opts.Level)
l.SetFlags(opts.Flags)
l.SetTimezone(opts.Timezone)
l.SetPersistWriter(opts.PersistWriter)
l.SetWriter(opts.Writer)
l.handler = slog.NewJSONHandler(opts.PersistWriter, &slog.HandlerOptions{
AddSource: true,
Level: opts.Level,
ReplaceAttr: l.onAttr,
})
return l
}
func (l *logger) SetFlags(flags int) {
atomic.StoreInt32(&l.flags, int32(flags))
}
func (l *logger) Flags() int {
return int(atomic.LoadInt32(&l.flags))
}
func (l *logger) SetTimezone(loc *time.Location) {
// FIXME(hupeh): 如何原子化储存结构体实例
atomic.StorePointer(&l.timezone, unsafe.Pointer(loc))
}
func (l *logger) Timezone() *time.Location {
return (*time.Location)(atomic.LoadPointer(&l.timezone))
}
func (l *logger) SetLevel(level Level) {
atomic.StoreInt32(&l.level, int32(level))
}
func (l *logger) Level() Level {
return Level(int(atomic.LoadInt32(&l.level)))
}
func (l *logger) SetPersistWriter(w io.Writer) {
l.outMu.Lock()
defer l.outMu.Unlock()
l.persistWriter = w
atomic.StoreInt32(&l.isPersistDiscard, discard(w))
}
func (l *logger) SetWriter(w io.Writer) {
l.outMu.Lock()
defer l.outMu.Unlock()
l.writer = w
atomic.StoreInt32(&l.isDiscard, discard(w))
}
func discard(w io.Writer) int32 {
if w == io.Discard {
return 1
}
return 0
}
func (l *logger) onAttr(_ []string, a slog.Attr) slog.Attr {
switch a.Key {
case slog.LevelKey:
level := a.Value.Any().(slog.Level)
levelLabel := parseSlogLevel(level).String()
a.Value = slog.StringValue(levelLabel)
case slog.TimeKey:
t := a.Value.Any().(time.Time)
a.Value = slog.TimeValue(t.In(l.Timezone()))
case slog.SourceKey:
s := a.Value.Any().(*slog.Source)
var as []Attr
if s.Function != "" {
as = append(as, String("func", s.Function))
}
if s.File != "" {
as = append(as, String("file", s.File))
}
if s.Line != 0 {
as = append(as, Int("line", s.Line))
}
a.Value = slog.GroupValue(as...)
}
return a
}
func (l *logger) Handle(ctx context.Context, r slog.Record) error {
if atomic.LoadInt32(&l.isDiscard) == 0 {
child, ok := ctx.Value(childLoggerKey).(*childLogger)
indent, err := l.println(child, r)
if err != nil {
return err
}
if ok && indent > 0 {
atomic.StoreInt32(&child.indent, indent)
}
}
if atomic.LoadInt32(&l.isPersistDiscard) == 0 {
return l.handler.Handle(ctx, r)
}
return nil
}
func (l *logger) println(child *childLogger, r slog.Record) (int32, error) {
var output string
var sep string
var indent int32
write := func(s string) {
if sep == "" {
sep = " "
} else {
output += sep
}
output += s
}
flags := l.Flags()
colorful := flags&Lcolor != 0
msg := r.Message
level := parseSlogLevel(r.Level)
levelStr := level.String()
withChild := child != nil
if withChild {
indent = atomic.LoadInt32(&child.indent)
withChild = indent > 0
}
if withChild {
write(strings.Repeat(" ", int(indent)))
indent = 0
} else {
if flags&(Ldate|Ltime|Lmicroseconds) != 0 {
t := r.Time.In(l.Timezone())
if flags&Ldate != 0 {
write(t.Format("2006/01/02"))
}
if flags&(Ltime|Lmicroseconds) != 0 {
if flags&Lmicroseconds != 0 {
write(t.Format("15:04:05.000"))
} else {
write(t.Format("15:04:05"))
}
}
}
// 保存缩进
indent += int32(len(output) + len(levelStr) + 3)
if colorful {
switch level {
case LevelDebug:
levelStr = FgCyan.Wrap(levelStr)
msg = FgCyan.Wrap(msg)
case LevelInfo:
levelStr = FgBlue.Wrap(levelStr) + " "
msg = FgBlue.Wrap(msg)
indent += 1
case LevelWarn:
levelStr = FgYellow.Wrap(levelStr) + " "
msg = FgYellow.Wrap(msg)
indent += 1
case LevelError:
levelStr = FgRed.Wrap(levelStr)
msg = FgRed.Wrap(msg)
case LevelFatal:
levelStr = FgMagenta.Wrap(levelStr)
msg = FgMagenta.Wrap(msg)
}
levelStr = FgHiBlack.Wrap("[") + levelStr + FgHiBlack.Wrap("]")
} else {
levelStr = "[" + r.Level.String() + "]"
}
write(levelStr)
}
write(msg)
if flags&(Lshortfile|Llongfile) != 0 && r.PC > 0 {
var fileStr string
fs := runtime.CallersFrames([]uintptr{r.PC})
f, _ := fs.Next()
file := f.File
if flags&Lshortfile != 0 {
i := strings.LastIndexAny(file, "\\/")
if i > -1 {
file = file[i+1:]
}
}
fileStr = fmt.Sprintf("%s:%s:%d", f.Function, file, f.Line)
if colorful {
fileStr = fgGreenItalic.Wrap(fileStr)
}
write(fileStr)
}
if numAttrs := r.NumAttrs(); flags&Lfields != 0 && numAttrs > 0 {
fields := make(map[string]any, numAttrs)
r.Attrs(func(a Attr) bool {
fields[a.Key] = a.Value.Any()
return true
})
b, err := json.Marshal(fields)
if err != nil {
return 0, err
}
fieldsStr := string(bytes.TrimSpace(b))
if fieldsStr != "" {
if colorful {
fieldsStr = FgHiBlack.Wrap(fieldsStr)
}
write(fieldsStr)
}
}
l.l.Println(output)
return indent, nil
}
func (l *logger) clone() *logger {
c := *l
return &c
}
func (l *logger) With(args ...Attr) Logger {
if len(args) == 0 {
return l
}
c := l.clone()
c.handler = c.handler.WithAttrs(args)
return c
}
func (l *logger) WithGroup(name string) Logger {
if name == "" {
return l
}
c := l.clone()
c.handler = c.handler.WithGroup(name)
return c
}
func (l *logger) Enabled(level Level) bool {
return l.handler.Enabled(nil, level.slog().Level())
}
// Log logs at level.
func (l *logger) Log(level Level, msg string, args ...any) {
l.log(nil, level, msg, args...)
}
func (l *logger) ForkLevel(level Level, msg string, args ...any) ChildLogger {
c := &childLogger{
parent: l,
level: level,
indent: 0,
records: make([]slog.Record, 0),
closed: make(chan struct{}),
}
c.Print(msg, args...)
return c
}
// Trace logs at LevelTrace.
func (l *logger) Trace(msg string, args ...any) {
l.log(nil, LevelTrace, msg, args...)
}
func (l *logger) ForkTrace(msg string, args ...any) ChildLogger {
return l.ForkLevel(LevelTrace, msg, args...)
}
// Debug logs at LevelDebug.
func (l *logger) Debug(msg string, args ...any) {
l.log(nil, LevelDebug, msg, args...)
}
func (l *logger) ForkDebug(msg string, args ...any) ChildLogger {
return l.ForkLevel(LevelDebug, msg, args...)
}
// Info logs at LevelInfo.
func (l *logger) Info(msg string, args ...any) {
l.log(nil, LevelInfo, msg, args...)
}
func (l *logger) ForkInfo(msg string, args ...any) ChildLogger {
return l.ForkLevel(LevelInfo, msg, args...)
}
// Warn logs at LevelWarn.
func (l *logger) Warn(msg string, args ...any) {
l.log(nil, LevelWarn, msg, args...)
}
func (l *logger) ForkWarn(msg string, args ...any) ChildLogger {
return l.ForkLevel(LevelWarn, msg, args...)
}
// Error logs at LevelError.
func (l *logger) Error(msg string, args ...any) {
l.log(nil, LevelError, msg, args...)
}
func (l *logger) ForkError(msg string, args ...any) ChildLogger {
return l.ForkLevel(LevelError, msg, args...)
}
// Fatal logs at LevelFatal.
func (l *logger) Fatal(msg string, args ...any) {
l.log(nil, LevelFatal, msg, args...)
}
func (l *logger) ForkFatal(msg string, args ...any) ChildLogger {
return l.ForkLevel(LevelFatal, msg, args...)
}
func (l *logger) log(ctx context.Context, level Level, msg string, args ...any) {
if !l.Enabled(level) {
return
}
if ctx == nil {
ctx = context.Background()
}
_ = l.Handle(ctx, newRecord(level, msg, args))
}
func newRecord(level Level, msg string, args []any) slog.Record {
//var pc uintptr
//if !internal.IgnorePC {
// var pcs [1]uintptr
// // skip [runtime.Callers, this function, this function's caller]
// runtime.Callers(3, pcs[:])
// pc = pcs[0]
//}
//r := slog.NewRecord(time.Now(), level.slog().Level(), msg, pc)
r := slog.NewRecord(time.Now(), level.slog().Level(), msg, 0)
if len(args) > 0 {
var sprintfArgs []any
for _, arg := range args {
switch v := arg.(type) {
case Attr:
r.AddAttrs(v)
default:
sprintfArgs = append(sprintfArgs, arg)
}
}
if len(sprintfArgs) > 0 {
r.Message = fmt.Sprintf(msg, sprintfArgs...)
}
}
return r
}
type childLogger struct {
parent *logger
level Level
indent int32
begin slog.Record
finish slog.Record
records []slog.Record
closed chan struct{}
}
func (c *childLogger) Print(msg string, args ...any) {
select {
case <-c.closed:
default:
c.records = append(
c.records,
newRecord(c.level, msg, args),
)
}
}
func (c *childLogger) Finish() {
select {
case <-c.closed:
return
default:
close(c.closed)
}
ctx := context.Background()
ctx = context.WithValue(ctx, childLoggerKey, c)
for _, record := range c.records {
_ = c.parent.Handle(ctx, record)
}
}

@ -0,0 +1,13 @@
package misc
import "cmp"
func Clamp[T cmp.Ordered](val, min, max T) T {
if val > max {
val = max
}
if val < min {
val = min
}
return val
}

@ -0,0 +1,14 @@
package misc
// Fallback 当值为空时返回默认值
func Fallback[T any](v T, fs ...T) T {
if !IsZero(v) {
return v
}
for _, f := range fs {
if !IsZero(f) {
return f
}
}
return v
}

@ -0,0 +1,26 @@
package misc
import (
"context"
)
type Key[T any] struct {
Name string
}
func (k *Key[T]) Wrap(ctx context.Context, value *T) context.Context {
return context.WithValue(ctx, k, value)
}
func (k *Key[T]) Lookup(ctx context.Context) (*T, bool) {
t, ok := ctx.Value(k).(*T)
return t, ok
}
func (k *Key[T]) Value(ctx context.Context) *T {
t, ok := ctx.Value(k).(*T)
if !ok {
panic("not value found")
}
return t
}

@ -0,0 +1,45 @@
package misc
// Result 使用泛型模仿 Rust 实现一个简单的 Result 类型
// https://juejin.cn/post/7161342717190996005
type Result[T any] struct {
value T
err error
}
func (r *Result[T]) Ok() bool {
return r.err != nil
}
func (r *Result[T]) Err() error {
return r.err
}
func (r *Result[T]) Unwrap() T {
if r.err != nil {
panic(r.err)
}
return r.value
}
func (r *Result[T]) Expect(err error) T {
if r.err != nil {
panic(err)
}
return r.value
}
func NewResult[T any](v T, err error) *Result[T] {
return &Result[T]{
value: v,
err: err,
}
}
func Match[T any](r *Result[T], okF func(T), errF func(error)) {
if r.err != nil {
errF(r.err)
} else {
okF(r.value)
}
}

@ -0,0 +1,19 @@
package misc
import "reflect"
// IsZero 泛型零值判断
// https://stackoverflow.com/questions/74000242/in-golang-how-to-compare-interface-as-generics-type-to-nil
func IsZero[T any](v T) bool {
return isZero(reflect.ValueOf(v))
}
func isZero(ref reflect.Value) bool {
if !ref.IsValid() {
return true
}
if ref.Type().Kind() == reflect.Ptr {
return isZero(ref.Elem())
}
return ref.IsZero()
}

@ -0,0 +1,171 @@
package rsp
import (
"fmt"
"sort"
"strconv"
"strings"
)
var errInvalidTypeSubtype = "accept: Invalid type '%s'."
// headerAccept represents a parsed headerAccept(-Charset|-Encoding|-Language) header.
type headerAccept struct {
Type, Subtype string
Q float64
Extensions map[string]string
}
// AcceptSlice is a slice of headerAccept.
type acceptSlice []headerAccept
func Accepts(header, expect string) bool {
_, typeSubtype, err := parseMediaRange(expect)
if err != nil {
return false
}
return accepts(parse(header), typeSubtype[0], typeSubtype[1])
}
func accepts(slice acceptSlice, typ, sub string) bool {
for _, a := range slice {
if a.Type != typ {
continue
}
if a.Subtype == "*" || a.Subtype == sub {
return true
}
}
return false
}
// parses a HTTP headerAccept(-Charset|-Encoding|-Language) header and returns
// AcceptSlice, sorted in decreasing order of preference. If the header lists
// multiple types that have the same level of preference (same specificity of
// type and subtype, same qvalue, and same number of extensions), the type
// that was listed in the header first comes first in the returned value.
//
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14 for more information.
func parse(header string) acceptSlice {
mediaRanges := strings.Split(header, ",")
accepted := make(acceptSlice, 0, len(mediaRanges))
for _, mediaRange := range mediaRanges {
rangeParams, typeSubtype, err := parseMediaRange(mediaRange)
if err != nil {
continue
}
accept := headerAccept{
Type: typeSubtype[0],
Subtype: typeSubtype[1],
Q: 1.0,
Extensions: make(map[string]string),
}
// If there is only one rangeParams, we can stop here.
if len(rangeParams) == 1 {
accepted = append(accepted, accept)
continue
}
// Validate the rangeParams.
validParams := true
for _, v := range rangeParams[1:] {
nameVal := strings.SplitN(v, "=", 2)
if len(nameVal) != 2 {
validParams = false
break
}
nameVal[1] = strings.TrimSpace(nameVal[1])
if name := strings.TrimSpace(nameVal[0]); name == "q" {
qval, err := strconv.ParseFloat(nameVal[1], 64)
if err != nil || qval < 0 {
validParams = false
break
}
if qval > 1.0 {
qval = 1.0
}
accept.Q = qval
} else {
accept.Extensions[name] = nameVal[1]
}
}
if validParams {
accepted = append(accepted, accept)
}
}
sort.Sort(accepted)
return accepted
}
// Len implements the Len() method of the Sort interface.
func (a acceptSlice) Len() int {
return len(a)
}
// Less implements the Less() method of the Sort interface. Elements are
// sorted in order of decreasing preference.
func (a acceptSlice) Less(i, j int) bool {
// Higher qvalues come first.
if a[i].Q > a[j].Q {
return true
} else if a[i].Q < a[j].Q {
return false
}
// Specific types come before wildcard types.
if a[i].Type != "*" && a[j].Type == "*" {
return true
} else if a[i].Type == "*" && a[j].Type != "*" {
return false
}
// Specific subtypes come before wildcard subtypes.
if a[i].Subtype != "*" && a[j].Subtype == "*" {
return true
} else if a[i].Subtype == "*" && a[j].Subtype != "*" {
return false
}
// A lot of extensions comes before not a lot of extensions.
if len(a[i].Extensions) > len(a[j].Extensions) {
return true
}
return false
}
// Swap implements the Swap() method of the Sort interface.
func (a acceptSlice) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
// parseMediaRange parses the provided media range, and on success returns the
// parsed range params and type/subtype pair.
func parseMediaRange(mediaRange string) (rangeParams, typeSubtype []string, err error) {
rangeParams = strings.Split(mediaRange, ";")
typeSubtype = strings.Split(rangeParams[0], "/")
// typeSubtype should have a length of exactly two.
if len(typeSubtype) > 2 {
err = fmt.Errorf(errInvalidTypeSubtype, rangeParams[0])
return
} else {
typeSubtype = append(typeSubtype, "*")
}
// Sanitize typeSubtype.
typeSubtype[0] = strings.TrimSpace(typeSubtype[0])
typeSubtype[1] = strings.TrimSpace(typeSubtype[1])
if typeSubtype[0] == "" {
typeSubtype[0] = "*"
}
if typeSubtype[1] == "" {
typeSubtype[1] = "*"
}
return
}

@ -0,0 +1,102 @@
package rsp
import (
"fmt"
"github.com/labstack/echo/v4"
"strings"
)
var (
// ErrOK 表示没有任何错误。
// 对应 HTTP 响应状态码为 500。
ErrOK = NewError(0, "")
// ErrInternal 客户端请求有效,但服务器处理时发生了意外。
// 对应 HTTP 响应状态码为 500。
ErrInternal = NewError(-100, "internal error")
// ErrServiceUnavailable 服务器无法处理请求,一般用于网站维护状态。
// 对应 HTTP 响应状态码为 503。
ErrServiceUnavailable = NewError(-101, "Service Unavailable")
// ErrUnauthorized 用户未提供身份验证凭据,或者没有通过身份验证。
// 响应的 HTTP 状态码为 401。
ErrUnauthorized = NewError(-102, "unauthorized")
// ErrForbidden 用户通过了身份验证,但是不具有访问资源所需的权限。
// 响应的 HTTP 状态码为 403。
ErrForbidden = NewError(-103, "Forbidden")
// ErrGone 所请求的资源已从这个地址转移,不再可用。
// 响应的 HTTP 状态码为 410。
ErrGone = NewError(-104, "Gone")
// ErrUnsupportedMediaType 客户端要求的返回格式不支持。
// 比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。
// 响应的 HTTP 状态码为 415。
ErrUnsupportedMediaType = NewError(-105, "Unsupported Media Type")
// ErrUnprocessableEntity 无法处理客户端上传的附件,导致请求失败。
// 响应的 HTTP 状态码为 422。
ErrUnprocessableEntity = NewError(-106, "Unprocessable Entity")
// ErrTooManyRequests 客户端的请求次数超过限额。
// 响应的 HTTP 状态码为 422。
ErrTooManyRequests = NewError(-107, "Too Many Requests")
// ErrSeeOther 表示需要参考另一个 URL 才能完成接收的请求操作,
// 当请求方式使用 POST、PUT 和 DELETE 时,对应的 HTTP 状态码为 303,
// 其它的请求方式在大多数情况下应该使用 400 状态码。
ErrSeeOther = NewError(-108, "see other")
// ErrBadRequest 服务器不理解客户端的请求。
// 对应 HTTP 状态码为 404。
ErrBadRequest = NewError(-109, "bad request")
// ErrBadParams 客户端提交的参数不符合要求
// 对应 HTTP 状态码为 400。
ErrBadParams = NewError(-110, "bad parameters")
// ErrRecordNotFound 访问的数据不存在
// 对应 HTTP 状态码为 404。
ErrRecordNotFound = NewError(-111, "record not found")
)
type Error struct {
code int
text string
}
func NewError(code int, text string) *Error {
return &Error{code, text}
}
func (e *Error) Code() int {
return e.code
}
func (e *Error) Text() string {
return e.text
}
func (e *Error) WithText(text ...string) *Error {
for _, s := range text {
if s != "" {
return NewError(e.code, s)
}
}
return e
}
func (e *Error) AsHttpError(code int) *echo.HTTPError {
he := echo.NewHTTPError(code, e.text)
return he.WithInternal(e)
}
func (e *Error) String() string {
return strings.TrimSpace(fmt.Sprintf("%d %s", e.code, e.text))
}
func (e *Error) Error() string {
return e.String()
}

@ -0,0 +1,255 @@
package rsp
import (
"bytes"
"encoding/json"
"errors"
"github.com/labstack/echo/v4"
"net/http"
)
var (
TextMarshaller func(map[string]any) (string, error)
HtmlMarshaller func(map[string]any) (string, error)
JsonpCallbacks []string
DefaultJsonpCallback string
)
func init() {
TextMarshaller = toText
HtmlMarshaller = toText
JsonpCallbacks = []string{"callback", "cb", "jsonp"}
DefaultJsonpCallback = "callback"
}
func toText(m map[string]any) (string, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(true)
if err := enc.Encode(m); err != nil {
return "", err
} else {
return buf.String(), nil
}
}
type response struct {
code int
headers map[string]string
cookies []*http.Cookie
err error
message string
data any
}
type Option func(o *response)
func StatusCode(code int) Option {
return func(o *response) {
o.code = code
}
}
func Header(key, value string) Option {
return func(o *response) {
if o.headers == nil {
o.headers = make(map[string]string)
}
o.headers[key] = value
}
}
func Cookie(cookie *http.Cookie) Option {
return func(o *response) {
if o.cookies != nil {
for i, h := range o.cookies {
if h.Name == cookie.Name {
o.cookies[i] = cookie
return
}
}
}
o.cookies = append(o.cookies, cookie)
}
}
func Message(msg string) Option {
return func(o *response) {
o.message = msg
}
}
func Data(data any) Option {
return func(o *response) {
o.data = data
}
}
func respond(c echo.Context, o *response) error {
defer func() {
if o.err != nil {
c.Logger().Error(o.err)
}
}()
m := map[string]any{
"code": nil,
"success": false,
"message": o.message,
}
var err *Error
if errors.As(o.err, &err) {
m["code"] = err.code
m["message"] = err.text
m["success"] = errors.Is(err, ErrOK)
} else if o.err != nil {
m["code"] = ErrInternal.code
m["message"] = o.err.Error()
} else {
m["code"] = 0
m["success"] = true
if o.data != nil {
m["data"] = o.data
}
}
if m["message"] == "" {
m["message"] = http.StatusText(o.code)
}
if c.Response().Committed {
return nil
}
if o.headers != nil {
header := c.Response().Header()
for key, value := range o.headers {
header.Set(key, value)
}
}
if o.cookies != nil {
for _, cookie := range o.cookies {
c.SetCookie(cookie)
}
}
r := c.Request()
if r.Method == http.MethodHead {
return c.NoContent(o.code)
}
slice := parse(r.Header.Get("Accept"))
for _, a := range slice {
switch x := a.Type + "/" + a.Subtype; x {
case echo.MIMEApplicationJavaScript:
qs := c.Request().URL.Query()
for _, name := range JsonpCallbacks {
if cb := qs.Get(name); cb != "" {
return c.JSONP(o.code, cb, m)
}
}
return c.JSONP(o.code, DefaultJsonpCallback, m)
case echo.MIMEApplicationJSON:
return c.JSON(o.code, m)
case echo.MIMEApplicationXML, echo.MIMETextXML:
return c.XML(o.code, m)
case echo.MIMETextHTML:
if html, err := HtmlMarshaller(m); err != nil {
return err
} else {
return c.HTML(o.code, html)
}
case echo.MIMETextPlain:
if text, err := TextMarshaller(m); err != nil {
return err
} else {
return c.String(o.code, text)
}
}
}
return c.JSON(o.code, m)
}
func Respond(c echo.Context, opts ...Option) error {
o := response{code: http.StatusOK}
for _, option := range opts {
option(&o)
}
return respond(c, &o)
}
func Ok(c echo.Context, data any) error {
return Respond(c, Data(data))
}
func Created(c echo.Context, data any) error {
return Respond(c, Data(data), StatusCode(http.StatusCreated))
}
// Fail 响应一个错误
func Fail(c echo.Context, err error, opts ...Option) error {
o := response{code: http.StatusInternalServerError}
for _, option := range opts {
option(&o)
}
o.err = err
var he *echo.HTTPError
if errors.As(err, &he) {
o.code = he.Code
}
return respond(c, &o)
}
// InternalError 响应一个服务器内部错误
func InternalError(c echo.Context, message ...string) error {
return Fail(c, ErrInternal.WithText(message...))
}
// ServiceUnavailable 响应一个服务暂不可用的错误
func ServiceUnavailable(c echo.Context, message ...string) error {
return Fail(c,
ErrServiceUnavailable.WithText(message...),
StatusCode(http.StatusServiceUnavailable))
}
// Unauthorized 需要一个身份验证凭据异常的错误
func Unauthorized(c echo.Context, message ...string) error {
return Fail(c,
ErrUnauthorized.WithText(message...),
StatusCode(http.StatusUnauthorized))
}
// Forbidden 响应一个不具有访问资源所需权限的错误(用户通过了身份验证)
func Forbidden(c echo.Context, message ...string) error {
return Fail(c,
ErrForbidden.WithText(message...),
StatusCode(http.StatusForbidden))
}
// UnprocessableEntity 响应一个处理客户端上传失败的错误
func UnprocessableEntity(c echo.Context, message ...string) error {
return Fail(c,
ErrUnprocessableEntity.WithText(message...),
StatusCode(http.StatusUnprocessableEntity))
}
// BadRequest 响应一个服务器不理解客户端请求的错误
func BadRequest(c echo.Context, message ...string) error {
return Fail(c,
ErrBadRequest.WithText(message...),
StatusCode(http.StatusBadRequest))
}
// BadParams 响应一个客户端提交的参数不符合要求的错误
func BadParams(c echo.Context, message ...string) error {
return Fail(c,
ErrBadParams.WithText(message...),
StatusCode(http.StatusBadRequest))
}
// RecordNotFound 响应一个数据不存在的错误
func RecordNotFound(c echo.Context, message ...string) error {
return Fail(c,
ErrRecordNotFound.WithText(message...),
StatusCode(http.StatusNotFound))
}

@ -0,0 +1,72 @@
package rsp
import (
"errors"
"fmt"
"github.com/labstack/echo/v4"
"net/http"
"strings"
"time"
)
type SseOptions struct {
Id string
Event string
Data string
Retry int
}
func Echo(c echo.Context, e <-chan *SseOptions) error {
w := c.Response().Writer
f, ok := w.(http.Flusher)
if !ok {
return errors.New("unable to get http.Flusher interface; this is probably due " +
"to nginx buffering the response")
}
if want, have := "text/event-stream", c.Request().Header.Get("Accept"); want != have {
return fmt.Errorf("accept header: want %q, have %q; seems like the browser doesn't "+
"support server-side events", want, have)
}
// Instruct nginx to NOT buffer the response
w.Header().Set("X-Accel-Buffering", "no")
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.WriteHeader(http.StatusOK)
// Send heartbeats to ensure the connection stays up
heartbeat := time.NewTicker(30 * time.Second)
defer heartbeat.Stop()
for {
select {
case <-c.Request().Context().Done(): // When the browser closes the connection
f.Flush()
return nil
case <-heartbeat.C:
sendEvent(w, "", "heartbeat", "{}", 2000)
case opts := <-e:
sendEvent(w, cleanNewline(opts.Id), cleanNewline(opts.Event), cleanNewline(opts.Data), opts.Retry)
}
}
}
func cleanNewline(str string) string {
return strings.ReplaceAll(str, "\n", "")
}
func sendEvent(w http.ResponseWriter, id, event, data string, retry int) {
f := w.(http.Flusher)
if id != "" {
fmt.Fprintf(w, "id: %s\n", id)
}
if event != "" {
fmt.Fprintf(w, "event: %s\n", event)
}
if data != "" {
fmt.Fprintf(w, "data: %s\n", data)
}
if retry > 0 {
fmt.Fprintf(w, "retry: %d\n", retry)
}
fmt.Fprint(w, "\n")
f.Flush()
}

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

Loading…
Cancel
Save