refactor: 重构服务层和仓库层

This commit is contained in:
lan
2025-12-03 10:58:39 +08:00
parent 034e02e93a
commit e873c58af9
32 changed files with 1445 additions and 5213 deletions

View File

@@ -77,3 +77,5 @@ minio-data/

View File

@@ -62,3 +62,5 @@ ENTRYPOINT ["./server"]

View File

@@ -9,7 +9,6 @@ import (
"syscall" "syscall"
"time" "time"
_ "carrotskin/docs" // Swagger文档
"carrotskin/internal/container" "carrotskin/internal/container"
"carrotskin/internal/handler" "carrotskin/internal/handler"
"carrotskin/internal/middleware" "carrotskin/internal/middleware"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

99
go.mod
View File

@@ -1,98 +1,93 @@
module carrotskin module carrotskin
go 1.23.0 go 1.24.0
toolchain go1.24.2 toolchain go1.24.2
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.2.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/minio/minio-go/v7 v7.0.66 github.com/minio/minio-go/v7 v7.0.97
github.com/redis/go-redis/v9 v9.0.5 github.com/redis/go-redis/v9 v9.17.2
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/wenlng/go-captcha-assets v1.0.7 github.com/wenlng/go-captcha-assets v1.0.7
github.com/wenlng/go-captcha/v2 v2.0.4 github.com/wenlng/go-captcha/v2 v2.0.4
go.uber.org/zap v1.26.0 go.uber.org/zap v1.27.1
gorm.io/datatypes v1.2.7 gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0 gorm.io/gorm v1.31.1
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect github.com/klauspost/crc32 v1.3.0 // indirect
golang.org/x/image v0.16.0 // indirect github.com/minio/crc64nvme v1.1.0 // indirect
golang.org/x/sync v0.16.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect
gorm.io/driver/mysql v1.5.6 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/image v0.33.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sync v0.18.0 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/bytedance/sonic v1.14.2 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v1.1.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-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.1 // indirect github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 github.com/jackc/pgx/v5 v5.7.6
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rs/xid v1.5.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/swag v1.16.2
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.40.0 golang.org/x/crypto v0.45.0
golang.org/x/net v0.42.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.35.0 // indirect golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

258
go.sum
View File

@@ -1,24 +1,19 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -30,51 +25,40 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 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/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 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -82,8 +66,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -94,65 +78,56 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs= github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -164,124 +139,105 @@ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjb
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/wenlng/go-captcha-assets v1.0.7 h1:tfF84A4un/i4p+TbRVHDqDPeQeatvddOfB2xbKvLVq8= github.com/wenlng/go-captcha-assets v1.0.7 h1:tfF84A4un/i4p+TbRVHDqDPeQeatvddOfB2xbKvLVq8=
github.com/wenlng/go-captcha-assets v1.0.7/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU41d00+43pNX+b9+MM= github.com/wenlng/go-captcha-assets v1.0.7/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU41d00+43pNX+b9+MM=
github.com/wenlng/go-captcha/v2 v2.0.4 h1:5cSUF36ZyA03qeDMjKmeXGpbYJMXEexZIYK3Vga3ME0= github.com/wenlng/go-captcha/v2 v2.0.4 h1:5cSUF36ZyA03qeDMjKmeXGpbYJMXEexZIYK3Vga3ME0=
github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34= github.com/wenlng/go-captcha/v2 v2.0.4/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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.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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/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-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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -86,7 +86,8 @@ func NewContainer(
// 初始化SignatureService // 初始化SignatureService
signatureService := service.NewSignatureService(c.ProfileRepo, redisClient, logger) signatureService := service.NewSignatureService(c.ProfileRepo, redisClient, logger)
c.YggdrasilService = service.NewYggdrasilService(db, c.UserRepo, c.ProfileRepo, c.TextureRepo, c.TokenRepo, c.YggdrasilRepo, signatureService, redisClient, logger) // 使用组合服务(内部包含认证、会话、序列化、证书服务)
c.YggdrasilService = service.NewYggdrasilServiceComposite(db, c.UserRepo, c.ProfileRepo, c.TokenRepo, c.YggdrasilRepo, signatureService, redisClient, logger)
// 初始化其他服务 // 初始化其他服务
c.SecurityService = service.NewSecurityService(redisClient) c.SecurityService = service.NewSecurityService(redisClient)

View File

@@ -47,6 +47,19 @@ var (
ErrStorageUnavailable = errors.New("存储服务不可用") ErrStorageUnavailable = errors.New("存储服务不可用")
ErrUploadFailed = errors.New("上传失败") ErrUploadFailed = errors.New("上传失败")
// Yggdrasil相关错误
ErrPasswordMismatch = errors.New("密码错误")
ErrPasswordNotSet = errors.New("未生成密码")
ErrInvalidServerID = errors.New("服务器ID格式无效")
ErrSessionNotFound = errors.New("会话不存在或已过期")
ErrSessionMismatch = errors.New("会话验证失败")
ErrUsernameMismatch = errors.New("用户名不匹配")
ErrIPMismatch = errors.New("IP地址不匹配")
ErrInvalidAccessToken = errors.New("访问令牌无效")
ErrProfileMismatch = errors.New("selectedProfile与Token不匹配")
ErrUUIDRequired = errors.New("UUID不能为空")
ErrCertificateGenerate = errors.New("生成证书失败")
// 通用错误 // 通用错误
ErrBadRequest = errors.New("请求参数错误") ErrBadRequest = errors.New("请求参数错误")
ErrInternalServer = errors.New("服务器内部错误") ErrInternalServer = errors.New("服务器内部错误")

View File

@@ -0,0 +1,227 @@
package handler
import (
"carrotskin/internal/container"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// CustomSkinHandler CustomSkinAPI处理器
type CustomSkinHandler struct {
container *container.Container
logger *zap.Logger
}
// NewCustomSkinHandler 创建CustomSkinHandler实例
func NewCustomSkinHandler(c *container.Container) *CustomSkinHandler {
return &CustomSkinHandler{
container: c,
logger: c.Logger,
}
}
// CustomSkinAPIResponse CustomSkinAPI响应格式
type CustomSkinAPIResponse struct {
Username string `json:"username"`
Textures map[string]string `json:"textures,omitempty"`
Skin string `json:"skin,omitempty"`
Cape string `json:"cape,omitempty"`
Elytra string `json:"elytra,omitempty"`
}
// GetPlayerInfo 获取玩家信息
// GET {ROOT}/{USERNAME}.json
func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
username := c.Param("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名不能为空"})
return
}
// 移除 .json 后缀(如果存在)
username = strings.TrimSuffix(username, ".json")
// 查找Profile不区分大小写
profile, err := h.container.ProfileService.GetByProfileName(c.Request.Context(), username)
if err != nil {
h.logger.Debug("未找到玩家",
zap.String("username", username),
zap.Error(err),
)
c.JSON(http.StatusNotFound, gin.H{"error": "玩家未找到"})
return
}
// 构建响应
response := CustomSkinAPIResponse{
Username: profile.Name,
}
// Profile 已经通过 GetByProfileName 预加载了 Skin 和 Cape
// 构建材质字典
textures := make(map[string]string)
hasSkin := false
hasCape := false
hasElytra := false
// 处理皮肤
if profile.SkinID != nil && profile.Skin != nil {
skinHash := profile.Skin.Hash
hasSkin = true
if profile.Skin.IsSlim {
// 如果是slim模型优先添加到slim然后添加default
textures["slim"] = skinHash
textures["default"] = skinHash
} else {
// 如果是default模型优先添加到default然后添加slim
textures["default"] = skinHash
textures["slim"] = skinHash
}
}
// 处理披风
if profile.CapeID != nil && profile.Cape != nil {
textures["cape"] = profile.Cape.Hash
hasCape = true
}
// 处理鞘翅使用cape的hash如果存在cape
if hasCape && profile.Cape != nil {
textures["elytra"] = profile.Cape.Hash
hasElytra = true
}
// 根据材质字典决定返回格式
// 根据协议如果只有皮肤使用default模型可以使用缩略格式
// 但如果有多个不同的材质或需要指定模型,使用完整格式
if hasSkin && !hasCape && !hasElytra {
// 如果只有皮肤使用缩略格式使用default模型的hash
if defaultHash, exists := textures["default"]; exists {
response.Skin = defaultHash
} else if slimHash, exists := textures["slim"]; exists {
// 如果只有slim也使用缩略格式但协议说这会导致手臂渲染错误
response.Skin = slimHash
}
} else if len(textures) > 0 {
// 如果有多个材质或需要指定模型,使用完整格式
response.Textures = textures
}
// 如果没有材质,不设置 textures 和 skin 字段(留空)
// 设置缓存头
c.Header("Cache-Control", "public, max-age=300") // 5分钟缓存
c.Header("Content-Type", "application/json; charset=utf-8")
// 响应If-Modified-Since
if modifiedSince := c.GetHeader("If-Modified-Since"); modifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil {
// 如果资源未修改返回304
if profile.UpdatedAt.Before(t.Add(time.Second)) {
c.Status(http.StatusNotModified)
return
}
}
}
// 设置Last-Modified
c.Header("Last-Modified", profile.UpdatedAt.UTC().Format(http.TimeFormat))
c.JSON(http.StatusOK, response)
}
// GetTexture 获取资源文件
// GET {ROOT}/textures/{hash}
func (h *CustomSkinHandler) GetTexture(c *gin.Context) {
hash := c.Param("hash")
if hash == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "资源标识符不能为空"})
return
}
// 查找Texture
texture, err := h.container.TextureService.GetByHash(c.Request.Context(), hash)
if err != nil {
h.logger.Debug("未找到材质",
zap.String("hash", hash),
zap.Error(err),
)
c.JSON(http.StatusNotFound, gin.H{"error": "资源未找到"})
return
}
// 检查材质状态
if texture.Status != 1 {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不可用"})
return
}
// 解析文件URL获取bucket和objectName
if h.container.Storage == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储服务不可用"})
return
}
bucket, objectName, err := h.container.Storage.ParseFileURL(texture.URL)
if err != nil {
h.logger.Error("解析文件URL失败",
zap.String("url", texture.URL),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "解析文件URL失败"})
return
}
// 获取文件对象
ctx := c.Request.Context()
reader, objInfo, err := h.container.Storage.GetObject(ctx, bucket, objectName)
if err != nil {
h.logger.Error("获取文件失败",
zap.String("bucket", bucket),
zap.String("objectName", objectName),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取文件失败"})
return
}
defer reader.Close()
// 设置HTTP头
c.Header("Content-Type", objInfo.ContentType)
c.Header("Content-Length", fmt.Sprintf("%d", objInfo.Size))
c.Header("Last-Modified", objInfo.LastModified.UTC().Format(http.TimeFormat))
c.Header("ETag", objInfo.ETag)
c.Header("Cache-Control", "public, max-age=86400") // 24小时缓存
// 响应If-Modified-Since
if modifiedSince := c.GetHeader("If-Modified-Since"); modifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil {
// 如果资源未修改返回304
if objInfo.LastModified.Before(t.Add(time.Second)) {
c.Status(http.StatusNotModified)
return
}
}
}
// 响应If-None-Match (ETag)
if noneMatch := c.GetHeader("If-None-Match"); noneMatch != "" {
if noneMatch == objInfo.ETag || noneMatch == fmt.Sprintf(`"%s"`, objInfo.ETag) {
c.Status(http.StatusNotModified)
return
}
}
// 增加下载计数(异步)
go func() {
_ = h.container.TextureRepo.IncrementDownloadCount(texture.ID)
}()
// 流式传输文件内容
c.DataFromReader(http.StatusOK, objInfo.Size, objInfo.ContentType, reader, nil)
}

