commit
815fbd568f
@ -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= |
@ -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 |
||||||
|
) |
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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…
Reference in new issue