View File

@@ -16,6 +16,7 @@ type Handlers struct {
Profile *ProfileHandler Profile *ProfileHandler
Captcha *CaptchaHandler Captcha *CaptchaHandler
Yggdrasil *YggdrasilHandler Yggdrasil *YggdrasilHandler
CustomSkin *CustomSkinHandler
} }
// NewHandlers 创建所有Handler实例 // NewHandlers 创建所有Handler实例
@@ -27,13 +28,14 @@ func NewHandlers(c *container.Container) *Handlers {
Profile: NewProfileHandler(c), Profile: NewProfileHandler(c),
Captcha: NewCaptchaHandler(c), Captcha: NewCaptchaHandler(c),
Yggdrasil: NewYggdrasilHandler(c), Yggdrasil: NewYggdrasilHandler(c),
CustomSkin: NewCustomSkinHandler(c),
} }
} }
// RegisterRoutesWithDI 使用依赖注入注册所有路由 // RegisterRoutesWithDI 使用依赖注入注册所有路由
func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) { func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
// 设置Swagger文档 // 健康检查路由
SetupSwagger(router) router.GET("/health", HealthCheck)
// 创建Handler实例 // 创建Handler实例
h := NewHandlers(c) h := NewHandlers(c)
@@ -61,6 +63,9 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
// 系统路由 // 系统路由
registerSystemRoutes(v1) registerSystemRoutes(v1)
// CustomSkinAPI 路由
registerCustomSkinRoutes(v1, h.CustomSkin)
} }
} }
@@ -191,3 +196,22 @@ func registerSystemRoutes(v1 *gin.RouterGroup) {
}) })
} }
} }
// registerCustomSkinRoutes 注册CustomSkinAPI路由
// CustomSkinAPI 协议要求根地址必须以 / 结尾
// 路由格式:
// - {ROOT}/{USERNAME}.json - 获取玩家信息
// - {ROOT}/textures/{hash} - 获取资源文件
//
// 根路径为 /api/v1/csl/
func registerCustomSkinRoutes(v1 *gin.RouterGroup, h *CustomSkinHandler) {
// CustomSkinAPI 路由组
csl := v1.Group("/csl")
{
// 获取玩家信息: {ROOT}/{USERNAME}.json
csl.GET("/:username", h.GetPlayerInfo)
// 获取资源文件: {ROOT}/textures/{hash}
csl.GET("/textures/:hash", h.GetTexture)
}
}

View File

@@ -4,56 +4,9 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
) )
// @title CarrotSkin API
// @version 1.0
// @description CarrotSkin 是一个优秀的 Minecraft 皮肤站 API 服务
// @description
// @description ## 功能特性
// @description - 用户注册/登录/管理
// @description - 材质上传/下载/管理
// @description - Minecraft 档案管理
// @description - 权限控制系统
// @description - 积分系统
// @description
// @description ## 认证方式
// @description 使用 JWT Token 进行身份认证,需要在请求头中包含:
// @description ```
// @description Authorization: Bearer <your-jwt-token>
// @description ```
// @contact.name CarrotSkin Team
// @contact.email support@carrotskin.com
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
func SetupSwagger(router *gin.Engine) {
// Swagger文档路由
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 健康检查接口
router.GET("/health", HealthCheck)
}
// HealthCheck 健康检查 // HealthCheck 健康检查
// @Summary 健康检查
// @Description 检查服务是否正常运行
// @Tags system
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{} "成功"
// @Router /health [get]
func HealthCheck(c *gin.Context) { func HealthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"status": "ok", "status": "ok",

View File

@@ -83,3 +83,5 @@ type YggdrasilRepository interface {
ResetPassword(id int64, password string) error ResetPassword(id int64, password string) error
} }

View File

@@ -37,7 +37,11 @@ func (r *profileRepository) FindByUUID(uuid string) (*model.Profile, error) {
func (r *profileRepository) FindByName(name string) (*model.Profile, error) { func (r *profileRepository) FindByName(name string) (*model.Profile, error) {
var profile model.Profile var profile model.Profile
err := r.db.Where("name = ?", name).First(&profile).Error // 使用 LOWER 函数进行不区分大小写的查询,并预加载 Skin 和 Cape
err := r.db.Where("LOWER(name) = LOWER(?)", name).
Preload("Skin").
Preload("Cape").
First(&profile).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -98,7 +102,10 @@ func (r *profileRepository) UpdateLastUsedAt(uuid string) error {
func (r *profileRepository) GetByNames(names []string) ([]*model.Profile, error) { func (r *profileRepository) GetByNames(names []string) ([]*model.Profile, error) {
var profiles []*model.Profile var profiles []*model.Profile
err := r.db.Where("name in (?)", names).Find(&profiles).Error err := r.db.Where("name in (?)", names).
Preload("Skin").
Preload("Cape").
Find(&profiles).Error
return profiles, err return profiles, err
} }

View File

@@ -37,7 +37,7 @@ func (r *tokenRepository) GetByUserID(userId int64) ([]*model.Token, error) {
func (r *tokenRepository) GetUUIDByAccessToken(accessToken string) (string, error) { func (r *tokenRepository) GetUUIDByAccessToken(accessToken string) (string, error) {
var token model.Token var token model.Token
err := r.db.Where("access_token = ?", accessToken).First(&token).Error err := r.db.Select("profile_id").Where("access_token = ?", accessToken).First(&token).Error
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -46,7 +46,7 @@ func (r *tokenRepository) GetUUIDByAccessToken(accessToken string) (string, erro
func (r *tokenRepository) GetUserIDByAccessToken(accessToken string) (int64, error) { func (r *tokenRepository) GetUserIDByAccessToken(accessToken string) (int64, error) {
var token model.Token var token model.Token
err := r.db.Where("access_token = ?", accessToken).First(&token).Error err := r.db.Select("user_id").Where("access_token = ?", accessToken).First(&token).Error
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@@ -18,7 +18,7 @@ func NewYggdrasilRepository(db *gorm.DB) YggdrasilRepository {
func (r *yggdrasilRepository) GetPasswordByID(id int64) (string, error) { func (r *yggdrasilRepository) GetPasswordByID(id int64) (string, error) {
var yggdrasil model.Yggdrasil var yggdrasil model.Yggdrasil
err := r.db.Where("id = ?", id).First(&yggdrasil).Error err := r.db.Select("password").Where("id = ?", id).First(&yggdrasil).Error
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -28,5 +28,3 @@ func (r *yggdrasilRepository) GetPasswordByID(id int64) (string, error) {
func (r *yggdrasilRepository) ResetPassword(id int64, password string) error { func (r *yggdrasilRepository) ResetPassword(id int64, password string) error {
return r.db.Model(&model.Yggdrasil{}).Where("id = ?", id).Update("password", password).Error return r.db.Model(&model.Yggdrasil{}).Where("id = ?", id).Update("password", password).Error
} }

View File

@@ -18,7 +18,6 @@ import (
var ( var (
slideTileCapt slide.Captcha slideTileCapt slide.Captcha
cfg *config.Config
) )
// 常量定义业务相关配置与Redis连接配置分离 // 常量定义业务相关配置与Redis连接配置分离
@@ -29,8 +28,6 @@ const (
// Init 验证码图初始化 // Init 验证码图初始化
func init() { func init() {
cfg, _ = config.Load()
// 从默认仓库中获取主图
builder := slide.NewBuilder() builder := slide.NewBuilder()
bgImage, err := imagesv2.GetImages() bgImage, err := imagesv2.GetImages()
if err != nil { if err != nil {

View File

@@ -58,6 +58,7 @@ type TextureService interface {
// 材质CRUD // 材质CRUD
Create(ctx context.Context, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error) Create(ctx context.Context, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error)
GetByID(ctx context.Context, id int64) (*model.Texture, error) GetByID(ctx context.Context, id int64) (*model.Texture, error)
GetByHash(ctx context.Context, hash string) (*model.Texture, error)
GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
Search(ctx context.Context, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) Search(ctx context.Context, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
Update(ctx context.Context, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error) Update(ctx context.Context, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error)

View File

@@ -120,6 +120,37 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
return texture2, nil return texture2, nil
} }
func (s *textureService) GetByHash(ctx context.Context, hash string) (*model.Texture, error) {
// 尝试从缓存获取
cacheKey := s.cacheKeys.TextureByHash(hash)
var texture model.Texture
if err := s.cache.Get(ctx, cacheKey, &texture); err == nil {
if texture.Status == -1 {
return nil, errors.New("材质已删除")
}
return &texture, nil
}
// 缓存未命中,从数据库查询
texture2, err := s.textureRepo.FindByHash(hash)
if err != nil {
return nil, err
}
if texture2 == nil {
return nil, ErrTextureNotFound
}
if texture2.Status == -1 {
return nil, errors.New("材质已删除")
}
// 存入缓存异步5分钟过期
go func() {
_ = s.cache.Set(context.Background(), cacheKey, texture2, 5*time.Minute)
}()
return texture2, nil
}
func (s *textureService) GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) { func (s *textureService) GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) {
page, pageSize = NormalizePagination(page, pageSize) page, pageSize = NormalizePagination(page, pageSize)

View File

@@ -0,0 +1,91 @@
package service
import (
apperrors "carrotskin/internal/errors"
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/auth"
"context"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
)
// yggdrasilAuthService Yggdrasil认证服务实现
// 负责认证和密码管理
type yggdrasilAuthService struct {
db *gorm.DB
userRepo repository.UserRepository
yggdrasilRepo repository.YggdrasilRepository
logger *zap.Logger
}
// NewYggdrasilAuthService 创建Yggdrasil认证服务实例内部使用
func NewYggdrasilAuthService(
db *gorm.DB,
userRepo repository.UserRepository,
yggdrasilRepo repository.YggdrasilRepository,
logger *zap.Logger,
) *yggdrasilAuthService {
return &yggdrasilAuthService{
db: db,
userRepo: userRepo,
yggdrasilRepo: yggdrasilRepo,
logger: logger,
}
}
func (s *yggdrasilAuthService) GetUserIDByEmail(ctx context.Context, email string) (int64, error) {
user, err := s.userRepo.FindByEmail(email)
if err != nil {
return 0, apperrors.ErrUserNotFound
}
return user.ID, nil
}
func (s *yggdrasilAuthService) VerifyPassword(ctx context.Context, password string, userID int64) error {
passwordStore, err := s.yggdrasilRepo.GetPasswordByID(userID)
if err != nil {
return apperrors.ErrPasswordNotSet
}
// 使用 bcrypt 验证密码
if !auth.CheckPassword(passwordStore, password) {
return apperrors.ErrPasswordMismatch
}
return nil
}
func (s *yggdrasilAuthService) ResetYggdrasilPassword(ctx context.Context, userID int64) (string, error) {
// 生成新的16位随机密码明文返回给用户
plainPassword := model.GenerateRandomPassword(16)
// 使用 bcrypt 加密密码后存储
hashedPassword, err := auth.HashPassword(plainPassword)
if err != nil {
return "", fmt.Errorf("密码加密失败: %w", err)
}
// 检查Yggdrasil记录是否存在
_, err = s.yggdrasilRepo.GetPasswordByID(userID)
if err != nil {
// 如果不存在,创建新记录
yggdrasil := model.Yggdrasil{
ID: userID,
Password: hashedPassword,
}
if err := s.db.Create(&yggdrasil).Error; err != nil {
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
}
return plainPassword, nil
}
// 如果存在,更新密码(存储加密后的密码)
if err := s.yggdrasilRepo.ResetPassword(userID, hashedPassword); err != nil {
return "", fmt.Errorf("重置Yggdrasil密码失败: %w", err)
}
// 返回明文密码给用户
return plainPassword, nil
}

View File

@@ -0,0 +1,112 @@
package service
import (
apperrors "carrotskin/internal/errors"
"carrotskin/internal/repository"
"context"
"fmt"
"time"
"go.uber.org/zap"
)
// CertificateService 证书服务接口
type CertificateService interface {
// GeneratePlayerCertificate 生成玩家证书
GeneratePlayerCertificate(ctx context.Context, uuid string) (map[string]interface{}, error)
// GetPublicKey 获取公钥
GetPublicKey(ctx context.Context) (string, error)
}
// yggdrasilCertificateService 证书服务实现
type yggdrasilCertificateService struct {
profileRepo repository.ProfileRepository
signatureService *signatureService
logger *zap.Logger
}
// NewCertificateService 创建证书服务实例
func NewCertificateService(
profileRepo repository.ProfileRepository,
signatureService *signatureService,
logger *zap.Logger,
) CertificateService {
return &yggdrasilCertificateService{
profileRepo: profileRepo,
signatureService: signatureService,
logger: logger,
}
}
// GeneratePlayerCertificate 生成玩家证书
func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Context, uuid string) (map[string]interface{}, error) {
if uuid == "" {
return nil, apperrors.ErrUUIDRequired
}
s.logger.Info("开始生成玩家证书",
zap.String("uuid", uuid),
)
// 获取密钥对
keyPair, err := s.profileRepo.GetKeyPair(uuid)
if err != nil {
s.logger.Info("获取用户密钥对失败,将创建新密钥对",
zap.Error(err),
zap.String("uuid", uuid),
)
keyPair = nil
}
// 如果没有找到密钥对或密钥对已过期,创建一个新的
now := time.Now().UTC()
if keyPair == nil || keyPair.Refresh.Before(now) || keyPair.PrivateKey == "" || keyPair.PublicKey == "" {
s.logger.Info("为用户创建新的密钥对",
zap.String("uuid", uuid),
)
keyPair, err = s.signatureService.NewKeyPair()
if err != nil {
s.logger.Error("生成玩家证书密钥对失败",
zap.Error(err),
zap.String("uuid", uuid),
)
return nil, fmt.Errorf("生成玩家证书密钥对失败: %w", err)
}
// 保存密钥对到数据库
err = s.profileRepo.UpdateKeyPair(uuid, keyPair)
if err != nil {
s.logger.Warn("更新用户密钥对失败",
zap.Error(err),
zap.String("uuid", uuid),
)
// 继续执行,即使保存失败
}
}
// 计算expiresAt的毫秒时间戳
expiresAtMillis := keyPair.Expiration.UnixMilli()
// 返回玩家证书
certificate := map[string]interface{}{
"keyPair": map[string]interface{}{
"privateKey": keyPair.PrivateKey,
"publicKey": keyPair.PublicKey,
},
"publicKeySignature": keyPair.PublicKeySignature,
"publicKeySignatureV2": keyPair.PublicKeySignatureV2,
"expiresAt": expiresAtMillis,
"refreshedAfter": keyPair.Refresh.UnixMilli(),
}
s.logger.Info("成功生成玩家证书",
zap.String("uuid", uuid),
)
return certificate, nil
}
// GetPublicKey 获取公钥
func (s *yggdrasilCertificateService) GetPublicKey(ctx context.Context) (string, error) {
return s.signatureService.GetPublicKeyFromRedis()
}

View File

@@ -0,0 +1,156 @@
package service
import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"context"
"encoding/base64"
"time"
"go.uber.org/zap"
)
// SerializationService 序列化服务接口
type SerializationService interface {
// SerializeProfile 序列化档案为Yggdrasil格式
SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{}
// SerializeUser 序列化用户为Yggdrasil格式
SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{}
}
// Property Yggdrasil属性
type Property struct {
Name string `json:"name"`
Value string `json:"value"`
Signature string `json:"signature,omitempty"`
}
// yggdrasilSerializationService 序列化服务实现
type yggdrasilSerializationService struct {
textureRepo repository.TextureRepository
signatureService *signatureService
logger *zap.Logger
}
// NewSerializationService 创建序列化服务实例
func NewSerializationService(
textureRepo repository.TextureRepository,
signatureService *signatureService,
logger *zap.Logger,
) SerializationService {
return &yggdrasilSerializationService{
textureRepo: textureRepo,
signatureService: signatureService,
logger: logger,
}
}
// SerializeProfile 序列化档案为Yggdrasil格式
func (s *yggdrasilSerializationService) SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{} {
// 创建基本材质数据
texturesMap := make(map[string]interface{})
textures := map[string]interface{}{
"timestamp": time.Now().UnixMilli(),
"profileId": profile.UUID,
"profileName": profile.Name,
"textures": texturesMap,
}
// 处理皮肤
if profile.SkinID != nil {
skin, err := s.textureRepo.FindByID(*profile.SkinID)
if err != nil {
s.logger.Error("获取皮肤失败",
zap.Error(err),
zap.Int64("skinID", *profile.SkinID),
)
} else if skin != nil {
texturesMap["SKIN"] = map[string]interface{}{
"url": skin.URL,
"metadata": skin.Size,
}
}
}
// 处理披风
if profile.CapeID != nil {
cape, err := s.textureRepo.FindByID(*profile.CapeID)
if err != nil {
s.logger.Error("获取披风失败",
zap.Error(err),
zap.Int64("capeID", *profile.CapeID),
)
} else if cape != nil {
texturesMap["CAPE"] = map[string]interface{}{
"url": cape.URL,
"metadata": cape.Size,
}
}
}
// 将textures编码为base64
bytes, err := json.Marshal(textures)
if err != nil {
s.logger.Error("序列化textures失败",
zap.Error(err),
zap.String("profileUUID", profile.UUID),
)
return nil
}
textureData := base64.StdEncoding.EncodeToString(bytes)
signature, err := s.signatureService.SignStringWithSHA1withRSA(textureData)
if err != nil {
s.logger.Error("签名textures失败",
zap.Error(err),
zap.String("profileUUID", profile.UUID),
)
return nil
}
// 构建结果
data := map[string]interface{}{
"id": profile.UUID,
"name": profile.Name,
"properties": []Property{
{
Name: "textures",
Value: textureData,
Signature: signature,
},
},
}
return data
}
// SerializeUser 序列化用户为Yggdrasil格式
func (s *yggdrasilSerializationService) SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{} {
if user == nil {
s.logger.Error("尝试序列化空用户")
return nil
}
data := map[string]interface{}{
"id": uuid,
}
// 正确处理 *datatypes.JSON 指针类型
// 如果 Properties 为 nil则设置为 nil否则解引用并解析为 JSON 值
if user.Properties == nil {
data["properties"] = nil
} else {
// datatypes.JSON 是 []byte 类型,需要解析为实际的 JSON 值
var propertiesValue interface{}
if err := json.Unmarshal(*user.Properties, &propertiesValue); err != nil {
s.logger.Warn("解析用户Properties失败使用空值",
zap.Error(err),
zap.Int64("userID", user.ID),
)
data["properties"] = nil
} else {
data["properties"] = propertiesValue
}
}
return data
}

View File

@@ -1,402 +0,0 @@
package service
import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/auth"
"carrotskin/pkg/redis"
"carrotskin/pkg/utils"
"context"
"encoding/base64"
"errors"
"fmt"
"net"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)
// SessionKeyPrefix Redis会话键前缀
const SessionKeyPrefix = "Join_"
// SessionTTL 会话超时时间 - 增加到15分钟
const SessionTTL = 15 * time.Minute
type SessionData struct {
AccessToken string `json:"accessToken"`
UserName string `json:"userName"`
SelectedProfile string `json:"selectedProfile"`
IP string `json:"ip"`
}
// yggdrasilService YggdrasilService的实现
type yggdrasilService struct {
db *gorm.DB
userRepo repository.UserRepository
profileRepo repository.ProfileRepository
textureRepo repository.TextureRepository
tokenRepo repository.TokenRepository
yggdrasilRepo repository.YggdrasilRepository
signatureService *signatureService
redis *redis.Client
logger *zap.Logger
}
// NewYggdrasilService 创建YggdrasilService实例
func NewYggdrasilService(
db *gorm.DB,
userRepo repository.UserRepository,
profileRepo repository.ProfileRepository,
textureRepo repository.TextureRepository,
tokenRepo repository.TokenRepository,
yggdrasilRepo repository.YggdrasilRepository,
signatureService *signatureService,
redisClient *redis.Client,
logger *zap.Logger,
) YggdrasilService {
return &yggdrasilService{
db: db,
userRepo: userRepo,
profileRepo: profileRepo,
textureRepo: textureRepo,
tokenRepo: tokenRepo,
yggdrasilRepo: yggdrasilRepo,
signatureService: signatureService,
redis: redisClient,
logger: logger,
}
}
func (s *yggdrasilService) GetUserIDByEmail(ctx context.Context, email string) (int64, error) {
user, err := s.userRepo.FindByEmail(email)
if err != nil {
return 0, errors.New("用户不存在")
}
if user == nil {
return 0, errors.New("用户不存在")
}
return user.ID, nil
}
func (s *yggdrasilService) VerifyPassword(ctx context.Context, password string, userID int64) error {
passwordStore, err := s.yggdrasilRepo.GetPasswordByID(userID)
if err != nil {
return errors.New("未生成密码")
}
// 使用 bcrypt 验证密码
if !auth.CheckPassword(passwordStore, password) {
return errors.New("密码错误")
}
return nil
}
func (s *yggdrasilService) ResetYggdrasilPassword(ctx context.Context, userID int64) (string, error) {
// 生成新的16位随机密码明文返回给用户
plainPassword := model.GenerateRandomPassword(16)
// 使用 bcrypt 加密密码后存储
hashedPassword, err := auth.HashPassword(plainPassword)
if err != nil {
return "", fmt.Errorf("密码加密失败: %w", err)
}
// 检查Yggdrasil记录是否存在
_, err = s.yggdrasilRepo.GetPasswordByID(userID)
if err != nil {
// 如果不存在,创建新记录
yggdrasil := model.Yggdrasil{
ID: userID,
Password: hashedPassword,
}
if err := s.db.Create(&yggdrasil).Error; err != nil {
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
}
return plainPassword, nil
}
// 如果存在,更新密码(存储加密后的密码)
if err := s.yggdrasilRepo.ResetPassword(userID, hashedPassword); err != nil {
return "", fmt.Errorf("重置Yggdrasil密码失败: %w", err)
}
// 返回明文密码给用户
return plainPassword, nil
}
func (s *yggdrasilService) JoinServer(ctx context.Context, serverID, accessToken, selectedProfile, ip string) error {
// 输入验证
if serverID == "" || accessToken == "" || selectedProfile == "" {
return errors.New("参数不能为空")
}
// 验证serverId格式防止注入攻击
if len(serverID) > 100 || strings.ContainsAny(serverID, "<>\"'&") {
return errors.New("服务器ID格式无效")
}
// 验证IP格式
if ip != "" {
if net.ParseIP(ip) == nil {
return errors.New("IP地址格式无效")
}
}
// 获取和验证Token
token, err := s.tokenRepo.FindByAccessToken(accessToken)
if err != nil {
s.logger.Error(
"验证Token失败",
zap.Error(err),
zap.String("accessToken", accessToken),
)
return fmt.Errorf("验证Token失败: %w", err)
}
// 格式化UUID并验证与Token关联的配置文件
formattedProfile := utils.FormatUUID(selectedProfile)
if token.ProfileId != formattedProfile {
return errors.New("selectedProfile与Token不匹配")
}
profile, err := s.profileRepo.FindByUUID(formattedProfile)
if err != nil {
s.logger.Error(
"获取Profile失败",
zap.Error(err),
zap.String("uuid", formattedProfile),
)
return fmt.Errorf("获取Profile失败: %w", err)
}
// 创建会话数据
data := SessionData{
AccessToken: accessToken,
UserName: profile.Name,
SelectedProfile: formattedProfile,
IP: ip,
}
// 序列化会话数据
marshaledData, err := json.Marshal(data)
if err != nil {
s.logger.Error(
"[ERROR]序列化会话数据失败",
zap.Error(err),
)
return fmt.Errorf("序列化会话数据失败: %w", err)
}
// 存储会话数据到Redis - 使用传入的 ctx
sessionKey := SessionKeyPrefix + serverID
if err = s.redis.Set(ctx, sessionKey, marshaledData, SessionTTL); err != nil {
s.logger.Error(
"保存会话数据失败",
zap.Error(err),
zap.String("serverId", serverID),
)
return fmt.Errorf("保存会话数据失败: %w", err)
}
s.logger.Info(
"玩家成功加入服务器",
zap.String("username", profile.Name),
zap.String("serverId", serverID),
)
return nil
}
func (s *yggdrasilService) HasJoinedServer(ctx context.Context, serverID, username, ip string) error {
if serverID == "" || username == "" {
return errors.New("服务器ID和用户名不能为空")
}
// 从Redis获取会话数据 - 使用传入的 ctx
sessionKey := SessionKeyPrefix + serverID
data, err := s.redis.GetBytes(ctx, sessionKey)
if err != nil {
s.logger.Error("[ERROR] 获取会话数据失败:", zap.Error(err), zap.Any("serverId:", serverID))
return fmt.Errorf("获取会话数据失败: %w", err)
}
// 反序列化会话数据
var sessionData SessionData
if err = json.Unmarshal(data, &sessionData); err != nil {
s.logger.Error("[ERROR] 解析会话数据失败: ", zap.Error(err))
return fmt.Errorf("解析会话数据失败: %w", err)
}
// 验证用户名
if sessionData.UserName != username {
return errors.New("用户名不匹配")
}
// 验证IP(如果提供)
if ip != "" && sessionData.IP != ip {
return errors.New("IP地址不匹配")
}
return nil
}
func (s *yggdrasilService) SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{} {
// 创建基本材质数据
texturesMap := make(map[string]interface{})
textures := map[string]interface{}{
"timestamp": time.Now().UnixMilli(),
"profileId": profile.UUID,
"profileName": profile.Name,
"textures": texturesMap,
}
// 处理皮肤
if profile.SkinID != nil {
skin, err := s.textureRepo.FindByID(*profile.SkinID)
if err != nil {
s.logger.Error("[ERROR] 获取皮肤失败:", zap.Error(err), zap.Any("SkinID:", *profile.SkinID))
} else {
texturesMap["SKIN"] = map[string]interface{}{
"url": skin.URL,
"metadata": skin.Size,
}
}
}
// 处理披风
if profile.CapeID != nil {
cape, err := s.textureRepo.FindByID(*profile.CapeID)
if err != nil {
s.logger.Error("[ERROR] 获取披风失败:", zap.Error(err), zap.Any("capeID:", *profile.CapeID))
} else {
texturesMap["CAPE"] = map[string]interface{}{
"url": cape.URL,
"metadata": cape.Size,
}
}
}
// 将textures编码为base64
bytes, err := json.Marshal(textures)
if err != nil {
s.logger.Error("[ERROR] 序列化textures失败: ", zap.Error(err))
return nil
}
textureData := base64.StdEncoding.EncodeToString(bytes)
signature, err := s.signatureService.SignStringWithSHA1withRSA(textureData)
if err != nil {
s.logger.Error("[ERROR] 签名textures失败: ", zap.Error(err))
return nil
}
// 构建结果
data := map[string]interface{}{
"id": profile.UUID,
"name": profile.Name,
"properties": []Property{
{
Name: "textures",
Value: textureData,
Signature: signature,
},
},
}
return data
}
func (s *yggdrasilService) SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{} {
if user == nil {
s.logger.Error("[ERROR] 尝试序列化空用户")
return nil
}
data := map[string]interface{}{
"id": uuid,
}
// 正确处理 *datatypes.JSON 指针类型
// 如果 Properties 为 nil则设置为 nil否则解引用并解析为 JSON 值
if user.Properties == nil {
data["properties"] = nil
} else {
// datatypes.JSON 是 []byte 类型,需要解析为实际的 JSON 值
var propertiesValue interface{}
if err := json.Unmarshal(*user.Properties, &propertiesValue); err != nil {
s.logger.Warn("[WARN] 解析用户Properties失败使用空值", zap.Error(err))
data["properties"] = nil
} else {
data["properties"] = propertiesValue
}
}
return data
}
func (s *yggdrasilService) GeneratePlayerCertificate(ctx context.Context, uuid string) (map[string]interface{}, error) {
if uuid == "" {
return nil, fmt.Errorf("UUID不能为空")
}
s.logger.Info("[INFO] 开始生成玩家证书用户UUID: %s", zap.String("uuid", uuid))
keyPair, err := s.profileRepo.GetKeyPair(uuid)
if err != nil {
s.logger.Info("[INFO] 获取用户密钥对失败,将创建新密钥对: %v",
zap.Error(err),
zap.String("uuid", uuid),
)
keyPair = nil
}
// 如果没有找到密钥对或密钥对已过期,创建一个新的
now := time.Now().UTC()
if keyPair == nil || keyPair.Refresh.Before(now) || keyPair.PrivateKey == "" || keyPair.PublicKey == "" {
s.logger.Info("[INFO] 为用户创建新的密钥对: %s", zap.String("uuid", uuid))
keyPair, err = s.signatureService.NewKeyPair()
if err != nil {
s.logger.Error("[ERROR] 生成玩家证书密钥对失败: %v",
zap.Error(err),
zap.String("uuid", uuid),
)
return nil, fmt.Errorf("生成玩家证书密钥对失败: %w", err)
}
// 保存密钥对到数据库
err = s.profileRepo.UpdateKeyPair(uuid, keyPair)
if err != nil {
s.logger.Warn("[WARN] 更新用户密钥对失败: %v",
zap.Error(err),
zap.String("uuid", uuid),
)
// 继续执行,即使保存失败
}
}
// 计算expiresAt的毫秒时间戳
expiresAtMillis := keyPair.Expiration.UnixMilli()
// 返回玩家证书
certificate := map[string]interface{}{
"keyPair": map[string]interface{}{
"privateKey": keyPair.PrivateKey,
"publicKey": keyPair.PublicKey,
},
"publicKeySignature": keyPair.PublicKeySignature,
"publicKeySignatureV2": keyPair.PublicKeySignatureV2,
"expiresAt": expiresAtMillis,
"refreshedAfter": keyPair.Refresh.UnixMilli(),
}
s.logger.Info("[INFO] 成功生成玩家证书", zap.String("uuid", uuid))
return certificate, nil
}
func (s *yggdrasilService) GetPublicKey(ctx context.Context) (string, error) {
return s.signatureService.GetPublicKeyFromRedis()
}
type Property struct {
Name string `json:"name"`
Value string `json:"value"`
Signature string `json:"signature,omitempty"`
}

View File

@@ -0,0 +1,131 @@
package service
import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/redis"
"carrotskin/pkg/utils"
"context"
"errors"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
)
// yggdrasilServiceComposite 组合服务,保持接口兼容性
// 将认证、会话、序列化、证书服务组合在一起
type yggdrasilServiceComposite struct {
authService *yggdrasilAuthService
sessionService SessionService
serializationService SerializationService
certificateService CertificateService
profileRepo repository.ProfileRepository
tokenRepo repository.TokenRepository
logger *zap.Logger
}
// NewYggdrasilServiceComposite 创建组合服务实例
func NewYggdrasilServiceComposite(
db *gorm.DB,
userRepo repository.UserRepository,
profileRepo repository.ProfileRepository,
tokenRepo repository.TokenRepository,
yggdrasilRepo repository.YggdrasilRepository,
signatureService *signatureService,
redisClient *redis.Client,
logger *zap.Logger,
) YggdrasilService {
// 创建各个专门的服务
authService := NewYggdrasilAuthService(db, userRepo, yggdrasilRepo, logger)
sessionService := NewSessionService(redisClient, logger)
serializationService := NewSerializationService(
repository.NewTextureRepository(db),
signatureService,
logger,
)
certificateService := NewCertificateService(profileRepo, signatureService, logger)
return &yggdrasilServiceComposite{
authService: authService,
sessionService: sessionService,
serializationService: serializationService,
certificateService: certificateService,
profileRepo: profileRepo,
tokenRepo: tokenRepo,
logger: logger,
}
}
// GetUserIDByEmail 获取用户ID通过邮箱
func (s *yggdrasilServiceComposite) GetUserIDByEmail(ctx context.Context, email string) (int64, error) {
return s.authService.GetUserIDByEmail(ctx, email)
}
// VerifyPassword 验证密码
func (s *yggdrasilServiceComposite) VerifyPassword(ctx context.Context, password string, userID int64) error {
return s.authService.VerifyPassword(ctx, password, userID)
}
// ResetYggdrasilPassword 重置Yggdrasil密码
func (s *yggdrasilServiceComposite) ResetYggdrasilPassword(ctx context.Context, userID int64) (string, error) {
return s.authService.ResetYggdrasilPassword(ctx, userID)
}
// JoinServer 加入服务器
func (s *yggdrasilServiceComposite) JoinServer(ctx context.Context, serverID, accessToken, selectedProfile, ip string) error {
// 验证Token
token, err := s.tokenRepo.FindByAccessToken(accessToken)
if err != nil {
s.logger.Error("验证Token失败",
zap.Error(err),
zap.String("accessToken", accessToken),
)
return fmt.Errorf("验证Token失败: %w", err)
}
// 格式化UUID并验证与Token关联的配置文件
formattedProfile := utils.FormatUUID(selectedProfile)
if token.ProfileId != formattedProfile {
return errors.New("selectedProfile与Token不匹配")
}
// 获取Profile以获取用户名
profile, err := s.profileRepo.FindByUUID(formattedProfile)
if err != nil {
s.logger.Error("获取Profile失败",
zap.Error(err),
zap.String("uuid", formattedProfile),
)
return fmt.Errorf("获取Profile失败: %w", err)
}
// 使用会话服务创建会话
return s.sessionService.CreateSession(ctx, serverID, accessToken, profile.Name, formattedProfile, ip)
}
// HasJoinedServer 验证玩家是否已加入服务器
func (s *yggdrasilServiceComposite) HasJoinedServer(ctx context.Context, serverID, username, ip string) error {
return s.sessionService.ValidateSession(ctx, serverID, username, ip)
}
// SerializeProfile 序列化档案
func (s *yggdrasilServiceComposite) SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{} {
return s.serializationService.SerializeProfile(ctx, profile)
}
// SerializeUser 序列化用户
func (s *yggdrasilServiceComposite) SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{} {
return s.serializationService.SerializeUser(ctx, user, uuid)
}
// GeneratePlayerCertificate 生成玩家证书
func (s *yggdrasilServiceComposite) GeneratePlayerCertificate(ctx context.Context, uuid string) (map[string]interface{}, error) {
return s.certificateService.GeneratePlayerCertificate(ctx, uuid)
}
// GetPublicKey 获取公钥
func (s *yggdrasilServiceComposite) GetPublicKey(ctx context.Context) (string, error) {
return s.certificateService.GetPublicKey(ctx)
}

View File

@@ -0,0 +1,181 @@
package service
import (
apperrors "carrotskin/internal/errors"
"carrotskin/pkg/redis"
"context"
"fmt"
"net"
"strings"
"time"
"go.uber.org/zap"
)
// SessionKeyPrefix Redis会话键前缀
const SessionKeyPrefix = "Join_"
// SessionTTL 会话超时时间 - 增加到15分钟
const SessionTTL = 15 * time.Minute
// SessionData 会话数据
type SessionData struct {
AccessToken string `json:"accessToken"`
UserName string `json:"userName"`
SelectedProfile string `json:"selectedProfile"`
IP string `json:"ip"`
}
// SessionService 会话管理服务接口
type SessionService interface {
// CreateSession 创建服务器会话
CreateSession(ctx context.Context, serverID, accessToken, username, profileUUID, ip string) error
// GetSession 获取会话数据
GetSession(ctx context.Context, serverID string) (*SessionData, error)
// ValidateSession 验证会话用户名和IP
ValidateSession(ctx context.Context, serverID, username, ip string) error
}
// yggdrasilSessionService 会话服务实现
type yggdrasilSessionService struct {
redis *redis.Client
logger *zap.Logger
}
// NewSessionService 创建会话服务实例
func NewSessionService(redisClient *redis.Client, logger *zap.Logger) SessionService {
return &yggdrasilSessionService{
redis: redisClient,
logger: logger,
}
}
// ValidateServerID 验证服务器ID格式
func ValidateServerID(serverID string) error {
if serverID == "" {
return apperrors.ErrInvalidServerID
}
if len(serverID) > 100 || strings.ContainsAny(serverID, "<>\"'&") {
return apperrors.ErrInvalidServerID
}
return nil
}
// ValidateIP 验证IP地址格式
func ValidateIP(ip string) error {
if ip == "" {
return nil // IP是可选的
}
if net.ParseIP(ip) == nil {
return apperrors.ErrIPMismatch
}
return nil
}
// CreateSession 创建服务器会话
func (s *yggdrasilSessionService) CreateSession(ctx context.Context, serverID, accessToken, username, profileUUID, ip string) error {
// 输入验证
if err := ValidateServerID(serverID); err != nil {
return err
}
if accessToken == "" {
return apperrors.ErrInvalidAccessToken
}
if username == "" {
return apperrors.ErrUsernameMismatch
}
if profileUUID == "" {
return apperrors.ErrProfileMismatch
}
if err := ValidateIP(ip); err != nil {
return err
}
// 创建会话数据
data := SessionData{
AccessToken: accessToken,
UserName: username,
SelectedProfile: profileUUID,
IP: ip,
}
// 序列化会话数据
marshaledData, err := json.Marshal(data)
if err != nil {
s.logger.Error("序列化会话数据失败",
zap.Error(err),
zap.String("serverID", serverID),
)
return fmt.Errorf("序列化会话数据失败: %w", err)
}
// 存储会话数据到Redis
sessionKey := SessionKeyPrefix + serverID
if err = s.redis.Set(ctx, sessionKey, marshaledData, SessionTTL); err != nil {
s.logger.Error("保存会话数据失败",
zap.Error(err),
zap.String("serverID", serverID),
)
return fmt.Errorf("保存会话数据失败: %w", err)
}
s.logger.Info("会话创建成功",
zap.String("username", username),
zap.String("serverID", serverID),
)
return nil
}
// GetSession 获取会话数据
func (s *yggdrasilSessionService) GetSession(ctx context.Context, serverID string) (*SessionData, error) {
if err := ValidateServerID(serverID); err != nil {
return nil, err
}
// 从Redis获取会话数据
sessionKey := SessionKeyPrefix + serverID
data, err := s.redis.GetBytes(ctx, sessionKey)
if err != nil {
s.logger.Error("获取会话数据失败",
zap.Error(err),
zap.String("serverID", serverID),
)
return nil, fmt.Errorf("获取会话数据失败: %w", err)
}
// 反序列化会话数据
var sessionData SessionData
if err = json.Unmarshal(data, &sessionData); err != nil {
s.logger.Error("解析会话数据失败",
zap.Error(err),
zap.String("serverID", serverID),
)
return nil, fmt.Errorf("解析会话数据失败: %w", err)
}
return &sessionData, nil
}
// ValidateSession 验证会话用户名和IP
func (s *yggdrasilSessionService) ValidateSession(ctx context.Context, serverID, username, ip string) error {
if serverID == "" || username == "" {
return apperrors.ErrSessionMismatch
}
sessionData, err := s.GetSession(ctx, serverID)
if err != nil {
return apperrors.ErrSessionNotFound
}
// 验证用户名
if sessionData.UserName != username {
return apperrors.ErrUsernameMismatch
}
// 验证IP如果提供
if ip != "" && sessionData.IP != ip {
return apperrors.ErrIPMismatch
}
return nil
}

View File

@@ -0,0 +1,81 @@
package service
import (
"errors"
"net"
"regexp"
"strings"
)
// Validator Yggdrasil验证器
type Validator struct{}
// NewValidator 创建验证器实例
func NewValidator() *Validator {
return &Validator{}
}
var (
// emailRegex 邮箱正则表达式
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
)
// ValidateServerID 验证服务器ID格式
func (v *Validator) ValidateServerID(serverID string) error {
if serverID == "" {
return errors.New("服务器ID不能为空")
}
if len(serverID) > 100 {
return errors.New("服务器ID长度超过限制最大100字符")
}
// 防止注入攻击:检查危险字符
if strings.ContainsAny(serverID, "<>\"'&") {
return errors.New("服务器ID包含非法字符")
}
return nil
}
// ValidateIP 验证IP地址格式
func (v *Validator) ValidateIP(ip string) error {
if ip == "" {
return nil // IP是可选的
}
if net.ParseIP(ip) == nil {
return errors.New("IP地址格式无效")
}
return nil
}
// ValidateEmail 验证邮箱格式
func (v *Validator) ValidateEmail(email string) error {
if email == "" {
return errors.New("邮箱不能为空")
}
if !emailRegex.MatchString(email) {
return errors.New("邮箱格式不正确")
}
return nil
}
// ValidateUUID 验证UUID格式简单验证
func (v *Validator) ValidateUUID(uuid string) error {
if uuid == "" {
return errors.New("UUID不能为空")
}
// UUID格式xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (32个十六进制字符 + 4个连字符)
if len(uuid) < 32 || len(uuid) > 36 {
return errors.New("UUID格式无效")
}
return nil
}
// ValidateAccessToken 验证访问令牌
func (v *Validator) ValidateAccessToken(token string) error {
if token == "" {
return errors.New("访问令牌不能为空")
}
if len(token) < 10 {
return errors.New("访问令牌格式无效")
}
return nil
}

View File

@@ -47,6 +47,7 @@ type DatabaseConfig struct {
MaxIdleConns int `mapstructure:"max_idle_conns"` MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"` MaxOpenConns int `mapstructure:"max_open_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间
} }
// RedisConfig Redis配置 // RedisConfig Redis配置
@@ -55,7 +56,14 @@ type RedisConfig struct {
Port int `mapstructure:"port"` Port int `mapstructure:"port"`
Password string `mapstructure:"password"` Password string `mapstructure:"password"`
Database int `mapstructure:"database"` Database int `mapstructure:"database"`
PoolSize int `mapstructure:"pool_size"` PoolSize int `mapstructure:"pool_size"` // 连接池大小
MinIdleConns int `mapstructure:"min_idle_conns"` // 最小空闲连接数
MaxRetries int `mapstructure:"max_retries"` // 最大重试次数
DialTimeout time.Duration `mapstructure:"dial_timeout"` // 连接超时
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 读取超时
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 写入超时
PoolTimeout time.Duration `mapstructure:"pool_timeout"` // 连接池超时
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间
} }
// RustFSConfig RustFS对象存储配置 (S3兼容) // RustFSConfig RustFS对象存储配置 (S3兼容)
@@ -159,12 +167,20 @@ func setDefaults() {
viper.SetDefault("database.max_idle_conns", 10) viper.SetDefault("database.max_idle_conns", 10)
viper.SetDefault("database.max_open_conns", 100) viper.SetDefault("database.max_open_conns", 100)
viper.SetDefault("database.conn_max_lifetime", "1h") viper.SetDefault("database.conn_max_lifetime", "1h")
viper.SetDefault("database.conn_max_idle_time", "10m")
// Redis默认配置 // Redis默认配置
viper.SetDefault("redis.host", "localhost") viper.SetDefault("redis.host", "localhost")
viper.SetDefault("redis.port", 6379) viper.SetDefault("redis.port", 6379)
viper.SetDefault("redis.database", 0) viper.SetDefault("redis.database", 0)
viper.SetDefault("redis.pool_size", 10) viper.SetDefault("redis.pool_size", 10)
viper.SetDefault("redis.min_idle_conns", 5)
viper.SetDefault("redis.max_retries", 3)
viper.SetDefault("redis.dial_timeout", "5s")
viper.SetDefault("redis.read_timeout", "3s")
viper.SetDefault("redis.write_timeout", "3s")
viper.SetDefault("redis.pool_timeout", "4s")
viper.SetDefault("redis.conn_max_idle_time", "30m")
// RustFS默认配置 // RustFS默认配置
viper.SetDefault("rustfs.endpoint", "127.0.0.1:9000") viper.SetDefault("rustfs.endpoint", "127.0.0.1:9000")
@@ -219,12 +235,24 @@ func setupEnvMappings() {
viper.BindEnv("database.database", "DATABASE_NAME") viper.BindEnv("database.database", "DATABASE_NAME")
viper.BindEnv("database.ssl_mode", "DATABASE_SSL_MODE") viper.BindEnv("database.ssl_mode", "DATABASE_SSL_MODE")
viper.BindEnv("database.timezone", "DATABASE_TIMEZONE") viper.BindEnv("database.timezone", "DATABASE_TIMEZONE")
viper.BindEnv("database.max_idle_conns", "DATABASE_MAX_IDLE_CONNS")
viper.BindEnv("database.max_open_conns", "DATABASE_MAX_OPEN_CONNS")
viper.BindEnv("database.conn_max_lifetime", "DATABASE_CONN_MAX_LIFETIME")
viper.BindEnv("database.conn_max_idle_time", "DATABASE_CONN_MAX_IDLE_TIME")
// Redis配置 // Redis配置
viper.BindEnv("redis.host", "REDIS_HOST") viper.BindEnv("redis.host", "REDIS_HOST")
viper.BindEnv("redis.port", "REDIS_PORT") viper.BindEnv("redis.port", "REDIS_PORT")
viper.BindEnv("redis.password", "REDIS_PASSWORD") viper.BindEnv("redis.password", "REDIS_PASSWORD")
viper.BindEnv("redis.database", "REDIS_DATABASE") viper.BindEnv("redis.database", "REDIS_DATABASE")
viper.BindEnv("redis.pool_size", "REDIS_POOL_SIZE")
viper.BindEnv("redis.min_idle_conns", "REDIS_MIN_IDLE_CONNS")
viper.BindEnv("redis.max_retries", "REDIS_MAX_RETRIES")
viper.BindEnv("redis.dial_timeout", "REDIS_DIAL_TIMEOUT")
viper.BindEnv("redis.read_timeout", "REDIS_READ_TIMEOUT")
viper.BindEnv("redis.write_timeout", "REDIS_WRITE_TIMEOUT")
viper.BindEnv("redis.pool_timeout", "REDIS_POOL_TIMEOUT")
viper.BindEnv("redis.conn_max_idle_time", "REDIS_CONN_MAX_IDLE_TIME")
// RustFS配置 // RustFS配置
viper.BindEnv("rustfs.endpoint", "RUSTFS_ENDPOINT") viper.BindEnv("rustfs.endpoint", "RUSTFS_ENDPOINT")
@@ -287,13 +315,61 @@ func overrideFromEnv(config *Config) {
} }
} }
// 处理Redis池大小 if connMaxIdleTime := os.Getenv("DATABASE_CONN_MAX_IDLE_TIME"); connMaxIdleTime != "" {
if val, err := time.ParseDuration(connMaxIdleTime); err == nil {
config.Database.ConnMaxIdleTime = val
}
}
// 处理Redis连接池配置
if poolSize := os.Getenv("REDIS_POOL_SIZE"); poolSize != "" { if poolSize := os.Getenv("REDIS_POOL_SIZE"); poolSize != "" {
if val, err := strconv.Atoi(poolSize); err == nil { if val, err := strconv.Atoi(poolSize); err == nil {
config.Redis.PoolSize = val config.Redis.PoolSize = val
} }
} }
if minIdleConns := os.Getenv("REDIS_MIN_IDLE_CONNS"); minIdleConns != "" {
if val, err := strconv.Atoi(minIdleConns); err == nil {
config.Redis.MinIdleConns = val
}
}
if maxRetries := os.Getenv("REDIS_MAX_RETRIES"); maxRetries != "" {
if val, err := strconv.Atoi(maxRetries); err == nil {
config.Redis.MaxRetries = val
}
}
if dialTimeout := os.Getenv("REDIS_DIAL_TIMEOUT"); dialTimeout != "" {
if val, err := time.ParseDuration(dialTimeout); err == nil {
config.Redis.DialTimeout = val
}
}
if readTimeout := os.Getenv("REDIS_READ_TIMEOUT"); readTimeout != "" {
if val, err := time.ParseDuration(readTimeout); err == nil {
config.Redis.ReadTimeout = val
}
}
if writeTimeout := os.Getenv("REDIS_WRITE_TIMEOUT"); writeTimeout != "" {
if val, err := time.ParseDuration(writeTimeout); err == nil {
config.Redis.WriteTimeout = val
}
}
if poolTimeout := os.Getenv("REDIS_POOL_TIMEOUT"); poolTimeout != "" {
if val, err := time.ParseDuration(poolTimeout); err == nil {
config.Redis.PoolTimeout = val
}
}
if connMaxIdleTime := os.Getenv("REDIS_CONN_MAX_IDLE_TIME"); connMaxIdleTime != "" {
if val, err := time.ParseDuration(connMaxIdleTime); err == nil {
config.Redis.ConnMaxIdleTime = val
}
}
// 处理文件上传配置 // 处理文件上传配置
if maxSize := os.Getenv("UPLOAD_MAX_SIZE"); maxSize != "" { if maxSize := os.Getenv("UPLOAD_MAX_SIZE"); maxSize != "" {
if val, err := strconv.ParseInt(maxSize, 10, 64); err == nil { if val, err := strconv.ParseInt(maxSize, 10, 64); err == nil {

View File

@@ -296,6 +296,11 @@ func (b *CacheKeyBuilder) Texture(textureID int64) string {
return fmt.Sprintf("%stexture:id:%d", b.prefix, textureID) return fmt.Sprintf("%stexture:id:%d", b.prefix, textureID)
} }
// TextureByHash 构建材质hash缓存键
func (b *CacheKeyBuilder) TextureByHash(hash string) string {
return fmt.Sprintf("%stexture:hash:%s", b.prefix, hash)
}
// TextureList 构建材质列表缓存键 // TextureList 构建材质列表缓存键
func (b *CacheKeyBuilder) TextureList(userID int64, page int) string { func (b *CacheKeyBuilder) TextureList(userID int64, page int) string {
return fmt.Sprintf("%stexture:user:%d:page:%d", b.prefix, userID, page) return fmt.Sprintf("%stexture:user:%d:page:%d", b.prefix, userID, page)

View File

@@ -69,10 +69,15 @@ func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
connMaxLifetime = 1 * time.Hour connMaxLifetime = 1 * time.Hour
} }
connMaxIdleTime := cfg.ConnMaxIdleTime
if connMaxIdleTime <= 0 {
connMaxIdleTime = 10 * time.Minute
}
sqlDB.SetMaxIdleConns(maxIdleConns) sqlDB.SetMaxIdleConns(maxIdleConns)
sqlDB.SetMaxOpenConns(maxOpenConns) sqlDB.SetMaxOpenConns(maxOpenConns)
sqlDB.SetConnMaxLifetime(connMaxLifetime) sqlDB.SetConnMaxLifetime(connMaxLifetime)
sqlDB.SetConnMaxIdleTime(10 * time.Minute) sqlDB.SetConnMaxIdleTime(connMaxIdleTime)
// 测试连接 // 测试连接
if err := sqlDB.Ping(); err != nil { if err := sqlDB.Ping(); err != nil {

View File

@@ -20,15 +20,60 @@ type Client struct {
// New 创建Redis客户端 // New 创建Redis客户端
func New(cfg config.RedisConfig, logger *zap.Logger) (*Client, error) { func New(cfg config.RedisConfig, logger *zap.Logger) (*Client, error) {
// 设置默认值
poolSize := cfg.PoolSize
if poolSize <= 0 {
poolSize = 10
}
minIdleConns := cfg.MinIdleConns
if minIdleConns <= 0 {
minIdleConns = 5
}
maxRetries := cfg.MaxRetries
if maxRetries <= 0 {
maxRetries = 3
}
dialTimeout := cfg.DialTimeout
if dialTimeout <= 0 {
dialTimeout = 5 * time.Second
}
readTimeout := cfg.ReadTimeout
if readTimeout <= 0 {
readTimeout = 3 * time.Second
}
writeTimeout := cfg.WriteTimeout
if writeTimeout <= 0 {
writeTimeout = 3 * time.Second
}
poolTimeout := cfg.PoolTimeout
if poolTimeout <= 0 {
poolTimeout = 4 * time.Second
}
connMaxIdleTime := cfg.ConnMaxIdleTime
if connMaxIdleTime <= 0 {
connMaxIdleTime = 30 * time.Minute
}
// 创建Redis客户端 // 创建Redis客户端
rdb := redis.NewClient(&redis.Options{ rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Password: cfg.Password, Password: cfg.Password,
DB: cfg.Database, DB: cfg.Database,
PoolSize: cfg.PoolSize, PoolSize: poolSize,
DialTimeout: 5 * time.Second, MinIdleConns: minIdleConns,
ReadTimeout: 3 * time.Second, MaxRetries: maxRetries,
WriteTimeout: 3 * time.Second, DialTimeout: dialTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
PoolTimeout: poolTimeout,
ConnMaxIdleTime: connMaxIdleTime,
}) })
// 测试连接 // 测试连接

View File

@@ -3,6 +3,9 @@ package storage
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"net/url"
"strings"
"time" "time"
"carrotskin/pkg/config" "carrotskin/pkg/config"
@@ -137,3 +140,64 @@ func (s *StorageClient) BuildFileURL(bucketName, objectName string) string {
func (s *StorageClient) GetPublicURL() string { func (s *StorageClient) GetPublicURL() string {
return s.publicURL return s.publicURL
} }
// ObjectInfo 对象信息
type ObjectInfo struct {
Size int64
LastModified time.Time
ContentType string
ETag string
}
// GetObject 获取对象内容和信息
func (s *StorageClient) GetObject(ctx context.Context, bucketName, objectName string) (io.ReadCloser, *ObjectInfo, error) {
obj, err := s.client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
return nil, nil, fmt.Errorf("获取对象失败: %w", err)
}
stat, err := obj.Stat()
if err != nil {
obj.Close()
return nil, nil, fmt.Errorf("获取对象信息失败: %w", err)
}
info := &ObjectInfo{
Size: stat.Size,
LastModified: stat.LastModified,
ContentType: stat.ContentType,
ETag: stat.ETag,
}
return obj, info, nil
}
// ParseFileURL 从文件URL中解析出bucket和objectName
// URL格式: {publicURL}/{bucket}/{objectName}
func (s *StorageClient) ParseFileURL(fileURL string) (bucket, objectName string, err error) {
// 移除 publicURL 前缀
if !strings.HasPrefix(fileURL, s.publicURL) {
return "", "", fmt.Errorf("URL格式不正确必须以 %s 开头", s.publicURL)
}
// 移除 publicURL 前缀和开头的 /
path := strings.TrimPrefix(fileURL, s.publicURL)
path = strings.TrimPrefix(path, "/")
// 解析路径
parts := strings.SplitN(path, "/", 2)
if len(parts) < 2 {
return "", "", fmt.Errorf("URL格式不正确无法解析bucket和objectName")
}
bucket = parts[0]
objectName = parts[1]
// URL解码 objectName
decoded, err := url.PathUnescape(objectName)
if err == nil {
objectName = decoded
}
return bucket, objectName, nil
}