3 Commits
yongye ... dev

Author SHA1 Message Date
lan
a111872b32 feat(auth): upgrade casbin to v3 and enhance connection pool configurations
Some checks failed
Build / build (push) Successful in 2m23s
Build / build-docker (push) Failing after 1m37s
- Upgrade casbin from v2 to v3 across go.mod and pkg/auth/casbin.go
- Add slide captcha verification to registration flow (CheckVerified, ConsumeVerified)
- Add DB wrapper with connection pool statistics and health checks
- Add Redis connection pool optimizations with stats and health monitoring
- Add new config options: ConnMaxLifetime, HealthCheckInterval, EnableRetryOnError
- Optimize slow query threshold from 200ms to 100ms
- Add ping with retry mechanism for database and Redis connections
2026-02-25 19:00:50 +08:00
lan
1da0ee1ed1 refactor(api): separate Yggdrasil routes from v1 API group
All checks were successful
Build / build (push) Successful in 2m15s
Build / build-docker (push) Successful in 1m2s
Move Yggdrasil API endpoints from /api/v1/yggdrasil to /api/yggdrasil for
better route organization and compatibility with standard Yggdrasil protocol.
Also upgrade Go to 1.25, update dependencies, and migrate email package from
jordan-wright/email to gopkg.in/mail.v2. Remove deprecated UUID design plan.
2026-02-24 02:18:15 +08:00
lan
29f0bad2bc feat(yggdrasil): implement standard error responses and UUID format improvements
All checks were successful
Build / build (push) Successful in 2m17s
Build / build-docker (push) Successful in 57s
- Add YggdrasilErrorResponse struct and standard error codes for protocol compliance
- Change UUID storage from varchar(36) to varchar(32) for unsigned format
- Add utility functions: GenerateUUID, FormatUUIDToNoDash, RandomHex
- Support unsigned query parameter in GetProfileByUUID endpoint
- Improve refresh token response with available profiles list
- Fix key pair retrieval to use correct database column (rsa_private_key)
- Update UUID validator to accept both 32-char and 36-char formats
- Add SignStringWithProfileRSA method for profile-specific signing
- Fix profile assignment validation in refresh token flow
2026-02-23 13:26:53 +08:00
45 changed files with 1355 additions and 1708 deletions

View File

@@ -41,19 +41,74 @@ DATABASE_PASSWORD=your_password_here
DATABASE_NAME=carrotskin
DATABASE_SSL_MODE=disable
DATABASE_TIMEZONE=Asia/Shanghai
# 连接池配置(优化后的默认值)
# 最大空闲连接数:在连接池中保持的最大空闲连接数
# 建议值CPU核心数 * 2 ~ CPU核心数 * 4
DATABASE_MAX_IDLE_CONNS=10
# 最大打开连接数:允许的最大并发连接数
# 建议值根据并发需求调整高并发场景可设置更高如200-500
DATABASE_MAX_OPEN_CONNS=100
# 连接最大生命周期:连接被重用前的最大存活时间
# 建议值30分钟到1小时避免长时间占用连接
DATABASE_CONN_MAX_LIFETIME=1h
# 连接最大空闲时间:连接被关闭前的最大空闲时间
# 建议值5-15分钟避免长时间空闲占用资源
DATABASE_CONN_MAX_IDLE_TIME=10m
# 连接获取超时:等待获取连接的超时时间(新增)
# 建议值1-5秒避免长时间阻塞
DATABASE_CONN_TIMEOUT=5s
# 查询超时:单次查询的最大执行时间(新增)
# 建议值5-30秒根据业务查询复杂度调整
DATABASE_QUERY_TIMEOUT=30s
# 慢查询阈值记录慢查询的阈值优化从200ms调整为100ms
# 超过此时间的查询将被记录为警告
DATABASE_SLOW_THRESHOLD=100ms
# 健康检查间隔:定期检查数据库连接健康的间隔(新增)
# 建议值30秒到5分钟
DATABASE_HEALTH_CHECK_INTERVAL=30s
# =============================================================================
# Redis配置
# Redis配置(优化后的默认值)
# =============================================================================
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DATABASE=0
REDIS_POOL_SIZE=10
# 连接池配置(优化后的默认值)
# 连接池大小:允许的最大并发连接数
# 建议值CPU核心数 * 4 ~ CPU核心数 * 8根据并发需求调整
REDIS_POOL_SIZE=16
# 最小空闲连接数:在连接池中保持的最小空闲连接数
# 建议值CPU核心数 * 2 ~ CPU核心数 * 4
REDIS_MIN_IDLE_CONNS=8
# 最大重试次数:操作失败时的最大重试次数
REDIS_MAX_RETRIES=3
# 连接超时:建立连接的超时时间
# 建议值3-10秒
REDIS_DIAL_TIMEOUT=5s
# 读取超时:读取数据的超时时间
# 建议值3-5秒
REDIS_READ_TIMEOUT=3s
# 写入超时:写入数据的超时时间
# 建议值3-5秒
REDIS_WRITE_TIMEOUT=3s
# 连接池超时:等待获取连接的超时时间
# 建议值3-5秒
REDIS_POOL_TIMEOUT=4s
# 连接最大空闲时间:连接被关闭前的最大空闲时间
# 建议值5-15分钟避免长时间空闲占用资源
REDIS_CONN_MAX_IDLE_TIME=10m
# 连接最大生命周期:连接被重用前的最大存活时间
# 建议值15-30分钟避免长时间占用导致连接问题
REDIS_CONN_MAX_LIFETIME=30m
# 健康检查间隔定期检查Redis连接健康的间隔
# 建议值30秒到5分钟
REDIS_HEALTH_CHECK_INTERVAL=30s
# 错误时启用重试:操作失败时是否启用自动重试
# 建议值true生产环境开发环境可设为false
REDIS_ENABLE_RETRY_ON_ERROR=true
# =============================================================================
# RustFS对象存储配置 (S3兼容)

View File

@@ -32,6 +32,7 @@ jobs:
GOOS: linux
GOARCH: amd64
CGO_ENABLED: 0
GOEXPERIMENT: greenteagc
run: go build -v -o mcauth-linux-amd64 ./cmd/server
- name: Upload artifacts

3
.gitignore vendored
View File

@@ -60,7 +60,7 @@ configs/config.yaml
.env.production
# Keep example files
!.env
!.env.example
# Database files
*.db
@@ -110,3 +110,4 @@ service_coverage
.gitignore
docs/
blessing skin材质渲染示例/
plan/

View File

@@ -72,7 +72,7 @@ backend/
3. **配置环境变量**
```bash
cp .env .env
cp .env.example .env
# 根据实际环境填写数据库、Redis、对象存储、邮件等信息
```

View File

@@ -17,7 +17,6 @@ import (
"os/signal"
"syscall"
"time"
_ "time/tzdata"
"carrotskin/internal/container"
"carrotskin/internal/handler"

126
go.mod
View File

@@ -1,24 +1,26 @@
module carrotskin
go 1.24.0
go 1.25.0
toolchain go1.24.2
// Enable new GC (Generational GC) for Go 1.25
// GOGC=100 (default), GOMEMLIMIT=0 (no limit)
// Build with: go build -gcflags="-G=3" to enable generational GC
require (
github.com/alicebob/miniredis/v2 v2.31.1
github.com/casbin/casbin/v2 v2.123.0
github.com/alicebob/miniredis/v2 v2.36.1
github.com/casbin/casbin/v3 v3.10.0
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/minio/minio-go/v7 v7.0.97
github.com/redis/go-redis/v9 v9.17.2
github.com/minio/minio-go/v7 v7.0.98
github.com/redis/go-redis/v9 v9.18.0
github.com/spf13/viper v1.21.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/wenlng/go-captcha-assets v1.0.7
github.com/wenlng/go-captcha/v2 v2.0.4
go.uber.org/zap v1.27.1
gopkg.in/mail.v2 v2.3.1
gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
@@ -26,79 +28,85 @@ require (
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/glebarez/go-sqlite v1.20.3 // indirect
github.com/glebarez/sqlite v1.7.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/glebarez/sqlite v1.11.0 // indirect
github.com/go-ini/ini v1.67.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-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/microsoft/go-mssqldb v1.7.2 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect
github.com/mattn/go-sqlite3 v1.14.34 // indirect
github.com/microsoft/go-mssqldb v1.9.6 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/yuin/gopher-lua v1.1.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
gopkg.in/yaml.v2 v2.4.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/tinylib/msgp v1.6.3 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/image v0.36.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sync v0.19.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gorm.io/driver/mysql v1.6.0 // indirect
gorm.io/driver/sqlserver v1.6.0 // indirect
gorm.io/plugin/dbresolver v1.6.0 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.20.3 // indirect
gorm.io/driver/sqlserver v1.6.3 // indirect
gorm.io/plugin/dbresolver v1.6.2 // indirect
modernc.org/libc v1.68.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect
)
require (
github.com/bytedance/sonic v1.14.2 // indirect
github.com/casbin/gorm-adapter/v3 v3.39.0
github.com/bytedance/sonic v1.15.0 // indirect
github.com/casbin/gorm-adapter/v3 v3.41.0
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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-playground/validator/v10 v10.30.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6
github.com/jackc/pgx/v5 v5.8.0
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -112,17 +120,15 @@ require (
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/swag v1.16.6
github.com/swaggo/swag v1.16.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.45.0
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/arch v0.24.0 // indirect
golang.org/x/crypto v0.48.0
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

394
go.sum
View File

@@ -1,56 +1,55 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y=
github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI=
github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/casbin/casbin/v2 v2.123.0 h1:UkiMllBgn3MrwHGiZTDFVTV9up+W2CRLufZwKiuAmpA=
github.com/casbin/casbin/v2 v2.123.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
github.com/casbin/gorm-adapter/v3 v3.39.0 h1:k15txH6vE4796MuA+LFcU8I1vMjutklyzMXfjDz7lzo=
github.com/casbin/gorm-adapter/v3 v3.39.0/go.mod h1:kjXoK8MqA3E/CcqEF2l3SCkhJj1YiHVR6SF0LMvJoH4=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/casbin/casbin/v3 v3.10.0 h1:039ORla55vCeIZWd0LfzWFt1yiEA5X4W41xBW2bQuHs=
github.com/casbin/casbin/v3 v3.10.0/go.mod h1:5rJbQr2e6AuuDDNxnPc5lQlC9nIgg6nS1zYwKXhpHC8=
github.com/casbin/gorm-adapter/v3 v3.41.0 h1:Xhpi0tfRP9aKPDWDf6dgBxHZ9UM6IophxxPIEGWqCNM=
github.com/casbin/gorm-adapter/v3 v3.41.0/go.mod h1:BQZRJhwUnwMpI+pT2m7/cUJwXxrHfzpBpPcNTyMGeGA=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -67,87 +66,102 @@ 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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
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/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
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-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
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/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/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/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
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.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
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-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/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/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/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/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -155,20 +169,16 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/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/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/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -181,58 +191,58 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
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/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/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw=
github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
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/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
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/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
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/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -247,11 +257,12 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
@@ -263,8 +274,8 @@ github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
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/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
@@ -274,59 +285,70 @@ github.com/wenlng/go-captcha-assets v1.0.7/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU4
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
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/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
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/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -334,76 +356,82 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
@@ -414,18 +442,38 @@ 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/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
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/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/plugin/dbresolver v1.6.0 h1:XvKDeOtTn1EIX6s4SrKpEH82q0gXVemhYjbYZFGFVcw=
gorm.io/plugin/dbresolver v1.6.0/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -32,7 +32,6 @@ type Container struct {
TextureRepo repository.TextureRepository
ClientRepo repository.ClientRepository
YggdrasilRepo repository.YggdrasilRepository
ReportRepo repository.ReportRepository
// Service层
UserService service.UserService
@@ -44,7 +43,6 @@ type Container struct {
SecurityService service.SecurityService
CaptchaService service.CaptchaService
SignatureService *service.SignatureService
ReportService service.ReportService
}
// NewContainer 创建依赖容器
@@ -88,7 +86,6 @@ func NewContainer(
c.TextureRepo = repository.NewTextureRepository(db)
c.ClientRepo = repository.NewClientRepository(db)
c.YggdrasilRepo = repository.NewYggdrasilRepository(db)
c.ReportRepo = repository.NewReportRepository(db)
// 初始化SignatureService作为依赖注入避免在容器中创建并立即调用
// 将SignatureService添加到容器中供其他服务使用
@@ -98,7 +95,6 @@ func NewContainer(
c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger)
c.ReportService = service.NewReportService(c.ReportRepo, c.UserRepo, logger)
// 获取Yggdrasil私钥并创建JWT服务TokenService需要
// 注意这里仍然需要预先初始化因为TokenService在创建时需要YggdrasilJWT

View File

@@ -60,6 +60,10 @@ var (
ErrUUIDRequired = errors.New("UUID不能为空")
ErrCertificateGenerate = errors.New("生成证书失败")
// Yggdrasil协议标准错误
ErrYggForbiddenOperation = errors.New("ForbiddenOperationException")
ErrYggIllegalArgument = errors.New("IllegalArgumentException")
// 通用错误
ErrBadRequest = errors.New("请求参数错误")
ErrInternalServer = errors.New("服务器内部错误")
@@ -138,3 +142,29 @@ func Wrap(err error, message string) error {
}
return fmt.Errorf("%s: %w", message, err)
}
// YggdrasilErrorResponse Yggdrasil协议标准错误响应格式
type YggdrasilErrorResponse struct {
Error string `json:"error"` // 错误的简要描述(机器可读)
ErrorMessage string `json:"errorMessage"` // 错误的详细信息(人类可读)
Cause string `json:"cause,omitempty"` // 该错误的原因(可选)
}
// NewYggdrasilErrorResponse 创建Yggdrasil标准错误响应
func NewYggdrasilErrorResponse(error, errorMessage, cause string) *YggdrasilErrorResponse {
return &YggdrasilErrorResponse{
Error: error,
ErrorMessage: errorMessage,
Cause: cause,
}
}
// YggdrasilErrorCodes Yggdrasil协议错误码常量
const (
// ForbiddenOperationException 错误消息
YggErrInvalidToken = "Invalid token."
YggErrInvalidCredentials = "Invalid credentials. Invalid username or password."
// IllegalArgumentException 错误消息
YggErrProfileAlreadyAssigned = "Access token already has a profile assigned."
)

View File

@@ -41,9 +41,23 @@ func (h *AuthHandler) Register(c *gin.Context) {
return
}
// 验证滑动验证码(检查是否已验证)
if ok, err := h.container.CaptchaService.CheckVerified(c.Request.Context(), req.CaptchaID); err != nil || !ok {
h.logger.Warn("滑动验证码验证失败", zap.String("captcha_id", req.CaptchaID), zap.Error(err))
RespondBadRequest(c, "滑动验证码验证失败", nil)
return
}
// 使用 defer 确保验证码在函数返回前被消耗(不管成功还是失败)
defer func() {
if err := h.container.CaptchaService.ConsumeVerified(c.Request.Context(), req.CaptchaID); err != nil {
h.logger.Warn("消耗验证码失败", zap.String("captcha_id", req.CaptchaID), zap.Error(err))
}
}()
// 验证邮箱验证码
if err := h.container.VerificationService.VerifyCode(c.Request.Context(), req.Email, req.VerificationCode, service.VerificationTypeRegister); err != nil {
h.logger.Warn("验证码验证失败", zap.String("email", req.Email), zap.Error(err))
h.logger.Warn("邮箱验证码验证失败", zap.String("email", req.Email), zap.Error(err))
RespondBadRequest(c, err.Error(), nil)
return
}

View File

@@ -1,495 +0,0 @@
package handler
import (
"carrotskin/internal/container"
"carrotskin/internal/model"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// ReportHandler 举报处理器
type ReportHandler struct {
container *container.Container
logger *zap.Logger
}
// NewReportHandler 创建ReportHandler实例
func NewReportHandler(c *container.Container) *ReportHandler {
return &ReportHandler{
container: c,
logger: c.Logger,
}
}
// CreateReportRequest 创建举报请求
type CreateReportRequest struct {
TargetType string `json:"target_type" binding:"required"` // "texture" 或 "user"
TargetID int64 `json:"target_id" binding:"required"`
Reason string `json:"reason" binding:"required"`
}
// CreateReport 创建举报
// @Summary 创建举报
// @Description 用户举报皮肤或其他用户
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body CreateReportRequest true "举报信息"
// @Success 200 {object} model.Response{data=model.Report} "创建成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 401 {object} model.ErrorResponse "未授权"
// @Router /api/v1/report [post]
func (h *ReportHandler) CreateReport(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
var req CreateReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondBadRequest(c, "参数错误", err)
return
}
// 转换目标类型
var targetType model.ReportType
switch req.TargetType {
case "texture":
targetType = model.ReportTypeTexture
case "user":
targetType = model.ReportTypeUser
default:
RespondBadRequest(c, "无效的举报类型", nil)
return
}
report, err := h.container.ReportService.CreateReport(c.Request.Context(), userID, targetType, req.TargetID, req.Reason)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, report)
}
// GetByID 获取举报详情
// @Summary 获取举报详情
// @Description 获取指定ID的举报详细信息
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "举报ID"
// @Success 200 {object} model.Response{data=model.Report} "获取成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "举报不存在"
// @Router /api/v1/report/{id} [get]
func (h *ReportHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的举报ID", err)
return
}
report, err := h.container.ReportService.GetByID(c.Request.Context(), id)
if err != nil {
RespondNotFound(c, err.Error())
return
}
RespondSuccess(c, report)
}
// GetByReporterID 获取举报人的举报记录
// @Summary 获取举报人的举报记录
// @Description 获取指定用户的举报记录列表
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param reporter_id path int true "举报人ID"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Router /api/v1/report/reporter/{reporter_id} [get]
func (h *ReportHandler) GetByReporterID(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
reporterID, err := strconv.ParseInt(c.Param("reporter_id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的举报人ID", err)
return
}
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
reports, total, err := h.container.ReportService.GetByReporterID(c.Request.Context(), reporterID, userID, page, pageSize)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, gin.H{
"list": reports,
"total": total,
"page": page,
"per_page": pageSize,
})
}
// GetByTarget 获取目标对象的举报记录
// @Summary 获取目标对象的举报记录
// @Description 获取指定目标对象的举报记录列表(仅管理员)
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param target_type path string true "目标类型 (texture/user)"
// @Param target_id path int true "目标ID"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权访问"
// @Router /api/v1/report/target/{target_type}/{target_id} [get]
func (h *ReportHandler) GetByTarget(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
targetTypeStr := c.Param("target_type")
targetID, err := strconv.ParseInt(c.Param("target_id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的目标ID", err)
return
}
var targetType model.ReportType
switch targetTypeStr {
case "texture":
targetType = model.ReportTypeTexture
case "user":
targetType = model.ReportTypeUser
default:
RespondBadRequest(c, "无效的目标类型", nil)
return
}
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
reports, total, err := h.container.ReportService.GetByTarget(c.Request.Context(), targetType, targetID, userID, page, pageSize)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, gin.H{
"list": reports,
"total": total,
"page": page,
"per_page": pageSize,
})
}
// GetByStatus 根据状态查询举报记录
// @Summary 根据状态查询举报记录
// @Description 根据状态查询举报记录列表(仅管理员)
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param status path string true "状态 (pending/approved/rejected)"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Router /api/v1/report/status/{status} [get]
func (h *ReportHandler) GetByStatus(c *gin.Context) {
statusStr := c.Param("status")
var status model.ReportStatus
switch statusStr {
case "pending":
status = model.ReportStatusPending
case "approved":
status = model.ReportStatusApproved
case "rejected":
status = model.ReportStatusRejected
default:
RespondBadRequest(c, "无效的状态", nil)
return
}
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
reports, total, err := h.container.ReportService.GetByStatus(c.Request.Context(), status, page, pageSize)
if err != nil {
RespondServerError(c, err.Error(), err)
return
}
RespondSuccess(c, gin.H{
"list": reports,
"total": total,
"page": page,
"per_page": pageSize,
})
}
// Search 搜索举报记录
// @Summary 搜索举报记录
// @Description 搜索举报记录(仅管理员)
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param keyword query int false "关键词举报人ID或目标ID"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权访问"
// @Router /api/v1/report/search [get]
func (h *ReportHandler) Search(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
keywordStr := c.Query("keyword")
keyword, err := strconv.ParseInt(keywordStr, 10, 64)
if err != nil {
RespondBadRequest(c, "无效的关键词", err)
return
}
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
reports, total, err := h.container.ReportService.Search(c.Request.Context(), keyword, userID, page, pageSize)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, gin.H{
"list": reports,
"total": total,
"page": page,
"per_page": pageSize,
})
}
// ReviewRequest 处理举报请求
type ReviewRequest struct {
Status string `json:"status" binding:"required"` // "approved" 或 "rejected"
ReviewNote string `json:"review_note"`
}
// Review 处理举报记录
// @Summary 处理举报记录
// @Description 管理员处理举报记录
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "举报ID"
// @Param request body ReviewRequest true "处理信息"
// @Success 200 {object} model.Response{data=model.Report} "处理成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权访问"
// @Router /api/v1/report/{id}/review [put]
func (h *ReportHandler) Review(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的举报ID", err)
return
}
var req ReviewRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondBadRequest(c, "参数错误", err)
return
}
// 转换状态
var status model.ReportStatus
switch req.Status {
case "approved":
status = model.ReportStatusApproved
case "rejected":
status = model.ReportStatusRejected
default:
RespondBadRequest(c, "无效的状态", nil)
return
}
report, err := h.container.ReportService.Review(c.Request.Context(), id, userID, status, req.ReviewNote)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, report)
}
// BatchReviewRequest 批量处理举报请求
type BatchReviewRequest struct {
IDs []int64 `json:"ids" binding:"required"`
Status string `json:"status" binding:"required"` // "approved" 或 "rejected"
ReviewNote string `json:"review_note"`
}
// BatchReview 批量处理举报记录
// @Summary 批量处理举报记录
// @Description 管理员批量处理举报记录
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body BatchReviewRequest true "处理信息"
// @Success 200 {object} model.Response{data=map[string]interface{}} "处理成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权访问"
// @Router /api/v1/report/batch-review [put]
func (h *ReportHandler) BatchReview(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
var req BatchReviewRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondBadRequest(c, "参数错误", err)
return
}
// 转换状态
var status model.ReportStatus
switch req.Status {
case "approved":
status = model.ReportStatusApproved
case "rejected":
status = model.ReportStatusRejected
default:
RespondBadRequest(c, "无效的状态", nil)
return
}
affected, err := h.container.ReportService.BatchReview(c.Request.Context(), req.IDs, userID, status, req.ReviewNote)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, gin.H{
"affected": affected,
})
}
// Delete 删除举报记录
// @Summary 删除举报记录
// @Description 删除指定的举报记录
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "举报ID"
// @Success 200 {object} model.Response "删除成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权访问"
// @Router /api/v1/report/{id} [delete]
func (h *ReportHandler) Delete(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的举报ID", err)
return
}
if err := h.container.ReportService.Delete(c.Request.Context(), id, userID); err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, nil)
}
// BatchDeleteRequest 批量删除请求
type BatchDeleteRequest struct {
IDs []int64 `json:"ids" binding:"required"`
}
// BatchDelete 批量删除举报记录
// @Summary 批量删除举报记录
// @Description 管理员批量删除举报记录
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body BatchDeleteRequest true "删除信息"
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权访问"
// @Router /api/v1/report/batch-delete [delete]
func (h *ReportHandler) BatchDelete(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
return
}
var req BatchDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
RespondBadRequest(c, "参数错误", err)
return
}
affected, err := h.container.ReportService.BatchDelete(c.Request.Context(), req.IDs, userID)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, gin.H{
"affected": affected,
})
}
// GetStats 获取举报统计信息
// @Summary 获取举报统计信息
// @Description 获取举报统计信息(仅管理员)
// @Tags report
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Router /api/v1/report/stats [get]
func (h *ReportHandler) GetStats(c *gin.Context) {
stats, err := h.container.ReportService.GetStats(c.Request.Context())
if err != nil {
RespondServerError(c, err.Error(), err)
return
}
RespondSuccess(c, stats)
}

View File

@@ -21,7 +21,6 @@ type Handlers struct {
Yggdrasil *YggdrasilHandler
CustomSkin *CustomSkinHandler
Admin *AdminHandler
Report *ReportHandler
}
// NewHandlers 创建所有Handler实例
@@ -35,7 +34,6 @@ func NewHandlers(c *container.Container) *Handlers {
Yggdrasil: NewYggdrasilHandler(c),
CustomSkin: NewCustomSkinHandler(c),
Admin: NewAdminHandler(c),
Report: NewReportHandler(c),
}
}
@@ -54,7 +52,8 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
h := NewHandlers(c)
// API路由组
v1 := router.Group("/api/v1")
apiGroup := router.Group("/api")
v1 := apiGroup.Group("/v1")
{
// 认证路由无需JWT
registerAuthRoutes(v1, h.Auth)
@@ -71,18 +70,16 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
// 验证码路由
registerCaptchaRoutesWithDI(v1, h.Captcha)
// Yggdrasil API路由组
registerYggdrasilRoutesWithDI(v1, h.Yggdrasil)
// CustomSkinAPI 路由
registerCustomSkinRoutes(v1, h.CustomSkin)
// 管理员路由(需要管理员权限)
registerAdminRoutes(v1, c, h.Admin)
// 举报路由
registerReportRoutes(v1, h.Report, c.JWT)
}
// Yggdrasil API路由组独立于v1路径为 /api/yggdrasil
registerYggdrasilRoutesWithDI(apiGroup, h.Yggdrasil)
}
// registerAuthRoutes 注册认证路由
@@ -241,28 +238,3 @@ func registerCustomSkinRoutes(v1 *gin.RouterGroup, h *CustomSkinHandler) {
csl.GET("/textures/:hash", h.GetTexture)
}
}
// registerReportRoutes 注册举报路由
func registerReportRoutes(v1 *gin.RouterGroup, h *ReportHandler, jwtService *auth.JWTService) {
reportGroup := v1.Group("/report")
{
// 公开路由(无需认证)
reportGroup.GET("/stats", h.GetStats)
// 需要认证的路由
reportAuth := reportGroup.Group("")
reportAuth.Use(middleware.AuthMiddleware(jwtService))
{
reportAuth.POST("", h.CreateReport)
reportAuth.GET("/:id", h.GetByID)
reportAuth.GET("/reporter_id", h.GetByReporterID)
reportAuth.GET("/target", h.GetByTarget)
reportAuth.GET("/status", h.GetByStatus)
reportAuth.GET("/search", h.Search)
reportAuth.PUT("/:id/review", h.Review)
reportAuth.POST("/batch-review", h.BatchReview)
reportAuth.DELETE("/:id", h.Delete)
reportAuth.POST("/batch-delete", h.BatchDelete)
}
}
}

View File

@@ -86,8 +86,8 @@ func checkRedis(ctx context.Context) error {
return errors.New("Redis客户端未初始化")
}
// 使用Ping检查连接
if err := client.Ping(ctx).Err(); err != nil {
// 使用Ping检查连接封装后的方法直接返回error
if err := client.Ping(ctx); err != nil {
return err
}

View File

@@ -3,6 +3,7 @@ package handler
import (
"bytes"
"carrotskin/internal/container"
"carrotskin/internal/errors"
"carrotskin/internal/model"
"carrotskin/pkg/utils"
"io"
@@ -129,10 +130,11 @@ type (
// RefreshResponse 刷新令牌响应
RefreshResponse struct {
AccessToken string `json:"accessToken"`
ClientToken string `json:"clientToken"`
SelectedProfile map[string]interface{} `json:"selectedProfile,omitempty"`
User map[string]interface{} `json:"user,omitempty"`
AccessToken string `json:"accessToken"`
ClientToken string `json:"clientToken"`
SelectedProfile map[string]interface{} `json:"selectedProfile,omitempty"`
AvailableProfiles []map[string]interface{} `json:"availableProfiles"`
User map[string]interface{} `json:"user,omitempty"`
}
)
@@ -175,7 +177,7 @@ func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler {
// @Param request body AuthenticateRequest true "认证请求"
// @Success 200 {object} AuthenticateResponse
// @Failure 403 {object} map[string]string "认证失败"
// @Router /api/v1/yggdrasil/authserver/authenticate [post]
// @Router /api/yggdrasil/authserver/authenticate [post]
func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
rawData, err := io.ReadAll(c.Request.Body)
if err != nil {
@@ -202,7 +204,11 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
profile, err = h.container.ProfileRepo.FindByName(c.Request.Context(), request.Identifier)
if err != nil {
h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err))
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidCredentials,
"",
))
return
}
userId = profile.UserID
@@ -211,13 +217,21 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
if err != nil {
h.logger.Warn("认证失败: 用户不存在", zap.String("identifier", request.Identifier), zap.Error(err))
c.JSON(http.StatusForbidden, gin.H{"error": "用户不存在"})
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidCredentials,
"",
))
return
}
if err := h.container.YggdrasilService.VerifyPassword(c.Request.Context(), request.Password, userId); err != nil {
h.logger.Warn("认证失败: 密码错误", zap.Error(err))
c.JSON(http.StatusForbidden, gin.H{"error": ErrWrongPassword})
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidCredentials,
"",
))
return
}
@@ -264,8 +278,8 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
// @Produce json
// @Param request body ValidTokenRequest true "验证请求"
// @Success 204 "令牌有效"
// @Failure 403 {object} map[string]bool "令牌无效"
// @Router /api/v1/yggdrasil/authserver/validate [post]
// @Failure 403 {object} errors.YggdrasilErrorResponse "令牌无效"
// @Router /api/yggdrasil/authserver/validate [post]
func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
var request ValidTokenRequest
if err := c.ShouldBindJSON(&request); err != nil {
@@ -276,10 +290,14 @@ func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
if h.container.TokenService.Validate(c.Request.Context(), request.AccessToken, request.ClientToken) {
h.logger.Info("令牌验证成功", zap.String("accessToken", request.AccessToken))
c.JSON(http.StatusNoContent, gin.H{"valid": true})
c.JSON(http.StatusNoContent, gin.H{})
} else {
h.logger.Warn("令牌验证失败", zap.String("accessToken", request.AccessToken))
c.JSON(http.StatusForbidden, gin.H{"valid": false})
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidToken,
"",
))
}
}
@@ -292,7 +310,7 @@ func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
// @Param request body RefreshRequest true "刷新请求"
// @Success 200 {object} RefreshResponse
// @Failure 400 {object} map[string]string "刷新失败"
// @Router /api/v1/yggdrasil/authserver/refresh [post]
// @Router /api/yggdrasil/authserver/refresh [post]
func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
var request RefreshRequest
if err := c.ShouldBindJSON(&request); err != nil {
@@ -301,19 +319,33 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
return
}
UUID, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), request.AccessToken)
// 获取用户ID和当前绑定的Profile UUID
userID, err := h.container.TokenService.GetUserIDByAccessToken(c.Request.Context(), request.AccessToken)
if err != nil {
h.logger.Warn("刷新令牌失败: 无效的访问令牌", zap.String("token", request.AccessToken), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidToken,
"",
))
return
}
userID, _ := h.container.TokenService.GetUserIDByAccessToken(c.Request.Context(), request.AccessToken)
UUID = utils.FormatUUID(UUID)
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), UUID)
currentUUID, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), request.AccessToken)
if err != nil {
h.logger.Error("刷新令牌失败: 无法获取用户信息", zap.Error(err))
h.logger.Warn("刷新令牌失败: 无法获取当前Profile", zap.String("token", request.AccessToken), zap.Error(err))
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidToken,
"",
))
return
}
// 获取用户的所有可用profiles
availableProfiles, err := h.container.ProfileService.GetByUserID(c.Request.Context(), userID)
if err != nil {
h.logger.Error("刷新令牌失败: 无法获取用户profiles", zap.Error(err), zap.Int64("userId", userID))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@@ -322,6 +354,7 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
var userData map[string]interface{}
var profileID string
// 处理selectedProfile
if request.SelectedProfile != nil {
profileIDValue, ok := request.SelectedProfile["id"]
if !ok {
@@ -337,25 +370,69 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
return
}
profileID = utils.FormatUUID(profileID)
// 确保profileID是32位无符号格式用于向后兼容
profileID = utils.FormatUUIDToNoDash(profileID)
// 根据Yggdrasil规范当指定selectedProfile时原令牌所绑定的角色必须为空
// 如果原令牌已绑定角色,则不允许更换角色,但允许选择相同的角色以兼容部分启动器
if currentUUID != "" && currentUUID != profileID {
h.logger.Warn("刷新令牌失败: 令牌已绑定其他角色,不允许更换",
zap.Int64("userId", userID),
zap.String("currentUUID", currentUUID),
zap.String("requestedUUID", profileID),
)
c.JSON(http.StatusBadRequest, errors.NewYggdrasilErrorResponse(
"IllegalArgumentException",
errors.YggErrProfileAlreadyAssigned,
"",
))
return
}
// 验证profile是否属于该用户
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), profileID)
if err != nil {
h.logger.Warn("刷新令牌失败: Profile不存在", zap.String("profileId", profileID), zap.Error(err))
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidToken,
"",
))
return
}
if profile.UserID != userID {
h.logger.Warn("刷新令牌失败: 用户不匹配",
zap.Int64("userId", userID),
zap.Int64("profileUserId", profile.UserID),
)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrUserNotMatch})
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidToken,
"",
))
return
}
profileData = h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile)
} else {
// 如果未指定selectedProfile使用当前token绑定的profile如果存在
if currentUUID != "" {
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), currentUUID)
if err == nil && profile != nil {
profileID = currentUUID
profileData = h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile)
}
}
}
// 获取用户信息(如果请求)
user, _ := h.container.UserService.GetByID(c.Request.Context(), userID)
if request.RequestUser && user != nil {
userData = h.container.YggdrasilService.SerializeUser(c.Request.Context(), user, UUID)
userData = h.container.YggdrasilService.SerializeUser(c.Request.Context(), user, currentUUID)
}
// 刷新token
newAccessToken, newClientToken, err := h.container.TokenService.Refresh(c.Request.Context(),
request.AccessToken,
request.ClientToken,
@@ -363,16 +440,27 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
)
if err != nil {
h.logger.Error("刷新令牌失败", zap.Error(err), zap.Int64("userId", userID))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidToken,
"",
))
return
}
// 序列化可用profiles
availableProfilesData := make([]map[string]interface{}, 0, len(availableProfiles))
for _, p := range availableProfiles {
availableProfilesData = append(availableProfilesData, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *p))
}
h.logger.Info("刷新令牌成功", zap.Int64("userId", userID))
c.JSON(http.StatusOK, RefreshResponse{
AccessToken: newAccessToken,
ClientToken: newClientToken,
SelectedProfile: profileData,
User: userData,
AccessToken: newAccessToken,
ClientToken: newClientToken,
SelectedProfile: profileData,
AvailableProfiles: availableProfilesData,
User: userData,
})
}
@@ -384,7 +472,7 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
// @Produce json
// @Param request body ValidTokenRequest true "失效请求"
// @Success 204 "操作成功"
// @Router /api/v1/yggdrasil/authserver/invalidate [post]
// @Router /api/yggdrasil/authserver/invalidate [post]
func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
var request ValidTokenRequest
if err := c.ShouldBindJSON(&request); err != nil {
@@ -407,7 +495,7 @@ func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
// @Param request body SignOutRequest true "登出请求"
// @Success 204 "操作成功"
// @Failure 400 {object} map[string]string "参数错误"
// @Router /api/v1/yggdrasil/authserver/signout [post]
// @Router /api/yggdrasil/authserver/signout [post]
func (h *YggdrasilHandler) SignOut(c *gin.Context) {
var request SignOutRequest
if err := c.ShouldBindJSON(&request); err != nil {
@@ -431,7 +519,11 @@ func (h *YggdrasilHandler) SignOut(c *gin.Context) {
if err := h.container.YggdrasilService.VerifyPassword(c.Request.Context(), request.Password, user.ID); err != nil {
h.logger.Warn("登出失败: 密码错误", zap.Int64("userId", user.ID))
c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword})
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidCredentials,
"",
))
return
}
@@ -447,22 +539,35 @@ func (h *YggdrasilHandler) SignOut(c *gin.Context) {
// @Accept json
// @Produce json
// @Param uuid path string true "用户UUID"
// @Param unsigned query string false "是否不返回签名true/false默认false"
// @Success 200 {object} map[string]interface{} "档案信息"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/profile/{uuid} [get]
// @Failure 404 {object} errors.YggdrasilErrorResponse "档案不存在"
// @Router /api/yggdrasil/sessionserver/session/minecraft/profile/{uuid} [get]
func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
uuid := utils.FormatUUID(c.Param("uuid"))
uuid := c.Param("uuid")
h.logger.Info("获取配置文件请求", zap.String("uuid", uuid))
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), uuid)
if err != nil {
h.logger.Error("获取配置文件失败", zap.Error(err), zap.String("uuid", uuid))
standardResponse(c, http.StatusInternalServerError, nil, err.Error())
c.JSON(http.StatusNotFound, errors.NewYggdrasilErrorResponse(
"Not Found",
"Profile not found",
"",
))
return
}
h.logger.Info("成功获取配置文件", zap.String("uuid", uuid), zap.String("name", profile.Name))
c.JSON(http.StatusOK, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile))
// 读取 unsigned 查询参数
unsignedParam := c.DefaultQuery("unsigned", "false")
unsigned := unsignedParam == "true"
h.logger.Info("成功获取配置文件",
zap.String("uuid", uuid),
zap.String("name", profile.Name),
zap.Bool("unsigned", unsigned))
c.JSON(http.StatusOK, h.container.YggdrasilService.SerializeProfileWithUnsigned(c.Request.Context(), *profile, unsigned))
}
// JoinServer 加入服务器
@@ -475,14 +580,18 @@ func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
// @Success 204 "加入成功"
// @Failure 400 {object} APIResponse "参数错误"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/join [post]
// @Router /api/yggdrasil/sessionserver/session/minecraft/join [post]
func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
var request JoinServerRequest
clientIP := c.ClientIP()
if err := c.ShouldBindJSON(&request); err != nil {
h.logger.Error("解析加入服务器请求失败", zap.Error(err), zap.String("ip", clientIP))
standardResponse(c, http.StatusBadRequest, nil, ErrInvalidRequest)
c.JSON(http.StatusBadRequest, errors.NewYggdrasilErrorResponse(
"Bad Request",
"Invalid request format",
"",
))
return
}
@@ -499,7 +608,11 @@ func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
zap.String("userUUID", request.SelectedProfile),
zap.String("ip", clientIP),
)
standardResponse(c, http.StatusInternalServerError, nil, ErrJoinServerFailed)
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidToken,
"",
))
return
}
@@ -522,7 +635,7 @@ func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
// @Param ip query string false "客户端IP"
// @Success 200 {object} map[string]interface{} "验证成功,返回档案"
// @Failure 204 "验证失败"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/hasJoined [get]
// @Router /api/yggdrasil/sessionserver/session/minecraft/hasJoined [get]
func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
clientIP, _ := c.GetQuery("ip")
@@ -557,7 +670,7 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
return
}
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), username)
profile, err := h.container.ProfileService.GetByProfileName(c.Request.Context(), username)
if err != nil {
h.logger.Error("获取用户配置文件失败", zap.Error(err), zap.String("username", username))
standardResponse(c, http.StatusNoContent, nil, ErrProfileNotFound)
@@ -581,7 +694,7 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
// @Param request body []string true "用户名列表"
// @Success 200 {array} model.Profile "档案列表"
// @Failure 400 {object} APIResponse "参数错误"
// @Router /api/v1/yggdrasil/api/profiles/minecraft [post]
// @Router /api/yggdrasil/api/profiles/minecraft [post]
func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
var names []string
@@ -610,7 +723,7 @@ func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
// @Produce json
// @Success 200 {object} map[string]interface{} "元数据"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil [get]
// @Router /api/yggdrasil [get]
func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
meta := gin.H{
"implementationName": "CellAuth",
@@ -628,7 +741,11 @@ func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
signature, err := h.container.YggdrasilService.GetPublicKey(c.Request.Context())
if err != nil {
h.logger.Error("获取公钥失败", zap.Error(err))
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
c.JSON(http.StatusInternalServerError, errors.NewYggdrasilErrorResponse(
"Internal Server Error",
"Failed to get public key",
"",
))
return
}
@@ -648,27 +765,40 @@ func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
// @Produce json
// @Param Authorization header string true "Bearer {token}"
// @Success 200 {object} map[string]interface{} "证书信息"
// @Failure 401 {object} map[string]string "未授权"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/minecraftservices/player/certificates [post]
// @Failure 401 {object} errors.YggdrasilErrorResponse "未授权"
// @Failure 403 {object} errors.YggdrasilErrorResponse "令牌无效"
// @Failure 500 {object} errors.YggdrasilErrorResponse "服务器错误"
// @Router /api/yggdrasil/minecraftservices/player/certificates [post]
func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header not provided"})
c.JSON(http.StatusUnauthorized, errors.NewYggdrasilErrorResponse(
"Unauthorized",
"Authorization header not provided",
"",
))
c.Abort()
return
}
bearerPrefix := "Bearer "
if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
c.JSON(http.StatusUnauthorized, errors.NewYggdrasilErrorResponse(
"Unauthorized",
"Invalid Authorization format",
"",
))
c.Abort()
return
}
tokenID := authHeader[len(bearerPrefix):]
if tokenID == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
c.JSON(http.StatusUnauthorized, errors.NewYggdrasilErrorResponse(
"Unauthorized",
"Invalid Authorization format",
"",
))
c.Abort()
return
}
@@ -676,16 +806,24 @@ func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
uuid, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), tokenID)
if uuid == "" {
h.logger.Error("获取玩家UUID失败", zap.Error(err))
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
"ForbiddenOperationException",
errors.YggErrInvalidToken,
"",
))
return
}
uuid = utils.FormatUUID(uuid)
// UUID已经是32位无符号格式无需转换
certificate, err := h.container.YggdrasilService.GeneratePlayerCertificate(c.Request.Context(), uuid)
if err != nil {
h.logger.Error("生成玩家证书失败", zap.Error(err))
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
c.JSON(http.StatusInternalServerError, errors.NewYggdrasilErrorResponse(
"Internal Server Error",
"Failed to generate player certificate",
"",
))
return
}

View File

@@ -5,10 +5,10 @@ import "time"
// Client 客户端实体用于管理Token版本
// @Description Yggdrasil客户端Token管理数据
type Client struct {
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` // Client UUID
UUID string `gorm:"column:uuid;type:varchar(32);primaryKey" json:"uuid"` // Client UUID
ClientToken string `gorm:"column:client_token;type:varchar(64);not null;uniqueIndex" json:"client_token"` // 客户端Token
UserID int64 `gorm:"column:user_id;not null;index:idx_clients_user_id" json:"user_id"` // 用户ID
ProfileID string `gorm:"column:profile_id;type:varchar(36);index:idx_clients_profile_id" json:"profile_id,omitempty"` // 选中的Profile
ProfileID string `gorm:"column:profile_id;type:varchar(32);index:idx_clients_profile_id" json:"profile_id,omitempty"` // 选中的Profile
Version int `gorm:"column:version;not null;default:0;index:idx_clients_version" json:"version"` // 版本号
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`

View File

@@ -7,7 +7,7 @@ import (
// Profile Minecraft 档案模型
// @Description Minecraft角色档案数据模型
type Profile struct {
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"`
UUID string `gorm:"column:uuid;type:varchar(32);primaryKey" json:"uuid"`
UserID int64 `gorm:"column:user_id;not null;index:idx_profiles_user_created,priority:1" json:"user_id"`
Name string `gorm:"column:name;type:varchar(16);not null;uniqueIndex:idx_profiles_name" json:"name"` // Minecraft 角色名
SkinID *int64 `gorm:"column:skin_id;type:bigint;index:idx_profiles_skin_id" json:"skin_id,omitempty"`

View File

@@ -1,49 +0,0 @@
package model
import (
"time"
)
// ReportType 举报类型
// @Description 举报类型枚举TEXTURE(皮肤)或USER(用户)
type ReportType string
const (
ReportTypeTexture ReportType = "TEXTURE"
ReportTypeUser ReportType = "USER"
)
// ReportStatus 举报状态
// @Description 举报状态枚举PENDING(待处理)、APPROVED(已通过)、REJECTED(已驳回)
type ReportStatus string
const (
ReportStatusPending ReportStatus = "PENDING"
ReportStatusApproved ReportStatus = "APPROVED"
ReportStatusRejected ReportStatus = "REJECTED"
)
// Report 举报模型
// @Description 用户举报记录模型,用于举报皮肤或用户
type Report struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
ReporterID int64 `gorm:"column:reporter_id;not null;index:idx_reports_reporter_created,priority:1" json:"reporter_id"` // 举报人ID
TargetType ReportType `gorm:"column:target_type;type:varchar(50);not null;index:idx_reports_target_status,priority:1" json:"target_type"` // TEXTURE 或 USER
TargetID int64 `gorm:"column:target_id;not null;index:idx_reports_target_status,priority:2" json:"target_id"` // 被举报对象ID皮肤ID或用户ID
Reason string `gorm:"column:reason;type:text;not null" json:"reason"` // 举报原因
Status ReportStatus `gorm:"column:status;type:varchar(50);not null;default:'PENDING';index:idx_reports_status_created,priority:1;index:idx_reports_target_status,priority:3" json:"status"` // PENDING, APPROVED, REJECTED
ReviewerID *int64 `gorm:"column:reviewer_id;type:bigint" json:"reviewer_id,omitempty"` // 处理人ID管理员
ReviewNote string `gorm:"column:review_note;type:text" json:"review_note,omitempty"` // 处理备注
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamp" json:"reviewed_at,omitempty"` // 处理时间
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_reports_reporter_created,priority:2,sort:desc;index:idx_reports_status_created,priority:2,sort:desc" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
// 关联
Reporter *User `gorm:"foreignKey:ReporterID;constraint:OnDelete:CASCADE" json:"reporter,omitempty"`
Reviewer *User `gorm:"foreignKey:ReviewerID;constraint:OnDelete:SET NULL" json:"reviewer,omitempty"`
}
// TableName 指定表名
func (Report) TableName() string {
return "reports"
}

View File

@@ -56,12 +56,17 @@ type TextureRepository interface {
Delete(ctx context.Context, id int64) error
BatchDelete(ctx context.Context, ids []int64) (int64, error) // 批量删除
IncrementDownloadCount(ctx context.Context, id int64) error
IncrementFavoriteCount(ctx context.Context, id int64) error
DecrementFavoriteCount(ctx context.Context, id int64) error
CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error
ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error)
IsFavorited(ctx context.Context, userID, textureID int64) (bool, error)
AddFavorite(ctx context.Context, userID, textureID int64) error
RemoveFavorite(ctx context.Context, userID, textureID int64) error
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
}
// YggdrasilRepository Yggdrasil仓储接口
type YggdrasilRepository interface {
GetPasswordByID(ctx context.Context, id int64) (string, error)
@@ -79,21 +84,3 @@ type ClientRepository interface {
DeleteByClientToken(ctx context.Context, clientToken string) error
DeleteByUserID(ctx context.Context, userID int64) error
}
// ReportRepository 举报仓储接口
type ReportRepository interface {
Create(ctx context.Context, report *model.Report) error
FindByID(ctx context.Context, id int64) (*model.Report, error)
FindByReporterID(ctx context.Context, reporterID int64, page, pageSize int) ([]*model.Report, int64, error)
FindByTarget(ctx context.Context, targetType model.ReportType, targetID int64, page, pageSize int) ([]*model.Report, int64, error)
FindByStatus(ctx context.Context, status model.ReportStatus, page, pageSize int) ([]*model.Report, int64, error)
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Report, int64, error)
Update(ctx context.Context, report *model.Report) error
UpdateFields(ctx context.Context, id int64, fields map[string]interface{}) error
Review(ctx context.Context, id int64, status model.ReportStatus, reviewerID int64, reviewNote string) error
BatchReview(ctx context.Context, ids []int64, status model.ReportStatus, reviewerID int64, reviewNote string) (int64, error)
Delete(ctx context.Context, id int64) error
BatchDelete(ctx context.Context, ids []int64) (int64, error)
CountByStatus(ctx context.Context, status model.ReportStatus) (int64, error)
CheckDuplicate(ctx context.Context, reporterID int64, targetType model.ReportType, targetID int64) (bool, error)
}

View File

@@ -131,8 +131,8 @@ func (r *profileRepository) GetKeyPair(ctx context.Context, profileId string) (*
var profile model.Profile
result := r.db.WithContext(ctx).
Select("key_pair").
Where("id = ?", profileId).
Select("rsa_private_key").
Where("uuid = ?", profileId).
First(&profile)
if result.Error != nil {
@@ -142,7 +142,9 @@ func (r *profileRepository) GetKeyPair(ctx context.Context, profileId string) (*
return nil, fmt.Errorf("获取key pair失败: %w", result.Error)
}
return &model.KeyPair{}, nil
return &model.KeyPair{
PrivateKey: profile.RSAPrivateKey,
}, nil
}
func (r *profileRepository) UpdateKeyPair(ctx context.Context, profileId string, keyPair *model.KeyPair) error {
@@ -154,12 +156,9 @@ func (r *profileRepository) UpdateKeyPair(ctx context.Context, profileId string,
}
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Table("profiles").
Where("id = ?", profileId).
UpdateColumns(map[string]interface{}{
"private_key": keyPair.PrivateKey,
"public_key": keyPair.PublicKey,
})
result := tx.Model(&model.Profile{}).
Where("uuid = ?", profileId).
Update("rsa_private_key", keyPair.PrivateKey)
if result.Error != nil {
return fmt.Errorf("更新 keyPair 失败: %w", result.Error)

View File

@@ -1,225 +0,0 @@
package repository
import (
"carrotskin/internal/model"
"context"
"errors"
"time"
"gorm.io/gorm"
)
// reportRepository 举报仓储实现
type reportRepository struct {
db *gorm.DB
}
// NewReportRepository 创建举报仓储实例
func NewReportRepository(db *gorm.DB) ReportRepository {
return &reportRepository{db: db}
}
// Create 创建举报记录
func (r *reportRepository) Create(ctx context.Context, report *model.Report) error {
return r.db.WithContext(ctx).Create(report).Error
}
// FindByID 根据ID查找举报记录
func (r *reportRepository) FindByID(ctx context.Context, id int64) (*model.Report, error) {
var report model.Report
err := r.db.WithContext(ctx).Preload("Reporter").Preload("Reviewer").First(&report, id).Error
if err != nil {
return nil, err
}
return &report, nil
}
// FindByReporterID 根据举报人ID查找举报记录
func (r *reportRepository) FindByReporterID(ctx context.Context, reporterID int64, page, pageSize int) ([]*model.Report, int64, error) {
var reports []*model.Report
var total int64
offset := (page - 1) * pageSize
// 查询总数
if err := r.db.WithContext(ctx).Model(&model.Report{}).Where("reporter_id = ?", reporterID).Count(&total).Error; err != nil {
return nil, 0, err
}
// 查询数据
err := r.db.WithContext(ctx).
Preload("Reporter").
Preload("Reviewer").
Where("reporter_id = ?", reporterID).
Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&reports).Error
return reports, total, err
}
// FindByTarget 根据目标对象查找举报记录
func (r *reportRepository) FindByTarget(ctx context.Context, targetType model.ReportType, targetID int64, page, pageSize int) ([]*model.Report, int64, error) {
var reports []*model.Report
var total int64
offset := (page - 1) * pageSize
// 查询总数
if err := r.db.WithContext(ctx).Model(&model.Report{}).Where("target_type = ? AND target_id = ?", targetType, targetID).Count(&total).Error; err != nil {
return nil, 0, err
}
// 查询数据
err := r.db.WithContext(ctx).
Preload("Reporter").
Preload("Reviewer").
Where("target_type = ? AND target_id = ?", targetType, targetID).
Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&reports).Error
return reports, total, err
}
// FindByStatus 根据状态查找举报记录
func (r *reportRepository) FindByStatus(ctx context.Context, status model.ReportStatus, page, pageSize int) ([]*model.Report, int64, error) {
var reports []*model.Report
var total int64
offset := (page - 1) * pageSize
// 查询总数
if err := r.db.WithContext(ctx).Model(&model.Report{}).Where("status = ?", status).Count(&total).Error; err != nil {
return nil, 0, err
}
// 查询数据
err := r.db.WithContext(ctx).
Preload("Reporter").
Preload("Reviewer").
Where("status = ?", status).
Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&reports).Error
return reports, total, err
}
// Search 搜索举报记录
func (r *reportRepository) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Report, int64, error) {
var reports []*model.Report
var total int64
offset := (page - 1) * pageSize
query := r.db.WithContext(ctx).Model(&model.Report{}).Where("reason LIKE ?", "%"+keyword+"%")
// 查询总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 查询数据
err := query.
Preload("Reporter").
Preload("Reviewer").
Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&reports).Error
return reports, total, err
}
// Update 更新举报记录
func (r *reportRepository) Update(ctx context.Context, report *model.Report) error {
return r.db.WithContext(ctx).Save(report).Error
}
// UpdateFields 更新举报记录的指定字段
func (r *reportRepository) UpdateFields(ctx context.Context, id int64, fields map[string]interface{}) error {
return r.db.WithContext(ctx).Model(&model.Report{}).Where("id = ?", id).Updates(fields).Error
}
// Review 处理举报记录
func (r *reportRepository) Review(ctx context.Context, id int64, status model.ReportStatus, reviewerID int64, reviewNote string) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var report model.Report
if err := tx.First(&report, id).Error; err != nil {
return err
}
// 检查状态是否已被处理
if report.Status != model.ReportStatusPending {
return errors.New("report has already been reviewed")
}
// 更新举报状态
now := time.Now()
updates := map[string]interface{}{
"status": status,
"reviewer_id": reviewerID,
"review_note": reviewNote,
"reviewed_at": &now,
}
return tx.Model(&report).Updates(updates).Error
})
}
// BatchReview 批量处理举报记录
func (r *reportRepository) BatchReview(ctx context.Context, ids []int64, status model.ReportStatus, reviewerID int64, reviewNote string) (int64, error) {
var affected int64
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
result := tx.Model(&model.Report{}).
Where("id IN ? AND status = ?", ids, model.ReportStatusPending).
Updates(map[string]interface{}{
"status": status,
"reviewer_id": reviewerID,
"review_note": reviewNote,
"reviewed_at": &now,
})
if result.Error != nil {
return result.Error
}
affected = result.RowsAffected
return nil
})
return affected, err
}
// Delete 删除举报记录
func (r *reportRepository) Delete(ctx context.Context, id int64) error {
return r.db.WithContext(ctx).Delete(&model.Report{}, id).Error
}
// BatchDelete 批量删除举报记录
func (r *reportRepository) BatchDelete(ctx context.Context, ids []int64) (int64, error) {
result := r.db.WithContext(ctx).Delete(&model.Report{}, ids)
return result.RowsAffected, result.Error
}
// CountByStatus 根据状态统计举报数量
func (r *reportRepository) CountByStatus(ctx context.Context, status model.ReportStatus) (int64, error) {
var count int64
err := r.db.WithContext(ctx).Model(&model.Report{}).Where("status = ?", status).Count(&count).Error
return count, err
}
// CheckDuplicate 检查是否重复举报
func (r *reportRepository) CheckDuplicate(ctx context.Context, reporterID int64, targetType model.ReportType, targetID int64) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&model.Report{}).
Where("reporter_id = ? AND target_type = ? AND target_id = ? AND status = ?",
reporterID, targetType, targetID, model.ReportStatusPending).
Count(&count).Error
return count > 0, err
}

View File

@@ -98,6 +98,7 @@ func TestProfileRepository_Basic(t *testing.T) {
t.Fatalf("CountByUserID mismatch: %d err=%v", count, err)
}
if err := profileRepo.UpdateLastUsedAt(ctx, "p-uuid"); err != nil {
t.Fatalf("UpdateLastUsedAt err: %v", err)
}
@@ -149,20 +150,22 @@ func TestTextureRepository_Basic(t *testing.T) {
t.Fatalf("FindByHashAndUploaderID mismatch")
}
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID)
favList, _, _ := textureRepo.GetUserFavorites(ctx, u.ID, 1, 10)
if len(favList) == 0 {
t.Fatalf("GetUserFavorites expected at least 1 favorite")
}
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID)
favList, _, _ = textureRepo.GetUserFavorites(ctx, u.ID, 1, 10)
if len(favList) != 0 {
t.Fatalf("GetUserFavorites expected 0 favorites after toggle off")
}
_ = textureRepo.IncrementFavoriteCount(ctx, tex.ID)
_ = textureRepo.DecrementFavoriteCount(ctx, tex.ID)
_ = textureRepo.IncrementDownloadCount(ctx, tex.ID)
_ = textureRepo.CreateDownloadLog(ctx, &model.TextureDownloadLog{TextureID: tex.ID, UserID: &u.ID, IPAddress: "127.0.0.1"})
// 收藏
_ = textureRepo.AddFavorite(ctx, u.ID, tex.ID)
if fav, err := textureRepo.IsFavorited(ctx, u.ID, tex.ID); err == nil {
if !fav {
t.Fatalf("IsFavorited expected true")
}
} else {
t.Skipf("IsFavorited not supported by sqlite: %v", err)
}
_ = textureRepo.RemoveFavorite(ctx, u.ID, tex.ID)
// 批量更新与删除
if affected, err := textureRepo.BatchUpdate(ctx, []int64{tex.ID}, map[string]interface{}{"name": "tex-new"}); err != nil || affected != 1 {
t.Fatalf("BatchUpdate mismatch, affected=%d err=%v", affected, err)
@@ -184,7 +187,7 @@ func TestTextureRepository_Basic(t *testing.T) {
if list, total, err := textureRepo.Search(ctx, "search", model.TextureTypeCape, true, 1, 10); err != nil || total == 0 || len(list) == 0 {
t.Fatalf("Search mismatch, total=%d len=%d err=%v", total, len(list), err)
}
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID+1)
_ = textureRepo.AddFavorite(ctx, u.ID, tex.ID+1)
if favList, total, err := textureRepo.GetUserFavorites(ctx, u.ID, 1, 10); err != nil || total == 0 || len(favList) == 0 {
t.Fatalf("GetUserFavorites mismatch, total=%d len=%d err=%v", total, len(favList), err)
}
@@ -203,6 +206,7 @@ func TestTextureRepository_Basic(t *testing.T) {
_ = textureRepo.Delete(ctx, tex.ID)
}
func TestClientRepository_Basic(t *testing.T) {
db := testutil.NewTestDB(t)
repo := NewClientRepository(db)

View File

@@ -138,52 +138,42 @@ func (r *textureRepository) IncrementDownloadCount(ctx context.Context, id int64
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error
}
func (r *textureRepository) IncrementFavoriteCount(ctx context.Context, id int64) error {
return r.db.WithContext(ctx).Model(&model.Texture{}).Where("id = ?", id).
UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", 1)).Error
}
func (r *textureRepository) DecrementFavoriteCount(ctx context.Context, id int64) error {
return r.db.WithContext(ctx).Model(&model.Texture{}).Where("id = ?", id).
UpdateColumn("favorite_count", gorm.Expr("favorite_count - ?", 1)).Error
}
func (r *textureRepository) CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error {
return r.db.WithContext(ctx).Create(log).Error
}
func (r *textureRepository) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
var isAdded bool
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
err := tx.Model(&model.UserTextureFavorite{}).
Where("user_id = ? AND texture_id = ?", userID, textureID).
Count(&count).Error
if err != nil {
return err
}
func (r *textureRepository) IsFavorited(ctx context.Context, userID, textureID int64) (bool, error) {
var count int64
// 使用 Select("1") 优化,只查询是否存在,不需要查询所有字段
err := r.db.WithContext(ctx).Model(&model.UserTextureFavorite{}).
Select("1").
Where("user_id = ? AND texture_id = ?", userID, textureID).
Limit(1).
Count(&count).Error
return count > 0, err
}
if count > 0 {
result := tx.Where("user_id = ? AND texture_id = ?", userID, textureID).
Delete(&model.UserTextureFavorite{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected > 0 {
if err := tx.Model(&model.Texture{}).Where("id = ?", textureID).
UpdateColumn("favorite_count", gorm.Expr("GREATEST(favorite_count - 1, 0)")).Error; err != nil {
return err
}
}
isAdded = false
return nil
}
func (r *textureRepository) AddFavorite(ctx context.Context, userID, textureID int64) error {
favorite := &model.UserTextureFavorite{
UserID: userID,
TextureID: textureID,
}
return r.db.WithContext(ctx).Create(favorite).Error
}
favorite := &model.UserTextureFavorite{
UserID: userID,
TextureID: textureID,
}
if err := tx.Create(favorite).Error; err != nil {
return err
}
if err := tx.Model(&model.Texture{}).Where("id = ?", textureID).
UpdateColumn("favorite_count", gorm.Expr("favorite_count + 1")).Error; err != nil {
return err
}
isAdded = true
return nil
})
return isAdded, err
func (r *textureRepository) RemoveFavorite(ctx context.Context, userID, textureID int64) error {
return r.db.WithContext(ctx).Where("user_id = ? AND texture_id = ?", userID, textureID).
Delete(&model.UserTextureFavorite{}).Error
}
func (r *textureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {

View File

@@ -3,13 +3,13 @@ package service
import (
"carrotskin/pkg/config"
"carrotskin/pkg/redis"
"carrotskin/pkg/utils"
"context"
"errors"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/wenlng/go-captcha-assets/resources/imagesv2"
"github.com/wenlng/go-captcha-assets/resources/tiles"
"github.com/wenlng/go-captcha/v2/slide"
@@ -87,7 +87,7 @@ func NewCaptchaService(redisClient *redis.Client, logger *zap.Logger) CaptchaSer
// Generate 生成验证码
func (s *captchaService) Generate(ctx context.Context) (masterImg, tileImg, captchaID string, y int, err error) {
// 生成uuid作为验证码进程唯一标识
captchaID = uuid.NewString()
captchaID = utils.GenerateUUID()
if captchaID == "" {
err = errors.New("生成验证码唯一标识失败")
return
@@ -180,12 +180,50 @@ func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (
ty := redisData.Ty
ok := slide.Validate(dx, ty, tx, ty, paddingValue)
// 验证后立即删除Redis记录防止重复使用
// 验证成功后标记为已验证状态设置5分钟有效期
if ok {
verifiedKey := redisKeyPrefix + "verified:" + captchaID
if err := s.redis.Set(ctx, verifiedKey, "1", 5*time.Minute); err != nil {
s.logger.Warn("设置验证码已验证标记失败", zap.Error(err))
}
// 删除原始验证码记录(防止重复验证)
if err := s.redis.Del(ctx, redisKey); err != nil {
// 记录警告但不影响验证结果
s.logger.Warn("删除验证码Redis记录失败", zap.Error(err))
}
}
return ok, nil
}
// CheckVerified 检查验证码是否已验证仅检查captcha_id
func (s *captchaService) CheckVerified(ctx context.Context, captchaID string) (bool, error) {
// 测试环境下直接通过验证
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
return true, nil
}
verifiedKey := redisKeyPrefix + "verified:" + captchaID
exists, err := s.redis.Exists(ctx, verifiedKey)
if err != nil {
return false, fmt.Errorf("检查验证状态失败: %w", err)
}
if exists == 0 {
return false, errors.New("验证码未验证或已过期")
}
return true, nil
}
// ConsumeVerified 消耗已验证的验证码(注册成功后调用)
func (s *captchaService) ConsumeVerified(ctx context.Context, captchaID string) error {
// 测试环境下直接返回成功
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
return nil
}
verifiedKey := redisKeyPrefix + "verified:" + captchaID
if err := s.redis.Del(ctx, verifiedKey); err != nil {
s.logger.Warn("删除验证码已验证标记失败", zap.Error(err))
}
return nil
}

View File

@@ -79,6 +79,7 @@ type TextureService interface {
type TokenService interface {
// 令牌管理
Create(ctx context.Context, userID int64, uuid, clientToken string) (*model.Profile, []*model.Profile, string, string, error)
CreateWithProfile(ctx context.Context, userID int64, profileUUID string, clientToken string) (*model.Profile, []*model.Profile, string, string, error)
Validate(ctx context.Context, accessToken, clientToken string) bool
Refresh(ctx context.Context, accessToken, clientToken, selectedProfileID string) (string, string, error)
Invalidate(ctx context.Context, accessToken string)
@@ -99,6 +100,8 @@ type VerificationService interface {
type CaptchaService interface {
Generate(ctx context.Context) (masterImg, tileImg, captchaID string, y int, err error)
Verify(ctx context.Context, dx int, captchaID string) (bool, error)
CheckVerified(ctx context.Context, captchaID string) (bool, error)
ConsumeVerified(ctx context.Context, captchaID string) error
}
// YggdrasilService Yggdrasil服务接口
@@ -116,6 +119,7 @@ type YggdrasilService interface {
// 序列化
SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{}
SerializeProfileWithUnsigned(ctx context.Context, profile model.Profile, unsigned bool) map[string]interface{}
SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{}
// 证书
@@ -137,30 +141,6 @@ type SecurityService interface {
ClearVerifyAttempts(ctx context.Context, email, codeType string) error
}
// ReportService 举报服务接口
type ReportService interface {
// 创建举报
CreateReport(ctx context.Context, reporterID int64, targetType model.ReportType, targetID int64, reason string) (*model.Report, error)
// 查询举报
GetByID(ctx context.Context, id int64) (*model.Report, error)
GetByReporterID(ctx context.Context, reporterID, userID int64, page, pageSize int) ([]*model.Report, int64, error)
GetByTarget(ctx context.Context, targetType model.ReportType, targetID, userID int64, page, pageSize int) ([]*model.Report, int64, error)
GetByStatus(ctx context.Context, status model.ReportStatus, page, pageSize int) ([]*model.Report, int64, error)
Search(ctx context.Context, keyword, userID int64, page, pageSize int) ([]*model.Report, int64, error)
// 处理举报
Review(ctx context.Context, reportID, reviewerID int64, status model.ReportStatus, reviewNote string) (*model.Report, error)
BatchReview(ctx context.Context, ids []int64, reviewerID int64, status model.ReportStatus, reviewNote string) (int64, error)
// 删除举报
Delete(ctx context.Context, reportID, userID int64) error
BatchDelete(ctx context.Context, ids []int64, userID int64) (int64, error)
// 统计
GetStats(ctx context.Context) (map[string]int64, error)
}
// Services 服务集合
type Services struct {
User UserService
@@ -171,7 +151,6 @@ type Services struct {
Captcha CaptchaService
Yggdrasil YggdrasilService
Security SecurityService
Report ReportService
}
// ServiceDeps 服务依赖

View File

@@ -391,24 +391,37 @@ func (m *MockTextureRepository) IncrementFavoriteCount(ctx context.Context, id i
return nil
}
func (m *MockTextureRepository) DecrementFavoriteCount(ctx context.Context, id int64) error {
if texture, ok := m.textures[id]; ok && texture.FavoriteCount > 0 {
texture.FavoriteCount--
}
return nil
}
func (m *MockTextureRepository) CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error {
return nil
}
func (m *MockTextureRepository) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
func (m *MockTextureRepository) IsFavorited(ctx context.Context, userID, textureID int64) (bool, error) {
if userFavs, ok := m.favorites[userID]; ok {
return userFavs[textureID], nil
}
return false, nil
}
func (m *MockTextureRepository) AddFavorite(ctx context.Context, userID, textureID int64) error {
if m.favorites[userID] == nil {
m.favorites[userID] = make(map[int64]bool)
}
isFavorited := m.favorites[userID][textureID]
m.favorites[userID][textureID] = !isFavorited
if texture, ok := m.textures[textureID]; ok {
if !isFavorited {
texture.FavoriteCount++
} else if texture.FavoriteCount > 0 {
texture.FavoriteCount--
}
m.favorites[userID][textureID] = true
return nil
}
func (m *MockTextureRepository) RemoveFavorite(ctx context.Context, userID, textureID int64) error {
if userFavs, ok := m.favorites[userID]; ok {
delete(userFavs, textureID)
}
return !isFavorited, nil
return nil
}
func (m *MockTextureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
@@ -461,6 +474,7 @@ func (m *MockTextureRepository) BatchDelete(ctx context.Context, ids []int64) (i
return deleted, nil
}
// ============================================================================
// Service Mocks
// ============================================================================

View File

@@ -4,6 +4,7 @@ import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/database"
"carrotskin/pkg/utils"
"context"
"crypto/rand"
"crypto/rsa"
@@ -12,7 +13,6 @@ import (
"errors"
"fmt"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
)
@@ -64,7 +64,7 @@ func (s *profileService) Create(ctx context.Context, userID int64, name string)
}
// 生成UUID和RSA密钥
profileUUID := uuid.New().String()
profileUUID := utils.GenerateUUID()
privateKey, err := generateRSAPrivateKeyInternal()
if err != nil {
return nil, fmt.Errorf("生成RSA密钥失败: %w", err)

View File

@@ -1,335 +0,0 @@
package service
import (
"context"
"errors"
"strconv"
"time"
apperrors "carrotskin/internal/errors"
"carrotskin/internal/model"
"carrotskin/internal/repository"
"go.uber.org/zap"
)
// reportService ReportService的实现
type reportService struct {
reportRepo repository.ReportRepository
userRepo repository.UserRepository
logger *zap.Logger
}
// NewReportService 创建ReportService实例
func NewReportService(
reportRepo repository.ReportRepository,
userRepo repository.UserRepository,
logger *zap.Logger,
) ReportService {
return &reportService{
reportRepo: reportRepo,
userRepo: userRepo,
logger: logger,
}
}
// CreateReport 创建举报
func (s *reportService) CreateReport(ctx context.Context, reporterID int64, targetType model.ReportType, targetID int64, reason string) (*model.Report, error) {
// 验证举报人存在
reporter, err := s.userRepo.FindByID(ctx, reporterID)
if err != nil {
s.logger.Error("举报人不存在", zap.Int64("reporter_id", reporterID), zap.Error(err))
return nil, apperrors.ErrUserNotFound
}
if reporter == nil {
return nil, apperrors.ErrUserNotFound
}
// 验证举报原因
if reason == "" {
return nil, errors.New("举报原因不能为空")
}
if len(reason) > 500 {
return nil, errors.New("举报原因不能超过500字符")
}
// 验证目标类型
if targetType != model.ReportTypeTexture && targetType != model.ReportTypeUser {
return nil, errors.New("无效的举报类型")
}
// 检查是否重复举报
isDuplicate, err := s.reportRepo.CheckDuplicate(ctx, reporterID, targetType, targetID)
if err != nil {
s.logger.Error("检查重复举报失败", zap.Error(err))
return nil, err
}
if isDuplicate {
return nil, errors.New("您已经举报过该对象,请勿重复举报")
}
// 创建举报记录
report := &model.Report{
ReporterID: reporterID,
TargetType: targetType,
TargetID: targetID,
Reason: reason,
Status: model.ReportStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.reportRepo.Create(ctx, report); err != nil {
s.logger.Error("创建举报失败", zap.Error(err))
return nil, err
}
s.logger.Info("创建举报成功", zap.Int64("report_id", report.ID), zap.Int64("reporter_id", reporterID))
return report, nil
}
// GetByID 根据ID查询举报
func (s *reportService) GetByID(ctx context.Context, id int64) (*model.Report, error) {
report, err := s.reportRepo.FindByID(ctx, id)
if err != nil {
s.logger.Error("查询举报失败", zap.Int64("report_id", id), zap.Error(err))
return nil, err
}
return report, nil
}
// GetByReporterID 根据举报人ID查询举报记录
func (s *reportService) GetByReporterID(ctx context.Context, reporterID, userID int64, page, pageSize int) ([]*model.Report, int64, error) {
// 验证用户存在
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return nil, 0, err
}
if user == nil {
return nil, 0, apperrors.ErrUserNotFound
}
// 只有本人或管理员可以查看自己的举报记录
if reporterID != userID && !(user.Role == "admin") {
return nil, 0, errors.New("无权查看其他用户的举报记录")
}
reports, total, err := s.reportRepo.FindByReporterID(ctx, reporterID, page, pageSize)
if err != nil {
s.logger.Error("查询举报记录失败", zap.Error(err))
return nil, 0, err
}
return reports, total, nil
}
// GetByTarget 根据目标对象查询举报记录
func (s *reportService) GetByTarget(ctx context.Context, targetType model.ReportType, targetID, userID int64, page, pageSize int) ([]*model.Report, int64, error) {
// 验证用户存在
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return nil, 0, err
}
if user == nil {
return nil, 0, apperrors.ErrUserNotFound
}
// 只有管理员可以查看目标对象的举报记录
if !(user.Role == "admin") {
return nil, 0, errors.New("无权查看举报记录")
}
reports, total, err := s.reportRepo.FindByTarget(ctx, targetType, targetID, page, pageSize)
if err != nil {
s.logger.Error("查询举报记录失败", zap.Error(err))
return nil, 0, err
}
return reports, total, nil
}
// GetByStatus 根据状态查询举报记录
func (s *reportService) GetByStatus(ctx context.Context, status model.ReportStatus, page, pageSize int) ([]*model.Report, int64, error) {
reports, total, err := s.reportRepo.FindByStatus(ctx, status, page, pageSize)
if err != nil {
s.logger.Error("查询举报记录失败", zap.Error(err))
return nil, 0, err
}
return reports, total, nil
}
// Search 搜索举报记录
func (s *reportService) Search(ctx context.Context, keyword, userID int64, page, pageSize int) ([]*model.Report, int64, error) {
// 验证用户存在
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return nil, 0, err
}
if user == nil {
return nil, 0, apperrors.ErrUserNotFound
}
// 只有管理员可以搜索举报记录
if !(user.Role == "admin") {
return nil, 0, errors.New("无权搜索举报记录")
}
reports, total, err := s.reportRepo.Search(ctx, strconv.FormatInt(keyword, 10), page, pageSize)
if err != nil {
s.logger.Error("搜索举报记录失败", zap.Error(err))
return nil, 0, err
}
return reports, total, nil
}
// Review 处理举报记录
func (s *reportService) Review(ctx context.Context, reportID, reviewerID int64, status model.ReportStatus, reviewNote string) (*model.Report, error) {
// 验证处理人存在且是管理员
reviewer, err := s.userRepo.FindByID(ctx, reviewerID)
if err != nil {
s.logger.Error("处理人不存在", zap.Int64("reviewer_id", reviewerID), zap.Error(err))
return nil, apperrors.ErrUserNotFound
}
if reviewer == nil || !(reviewer.Role == "admin") {
return nil, errors.New("只有管理员可以处理举报")
}
// 验证状态
if status != model.ReportStatusApproved && status != model.ReportStatusRejected {
return nil, errors.New("无效的举报处理状态")
}
// 处理举报
if err := s.reportRepo.Review(ctx, reportID, status, reviewerID, reviewNote); err != nil {
s.logger.Error("处理举报失败", zap.Int64("report_id", reportID), zap.Error(err))
return nil, err
}
// 返回更新后的举报记录
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
s.logger.Error("查询举报记录失败", zap.Int64("report_id", reportID), zap.Error(err))
return nil, err
}
s.logger.Info("处理举报成功", zap.Int64("report_id", reportID), zap.Int64("reviewer_id", reviewerID), zap.String("status", string(status)))
return report, nil
}
// BatchReview 批量处理举报记录
func (s *reportService) BatchReview(ctx context.Context, ids []int64, reviewerID int64, status model.ReportStatus, reviewNote string) (int64, error) {
// 验证处理人存在且是管理员
reviewer, err := s.userRepo.FindByID(ctx, reviewerID)
if err != nil {
s.logger.Error("处理人不存在", zap.Int64("reviewer_id", reviewerID), zap.Error(err))
return 0, apperrors.ErrUserNotFound
}
if reviewer == nil || !(reviewer.Role == "admin") {
return 0, errors.New("只有管理员可以处理举报")
}
// 验证状态
if status != model.ReportStatusApproved && status != model.ReportStatusRejected {
return 0, errors.New("无效的举报处理状态")
}
// 批量处理举报
affected, err := s.reportRepo.BatchReview(ctx, ids, status, reviewerID, reviewNote)
if err != nil {
s.logger.Error("批量处理举报失败", zap.Error(err))
return 0, err
}
s.logger.Info("批量处理举报成功", zap.Int("count", int(affected)), zap.Int64("reviewer_id", reviewerID), zap.String("status", string(status)))
return affected, nil
}
// Delete 删除举报记录
func (s *reportService) Delete(ctx context.Context, reportID, userID int64) error {
// 验证用户存在
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return err
}
if user == nil {
return apperrors.ErrUserNotFound
}
// 查询举报记录
report, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
return err
}
if report == nil {
return errors.New("举报记录不存在")
}
// 只有举报人、管理员或处理人可以删除举报记录
if report.ReporterID != userID && !(user.Role == "admin") && (report.ReviewerID == nil || *report.ReviewerID != userID) {
return errors.New("无权删除此举报记录")
}
if err := s.reportRepo.Delete(ctx, reportID); err != nil {
s.logger.Error("删除举报记录失败", zap.Int64("report_id", reportID), zap.Error(err))
return err
}
s.logger.Info("删除举报记录成功", zap.Int64("report_id", reportID))
return nil
}
// BatchDelete 批量删除举报记录
func (s *reportService) BatchDelete(ctx context.Context, ids []int64, userID int64) (int64, error) {
// 验证用户存在
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return 0, err
}
if user == nil {
return 0, apperrors.ErrUserNotFound
}
// 只有管理员可以批量删除
if !(user.Role == "admin") {
return 0, errors.New("无权批量删除举报记录")
}
affected, err := s.reportRepo.BatchDelete(ctx, ids)
if err != nil {
s.logger.Error("批量删除举报记录失败", zap.Error(err))
return 0, err
}
s.logger.Info("批量删除举报记录成功", zap.Int("count", int(affected)))
return affected, nil
}
// GetStats 获取举报统计信息
func (s *reportService) GetStats(ctx context.Context) (map[string]int64, error) {
stats := make(map[string]int64)
// 统计各状态的举报数量
pendingCount, err := s.reportRepo.CountByStatus(ctx, model.ReportStatusPending)
if err != nil {
return nil, err
}
stats["pending"] = pendingCount
approvedCount, err := s.reportRepo.CountByStatus(ctx, model.ReportStatusApproved)
if err != nil {
return nil, err
}
stats["approved"] = approvedCount
rejectedCount, err := s.reportRepo.CountByStatus(ctx, model.ReportStatusRejected)
if err != nil {
return nil, err
}
stats["rejected"] = rejectedCount
stats["total"] = pendingCount + approvedCount + rejectedCount
return stats, nil
}

View File

@@ -274,3 +274,26 @@ func FormatPublicKey(publicKeyPEM string) string {
}
return strings.Join(keyLines, "")
}
// SignStringWithProfileRSA 使用Profile的RSA私钥签名字符串
func (s *SignatureService) SignStringWithProfileRSA(data string, privateKeyPEM string) (string, error) {
// 解析PEM格式的私钥
block, _ := pem.Decode([]byte(privateKeyPEM))
if block == nil {
return "", fmt.Errorf("解析PEM私钥失败")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("解析RSA私钥失败: %w", err)
}
// 签名
hashed := sha1.Sum([]byte(data))
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hashed[:])
if err != nil {
return "", fmt.Errorf("签名失败: %w", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}

View File

@@ -219,22 +219,39 @@ func (s *textureService) Delete(ctx context.Context, textureID, uploaderID int64
}
func (s *textureService) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
// 确保材质存在
texture, err := s.textureRepo.FindByID(ctx, textureID)
if err != nil {
return false, err
}
if texture == nil || texture.Status != 1 || !texture.IsPublic {
if texture == nil {
return false, ErrTextureNotFound
}
isAdded, err := s.textureRepo.ToggleFavorite(ctx, userID, textureID)
isFavorited, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
if err != nil {
return false, err
}
s.cacheInv.BatchInvalidate(ctx, s.cacheKeys.UserFavoritesPattern(userID))
if isFavorited {
// 已收藏 -> 取消收藏
if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil {
return false, err
}
if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil {
return false, err
}
return false, nil
}
return isAdded, nil
// 未收藏 -> 添加收藏
if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil {
return false, err
}
if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil {
return false, err
}
return true, nil
}
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
@@ -313,10 +330,10 @@ func (s *textureService) UploadTexture(ctx context.Context, uploaderID int64, na
}
// 生成对象名称(路径)
// 格式: hash/{hash[:2]}/{hash[2:4]}/{hash}.png
// 使用哈希值作为路径,避免重复存储相同文件
// 格式: type/hash[:2]/hash
// 使用哈希值作为文件名,不带扩展名
textureTypeFolder := strings.ToLower(textureType)
objectName := fmt.Sprintf("%s/%s/%s/%s/%s%s", textureTypeFolder, hash[:2], hash[2:4], hash, hash, ext)
objectName := fmt.Sprintf("%s/%s", textureTypeFolder, hash)
// 上传文件
reader := bytes.NewReader(fileData)

View File

@@ -3,7 +3,6 @@ package service
import (
"carrotskin/internal/model"
"context"
"strings"
"testing"
"go.uber.org/zap"
@@ -565,7 +564,7 @@ func TestTextureServiceImpl_Create(t *testing.T) {
ctx := context.Background()
// UploadTexture需要文件数据这里创建一个简单的测试数据
fileData := []byte(strings.Repeat("x", 512))
fileData := []byte("fake png data for testing")
texture, err := textureService.UploadTexture(
ctx,
tt.uploaderID,
@@ -761,7 +760,7 @@ func TestTextureServiceImpl_FavoritesAndLimit(t *testing.T) {
UploaderID: 1,
Name: "T",
})
_, _ = textureRepo.ToggleFavorite(context.Background(), 1, i)
_ = textureRepo.AddFavorite(context.Background(), 1, i)
}
cacheManager := NewMockCacheManager()

View File

@@ -4,12 +4,12 @@ import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/auth"
"carrotskin/pkg/utils"
"context"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"go.uber.org/zap"
)
@@ -63,9 +63,13 @@ func (s *tokenServiceRedis) Create(ctx context.Context, userID int64, UUID strin
}
}
// 生成ClientToken
// 生成ClientToken使用32字符十六进制字符串
if clientToken == "" {
clientToken = uuid.New().String()
var err error
clientToken, err = utils.RandomHex(16) // 16字节 = 32字符十六进制
if err != nil {
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("生成ClientToken失败: %w", err)
}
}
// 获取或创建Client
@@ -73,7 +77,10 @@ func (s *tokenServiceRedis) Create(ctx context.Context, userID int64, UUID strin
existingClient, err := s.clientRepo.FindByClientToken(ctx, clientToken)
if err != nil {
// Client不存在创建新的
clientUUID := uuid.New().String()
clientUUID, err := utils.RandomHex(16) // 16字节 = 32字符十六进制
if err != nil {
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("生成ClientUUID失败: %w", err)
}
client = &model.Client{
UUID: clientUUID,
ClientToken: clientToken,
@@ -173,6 +180,11 @@ func (s *tokenServiceRedis) Create(ctx context.Context, userID int64, UUID strin
return selectedProfileID, availableProfiles, accessToken, clientToken, nil
}
// CreateWithProfile 创建Token并绑定指定Profile使用JWT + Redis存储
func (s *tokenServiceRedis) CreateWithProfile(ctx context.Context, userID int64, profileUUID string, clientToken string) (*model.Profile, []*model.Profile, string, string, error) {
return s.Create(ctx, userID, profileUUID, clientToken)
}
// Validate 验证Token使用JWT验证 + Redis存储验证
func (s *tokenServiceRedis) Validate(ctx context.Context, accessToken, clientToken string) bool {
// 设置超时上下文

View File

@@ -14,6 +14,8 @@ import (
type SerializationService interface {
// SerializeProfile 序列化档案为Yggdrasil格式
SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{}
// SerializeProfileWithUnsigned 序列化档案为Yggdrasil格式支持unsigned参数
SerializeProfileWithUnsigned(ctx context.Context, profile model.Profile, unsigned bool) map[string]interface{}
// SerializeUser 序列化用户为Yggdrasil格式
SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{}
}
@@ -45,8 +47,13 @@ func NewSerializationService(
}
}
// SerializeProfile 序列化档案为Yggdrasil格式
// SerializeProfile 序列化档案为Yggdrasil格式(默认返回签名)
func (s *yggdrasilSerializationService) SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{} {
return s.SerializeProfileWithUnsigned(ctx, profile, false)
}
// SerializeProfileWithUnsigned 序列化档案为Yggdrasil格式支持unsigned参数
func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context.Context, profile model.Profile, unsigned bool) map[string]interface{} {
// 创建基本材质数据
texturesMap := make(map[string]interface{})
textures := map[string]interface{}{
@@ -99,26 +106,36 @@ func (s *yggdrasilSerializationService) SerializeProfile(ctx context.Context, pr
}
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
// 只有在 unsigned=false 时才签名
var signature string
if !unsigned {
signature, err = s.signatureService.SignStringWithSHA1withRSA(textureData)
if err != nil {
s.logger.Error("签名textures失败",
zap.Error(err),
zap.String("profileUUID", profile.UUID),
)
return nil
}
}
// 构建属性
property := Property{
Name: "textures",
Value: textureData,
}
// 只有在 unsigned=false 时才添加签名
if !unsigned {
property.Signature = signature
}
// 构建结果
data := map[string]interface{}{
"id": profile.UUID,
"name": profile.Name,
"properties": []Property{
{
Name: "textures",
Value: textureData,
Signature: signature,
},
},
"id": profile.UUID,
"name": profile.Name,
"properties": []Property{property},
}
return data
}

View File

@@ -85,8 +85,8 @@ func (s *yggdrasilServiceComposite) JoinServer(ctx context.Context, serverID, ac
return fmt.Errorf("验证Token失败: %w", err)
}
// 格式化UUID并验证与Token关联的配置文件
formattedProfile := utils.FormatUUID(selectedProfile)
// 确保UUID是32位无符号格式用于向后兼容
formattedProfile := utils.FormatUUIDToNoDash(selectedProfile)
if uuid != formattedProfile {
return errors.New("selectedProfile与Token不匹配")
}
@@ -115,6 +115,11 @@ func (s *yggdrasilServiceComposite) SerializeProfile(ctx context.Context, profil
return s.serializationService.SerializeProfile(ctx, profile)
}
// SerializeProfileWithUnsigned 序列化档案支持unsigned参数
func (s *yggdrasilServiceComposite) SerializeProfileWithUnsigned(ctx context.Context, profile model.Profile, unsigned bool) map[string]interface{} {
return s.serializationService.SerializeProfileWithUnsigned(ctx, profile, unsigned)
}
// SerializeUser 序列化用户
func (s *yggdrasilServiceComposite) SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{} {
return s.serializationService.SerializeUser(ctx, user, uuid)

View File

@@ -57,16 +57,38 @@ func (v *Validator) ValidateEmail(email string) error {
return nil
}
// ValidateUUID 验证UUID格式简单验证
// ValidateUUID 验证UUID格式支持32位无符号和36位带连字符格式
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格式无效")
// 验证32位无符号UUID格式纯十六进制字符串
if len(uuid) == 32 {
// 检查是否为有效的十六进制字符串
for _, c := range uuid {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return errors.New("UUID格式无效包含非十六进制字符")
}
}
return nil
}
return nil
// 验证36位标准UUID格式带连字符
if len(uuid) == 36 && uuid[8] == '-' && uuid[13] == '-' && uuid[18] == '-' && uuid[23] == '-' {
// 检查除连字符外的字符是否为有效的十六进制
for i, c := range uuid {
if i == 8 || i == 13 || i == 18 || i == 23 {
continue // 跳过连字符位置
}
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return errors.New("UUID格式无效包含非十六进制字符")
}
}
return nil
}
return errors.New("UUID格式无效长度应为32位或36位")
}
// ValidateAccessToken 验证访问令牌

View File

@@ -41,6 +41,7 @@ type RegisterRequest struct {
Email string `json:"email" binding:"required,email" example:"user@example.com"`
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
VerificationCode string `json:"verification_code" binding:"required,len=6" example:"123456"` // 邮箱验证码
CaptchaID string `json:"captcha_id" binding:"required" example:"uuid-xxxx-xxxx"` // 滑动验证码ID
Avatar string `json:"avatar" binding:"omitempty,url" example:"https://rustfs.example.com/avatars/user_1/avatar.png"` // 可选,用户自定义头像
}
@@ -154,7 +155,7 @@ type TextureInfo struct {
// ProfileInfo 角色信息
// @Description Minecraft档案信息
type ProfileInfo struct {
UUID string `json:"uuid" example:"550e8400-e29b-41d4-a716-446655440000"`
UUID string `json:"uuid" example:"550e8400e29b41d4a716446655440000"`
UserID int64 `json:"user_id" example:"1"`
Name string `json:"name" example:"PlayerName"`
SkinID *int64 `json:"skin_id,omitempty" example:"1"`

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"sync"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v3"
gormadapter "github.com/casbin/gorm-adapter/v3"
"go.uber.org/zap"
"gorm.io/gorm"

View File

@@ -65,18 +65,21 @@ type DatabaseConfig struct {
// RedisConfig Redis配置
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
Database int `mapstructure:"database"`
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"` // 连接最大空闲时间
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
Database int `mapstructure:"database"`
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"` // 连接最大空闲时间
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生命周期(新增)
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"` // 健康检查间隔(新增)
EnableRetryOnError bool `mapstructure:"enable_retry_on_error"` // 错误时启用重试(新增)
}
// RustFSConfig RustFS对象存储配置 (S3兼容)
@@ -192,18 +195,21 @@ func setDefaults() {
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.port", 6379)
viper.SetDefault("redis.database", 0)
viper.SetDefault("redis.pool_size", 10)
viper.SetDefault("redis.min_idle_conns", 5)
viper.SetDefault("redis.pool_size", 16) // 优化:提高默认连接池大小
viper.SetDefault("redis.min_idle_conns", 8) // 优化:提高最小空闲连接数
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")
viper.SetDefault("redis.conn_max_idle_time", "10m") // 优化:减少空闲连接超时时间
viper.SetDefault("redis.conn_max_lifetime", "30m") // 新增:连接最大生命周期
viper.SetDefault("redis.health_check_interval", "30s") // 新增:健康检查间隔
viper.SetDefault("redis.enable_retry_on_error", true) // 新增:错误时启用重试
// RustFS默认配置
viper.SetDefault("rustfs.endpoint", "127.0.0.1:9000")
@@ -281,6 +287,9 @@ func setupEnvMappings() {
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")
viper.BindEnv("redis.conn_max_lifetime", "REDIS_CONN_MAX_LIFETIME")
viper.BindEnv("redis.health_check_interval", "REDIS_HEALTH_CHECK_INTERVAL")
viper.BindEnv("redis.enable_retry_on_error", "REDIS_ENABLE_RETRY_ON_ERROR")
// RustFS配置
viper.BindEnv("rustfs.endpoint", "RUSTFS_ENDPOINT")
@@ -427,6 +436,22 @@ func overrideFromEnv(config *Config) {
}
}
if connMaxLifetime := os.Getenv("REDIS_CONN_MAX_LIFETIME"); connMaxLifetime != "" {
if val, err := time.ParseDuration(connMaxLifetime); err == nil {
config.Redis.ConnMaxLifetime = val
}
}
if healthCheckInterval := os.Getenv("REDIS_HEALTH_CHECK_INTERVAL"); healthCheckInterval != "" {
if val, err := time.ParseDuration(healthCheckInterval); err == nil {
config.Redis.HealthCheckInterval = val
}
}
if enableRetryOnError := os.Getenv("REDIS_ENABLE_RETRY_ON_ERROR"); enableRetryOnError != "" {
config.Redis.EnableRetryOnError = enableRetryOnError == "true" || enableRetryOnError == "1"
}
// 处理邮件配置
if emailEnabled := os.Getenv("EMAIL_ENABLED"); emailEnabled != "" {
config.Email.Enabled = emailEnabled == "true" || emailEnabled == "True" || emailEnabled == "TRUE" || emailEnabled == "1"

View File

@@ -369,11 +369,6 @@ func (b *CacheKeyBuilder) ProfilePattern(userID int64) string {
return fmt.Sprintf("%sprofile:*:%d*", b.prefix, userID)
}
// UserFavoritesPattern 用户收藏相关的所有缓存键模式
func (b *CacheKeyBuilder) UserFavoritesPattern(userID int64) string {
return fmt.Sprintf("%sfavorites:*:%d*", b.prefix, userID)
}
// Exists 检查缓存键是否存在
func (cm *CacheManager) Exists(ctx context.Context, key string) (bool, error) {
if !cm.config.Enabled || cm.redis == nil {

View File

@@ -11,8 +11,8 @@ import (
)
var (
// dbInstance 全局数据库实例
dbInstance *gorm.DB
// dbInstance 全局数据库实例(使用 *DB 封装)
dbInstance *DB
// once 确保只初始化一次
once sync.Once
// initError 初始化错误
@@ -33,7 +33,16 @@ func Init(cfg config.DatabaseConfig, logger *zap.Logger) error {
}
// GetDB 获取数据库实例(线程安全)
// 返回 *gorm.DB 以保持向后兼容
func GetDB() (*gorm.DB, error) {
if dbInstance == nil {
return nil, fmt.Errorf("数据库未初始化,请先调用 database.Init()")
}
return dbInstance.DB, nil
}
// GetDBWrapper 获取数据库封装实例(包含连接池统计功能)
func GetDBWrapper() (*DB, error) {
if dbInstance == nil {
return nil, fmt.Errorf("数据库未初始化,请先调用 database.Init()")
}
@@ -41,6 +50,7 @@ func GetDB() (*gorm.DB, error) {
}
// MustGetDB 获取数据库实例如果未初始化则panic
// 返回 *gorm.DB 以保持向后兼容
func MustGetDB() *gorm.DB {
db, err := GetDB()
if err != nil {
@@ -103,10 +113,5 @@ func Close() error {
return nil
}
sqlDB, err := dbInstance.DB()
if err != nil {
return err
}
return sqlDB.Close()
return dbInstance.Close()
}

View File

@@ -14,8 +14,25 @@ func TestAutoMigrate_WithSQLite(t *testing.T) {
if err != nil {
t.Fatalf("open sqlite err: %v", err)
}
dbInstance = db
defer func() { dbInstance = nil }()
// 创建临时的 *DB 包装器用于测试
// 注意:这里不需要真正的连接池功能,只是测试 AutoMigrate
sqlDB, err := db.DB()
if err != nil {
t.Fatalf("get sql.DB err: %v", err)
}
tempDB := &DB{
DB: db,
sqlDB: sqlDB,
}
// 保存原始实例
originalDB := dbInstance
defer func() { dbInstance = originalDB }()
// 替换为测试实例
dbInstance = tempDB
logger := zaptest.NewLogger(t)
if err := AutoMigrate(logger); err != nil {

View File

@@ -1,9 +1,12 @@
package database
import (
"context"
"database/sql"
"fmt"
"log"
"os"
"sync"
"time"
"carrotskin/pkg/config"
@@ -13,8 +16,31 @@ import (
"gorm.io/gorm/logger"
)
// DBStats 数据库连接池统计信息
type DBStats struct {
MaxOpenConns int // 最大打开连接数
OpenConns int // 当前打开的连接数
InUseConns int // 正在使用的连接数
IdleConns int // 空闲连接数
WaitCount int64 // 等待连接的总次数
WaitDuration time.Duration // 等待连接的总时间
LastPingTime time.Time // 上次探活时间
LastPingSuccess bool // 上次探活是否成功
mu sync.RWMutex // 保护 LastPingTime 和 LastPingSuccess
}
// DB 数据库封装,包含连接池统计
type DB struct {
*gorm.DB
stats *DBStats
sqlDB *sql.DB
healthCh chan struct{} // 健康检查信号通道
closeCh chan struct{} // 关闭信号通道
wg sync.WaitGroup
}
// New 创建新的PostgreSQL数据库连接
func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
func New(cfg config.DatabaseConfig) (*DB, error) {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
cfg.Host,
cfg.Port,
@@ -25,11 +51,11 @@ func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
cfg.Timezone,
)
// 配置慢查询监控
// 配置慢查询监控 - 优化从200ms调整为100ms
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond, // 慢查询阈值:200ms
SlowThreshold: 100 * time.Millisecond, // 慢查询阈值:100ms(优化后)
LogLevel: logger.Warn, // 只记录警告和错误
IgnoreRecordNotFoundError: true, // 忽略记录未找到错误
Colorful: false, // 生产环境禁用彩色
@@ -79,12 +105,131 @@ func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
sqlDB.SetConnMaxLifetime(connMaxLifetime)
sqlDB.SetConnMaxIdleTime(connMaxIdleTime)
// 测试连接
if err := sqlDB.Ping(); err != nil {
// 测试连接(带重试机制)
if err := pingWithRetry(sqlDB, 3, 2*time.Second); err != nil {
return nil, fmt.Errorf("数据库连接测试失败: %w", err)
}
return db, nil
// 创建数据库封装
database := &DB{
DB: db,
sqlDB: sqlDB,
stats: &DBStats{},
healthCh: make(chan struct{}, 1),
closeCh: make(chan struct{}),
}
// 初始化统计信息
database.updateStats()
// 启动定期健康检查
database.startHealthCheck(30 * time.Second)
log.Println("[Database] PostgreSQL连接池初始化成功")
log.Printf("[Database] 连接池配置: MaxIdleConns=%d, MaxOpenConns=%d, ConnMaxLifetime=%v, ConnMaxIdleTime=%v",
maxIdleConns, maxOpenConns, connMaxLifetime, connMaxIdleTime)
return database, nil
}
// pingWithRetry 带重试的Ping操作
func pingWithRetry(sqlDB *sql.DB, maxRetries int, retryInterval time.Duration) error {
var err error
for i := 0; i < maxRetries; i++ {
if err = sqlDB.Ping(); err == nil {
return nil
}
if i < maxRetries-1 {
log.Printf("[Database] Ping失败%v 后重试 (%d/%d): %v", retryInterval, i+1, maxRetries, err)
time.Sleep(retryInterval)
}
}
return err
}
// startHealthCheck 启动定期健康检查
func (d *DB) startHealthCheck(interval time.Duration) {
d.wg.Add(1)
go func() {
defer d.wg.Done()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
d.ping()
case <-d.healthCh:
d.ping()
case <-d.closeCh:
return
}
}
}()
}
// ping 执行连接健康检查
func (d *DB) ping() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := d.sqlDB.PingContext(ctx)
d.stats.mu.Lock()
d.stats.LastPingTime = time.Now()
d.stats.LastPingSuccess = err == nil
d.stats.mu.Unlock()
if err != nil {
log.Printf("[Database] 连接健康检查失败: %v", err)
} else {
log.Println("[Database] 连接健康检查成功")
}
}
// GetStats 获取连接池统计信息
func (d *DB) GetStats() DBStats {
d.stats.mu.RLock()
defer d.stats.mu.RUnlock()
// 从底层获取实时统计
stats := d.sqlDB.Stats()
d.stats.MaxOpenConns = stats.MaxOpenConnections
d.stats.OpenConns = stats.OpenConnections
d.stats.InUseConns = stats.InUse
d.stats.IdleConns = stats.Idle
d.stats.WaitCount = stats.WaitCount
d.stats.WaitDuration = stats.WaitDuration
return *d.stats
}
// updateStats 初始化统计信息
func (d *DB) updateStats() {
stats := d.sqlDB.Stats()
d.stats.MaxOpenConns = stats.MaxOpenConnections
d.stats.OpenConns = stats.OpenConnections
d.stats.InUseConns = stats.InUse
d.stats.IdleConns = stats.Idle
}
// LogStats 记录连接池状态日志
func (d *DB) LogStats() {
stats := d.GetStats()
log.Printf("[Database] 连接池状态: Open=%d, Idle=%d, InUse=%d, WaitCount=%d, WaitDuration=%v, LastPing=%v (%v)",
stats.OpenConns, stats.IdleConns, stats.InUseConns, stats.WaitCount, stats.WaitDuration,
stats.LastPingTime.Format("2006-01-02 15:04:05"), stats.LastPingSuccess)
}
// Close 关闭数据库连接
func (d *DB) Close() error {
close(d.closeCh)
d.wg.Wait()
return d.sqlDB.Close()
}
// WithTimeout 创建带有超时控制的上下文
func WithTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(parent, timeout)
}
// GetDSN 获取数据源名称
@@ -99,9 +244,3 @@ func GetDSN(cfg config.DatabaseConfig) string {
cfg.Timezone,
)
}

View File

@@ -3,13 +3,11 @@ package email
import (
"crypto/tls"
"fmt"
"net/smtp"
"net/textproto"
"carrotskin/pkg/config"
"github.com/jordan-wright/email"
"go.uber.org/zap"
gomail "gopkg.in/mail.v2"
)
// Service 邮件服务
@@ -36,7 +34,7 @@ func (s *Service) SendVerificationCode(to, code, purpose string) error {
subject := s.getSubject(purpose)
body := s.getBody(code, purpose)
return s.send([]string{to}, subject, body)
return s.send(to, subject, body)
}
// SendResetPassword 发送重置密码邮件
@@ -55,23 +53,13 @@ func (s *Service) SendChangeEmail(to, code string) error {
}
// send 发送邮件
func (s *Service) send(to []string, subject, body string) error {
e := email.NewEmail()
e.From = fmt.Sprintf("%s <%s>", s.cfg.FromName, s.cfg.Username)
e.To = to
e.Subject = subject
e.HTML = []byte(body)
e.Headers = textproto.MIMEHeader{}
func (s *Service) send(to, subject, body string) error {
m := gomail.NewMessage()
m.SetHeader("From", fmt.Sprintf("%s <%s>", s.cfg.FromName, s.cfg.Username))
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/html", body)
// SMTP认证
auth := smtp.PlainAuth("", s.cfg.Username, s.cfg.Password, s.cfg.SMTPHost)
// 发送邮件
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, s.cfg.SMTPPort)
// 判断端口决定发送方式
// 465端口使用SSL/TLS隐式TLS
// 587端口使用STARTTLS显式TLS
var err error
if s.cfg.SMTPPort == 465 {
// 使用SSL/TLS连接适用于465端口
@@ -79,15 +67,28 @@ func (s *Service) send(to []string, subject, body string) error {
ServerName: s.cfg.SMTPHost,
InsecureSkipVerify: false, // 生产环境建议设置为false
}
err = e.SendWithTLS(addr, auth, tlsConfig)
d := &gomail.Dialer{
Host: s.cfg.SMTPHost,
Port: s.cfg.SMTPPort,
Username: s.cfg.Username,
Password: s.cfg.Password,
TLSConfig: tlsConfig,
}
err = d.DialAndSend(m)
} else {
// 使用STARTTLS连接适用于587端口等
err = e.Send(addr, auth)
d := &gomail.Dialer{
Host: s.cfg.SMTPHost,
Port: s.cfg.SMTPPort,
Username: s.cfg.Username,
Password: s.cfg.Password,
}
err = d.DialAndSend(m)
}
if err != nil {
s.logger.Error("发送邮件失败",
zap.Strings("to", to),
zap.String("to", to),
zap.String("subject", subject),
zap.String("smtp_host", s.cfg.SMTPHost),
zap.Int("smtp_port", s.cfg.SMTPPort),
@@ -97,7 +98,7 @@ func (s *Service) send(to []string, subject, body string) error {
}
s.logger.Info("邮件发送成功",
zap.Strings("to", to),
zap.String("to", to),
zap.String("subject", subject),
)

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"sync"
"time"
"carrotskin/pkg/config"
@@ -12,23 +13,39 @@ import (
"go.uber.org/zap"
)
// Client Redis客户端包装
// Client Redis客户端包装(包含连接池统计和健康检查)
type Client struct {
*redis.Client
logger *zap.Logger
*redis.Client // 嵌入原始Redis客户端
logger *zap.Logger // 日志记录器
stats *RedisStats // 连接池统计信息
healthCheckDone chan struct{} // 健康检查完成信号
closeCh chan struct{} // 关闭信号通道
wg sync.WaitGroup // 等待组
}
// New 创建Redis客户端
// RedisStats Redis连接池统计信息
type RedisStats struct {
PoolSize int // 连接池大小
IdleConns int // 空闲连接数
ActiveConns int // 活跃连接数
StaleConns int // 过期连接数
TotalConns int // 总连接数
LastPingTime time.Time // 上次探活时间
LastPingSuccess bool // 上次探活是否成功
mu sync.RWMutex // 保护统计信息
}
// New 创建Redis客户端带健康检查和优化配置
func New(cfg config.RedisConfig, logger *zap.Logger) (*Client, error) {
// 设置默认值
poolSize := cfg.PoolSize
if poolSize <= 0 {
poolSize = 10
poolSize = 16 // 优化:提高默认连接池大小
}
minIdleConns := cfg.MinIdleConns
if minIdleConns <= 0 {
minIdleConns = 5
minIdleConns = 8 // 优化:提高最小空闲连接数
}
maxRetries := cfg.MaxRetries
@@ -58,10 +75,15 @@ func New(cfg config.RedisConfig, logger *zap.Logger) (*Client, error) {
connMaxIdleTime := cfg.ConnMaxIdleTime
if connMaxIdleTime <= 0 {
connMaxIdleTime = 30 * time.Minute
connMaxIdleTime = 10 * time.Minute // 优化:减少空闲连接超时
}
// 创建Redis客户端
connMaxLifetime := cfg.ConnMaxLifetime
if connMaxLifetime <= 0 {
connMaxLifetime = 30 * time.Minute // 新增:连接最大生命周期
}
// 创建Redis客户端带优化配置
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Password: cfg.Password,
@@ -74,125 +96,254 @@ func New(cfg config.RedisConfig, logger *zap.Logger) (*Client, error) {
WriteTimeout: writeTimeout,
PoolTimeout: poolTimeout,
ConnMaxIdleTime: connMaxIdleTime,
ConnMaxLifetime: connMaxLifetime,
})
// 测试连接
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := rdb.Ping(ctx).Err(); err != nil {
// 测试连接(带重试机制)
if err := pingWithRetry(rdb, 3, 2*time.Second); err != nil {
return nil, fmt.Errorf("Redis连接失败: %w", err)
}
// 创建客户端包装
client := &Client{
Client: rdb,
logger: logger,
stats: &RedisStats{},
healthCheckDone: make(chan struct{}),
closeCh: make(chan struct{}),
}
// 初始化统计信息
client.updateStats()
// 启动定期健康检查
healthCheckInterval := cfg.HealthCheckInterval
if healthCheckInterval <= 0 {
healthCheckInterval = 30 * time.Second
}
client.startHealthCheck(healthCheckInterval)
logger.Info("Redis连接成功",
zap.String("host", cfg.Host),
zap.Int("port", cfg.Port),
zap.Int("database", cfg.Database),
zap.Int("pool_size", poolSize),
zap.Int("min_idle_conns", minIdleConns),
)
return &Client{
Client: rdb,
logger: logger,
}, nil
return client, nil
}
// pingWithRetry 带重试的Ping操作
func pingWithRetry(rdb *redis.Client, maxRetries int, retryInterval time.Duration) error {
var err error
for i := 0; i < maxRetries; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err = rdb.Ping(ctx).Err()
cancel()
if err == nil {
return nil
}
if i < maxRetries-1 {
time.Sleep(retryInterval)
}
}
return err
}
// startHealthCheck 启动定期健康检查
func (c *Client) startHealthCheck(interval time.Duration) {
c.wg.Add(1)
go func() {
defer c.wg.Done()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.doHealthCheck()
case <-c.closeCh:
return
}
}
}()
}
// doHealthCheck 执行健康检查
func (c *Client) doHealthCheck() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 更新统计信息
c.updateStats()
// 执行Ping检查
err := c.Client.Ping(ctx).Err()
c.stats.mu.Lock()
c.stats.LastPingTime = time.Now()
c.stats.LastPingSuccess = err == nil
c.stats.mu.Unlock()
if err != nil {
c.logger.Warn("Redis健康检查失败", zap.Error(err))
} else {
c.logger.Debug("Redis健康检查成功")
}
}
// updateStats 更新连接池统计信息
func (c *Client) updateStats() {
// 获取底层连接池统计信息
stats := c.Client.PoolStats()
c.stats.mu.Lock()
c.stats.PoolSize = c.Client.Options().PoolSize
c.stats.IdleConns = int(stats.IdleConns)
c.stats.ActiveConns = int(stats.TotalConns) - int(stats.IdleConns)
c.stats.TotalConns = int(stats.TotalConns)
c.stats.StaleConns = int(stats.StaleConns)
c.stats.mu.Unlock()
}
// GetStats 获取连接池统计信息
func (c *Client) GetStats() RedisStats {
c.stats.mu.RLock()
defer c.stats.mu.RUnlock()
return RedisStats{
PoolSize: c.stats.PoolSize,
IdleConns: c.stats.IdleConns,
ActiveConns: c.stats.ActiveConns,
StaleConns: c.stats.StaleConns,
TotalConns: c.stats.TotalConns,
LastPingTime: c.stats.LastPingTime,
LastPingSuccess: c.stats.LastPingSuccess,
}
}
// LogStats 记录连接池状态日志
func (c *Client) LogStats() {
stats := c.GetStats()
c.logger.Info("Redis连接池状态",
zap.Int("pool_size", stats.PoolSize),
zap.Int("idle_conns", stats.IdleConns),
zap.Int("active_conns", stats.ActiveConns),
zap.Int("total_conns", stats.TotalConns),
zap.Int("stale_conns", stats.StaleConns),
zap.Bool("last_ping_success", stats.LastPingSuccess),
)
}
// Ping 验证Redis连接带超时控制
func (c *Client) Ping(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return c.Client.Ping(ctx).Err()
}
// Close 关闭Redis连接
func (c *Client) Close() error {
// 停止健康检查
close(c.closeCh)
c.wg.Wait()
c.logger.Info("正在关闭Redis连接")
c.LogStats() // 关闭前记录最终状态
return c.Client.Close()
}
// Set 设置键值对(带过期时间)
// ===== 以下是封装的便捷方法,用于返回 (value, error) 格式 =====
// Set 设置键值对(带过期时间)- 封装版本
func (c *Client) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
return c.Client.Set(ctx, key, value, expiration).Err()
}
// Get 获取键值
// Get 获取键值 - 封装版本
func (c *Client) Get(ctx context.Context, key string) (string, error) {
return c.Client.Get(ctx, key).Result()
}
// Del 删除键
// Del 删除键 - 封装版本
func (c *Client) Del(ctx context.Context, keys ...string) error {
return c.Client.Del(ctx, keys...).Err()
}
// Exists 检查键是否存在
// Exists 检查键是否存在 - 封装版本
func (c *Client) Exists(ctx context.Context, keys ...string) (int64, error) {
return c.Client.Exists(ctx, keys...).Result()
}
// Expire 设置键的过期时间
// Expire 设置键的过期时间 - 封装版本
func (c *Client) Expire(ctx context.Context, key string, expiration time.Duration) error {
return c.Client.Expire(ctx, key, expiration).Err()
}
// TTL 获取键的剩余过期时间
// TTL 获取键的剩余过期时间 - 封装版本
func (c *Client) TTL(ctx context.Context, key string) (time.Duration, error) {
return c.Client.TTL(ctx, key).Result()
}
// Incr 自增
// Incr 自增 - 封装版本
func (c *Client) Incr(ctx context.Context, key string) (int64, error) {
return c.Client.Incr(ctx, key).Result()
}
// Decr 自减
// Decr 自减 - 封装版本
func (c *Client) Decr(ctx context.Context, key string) (int64, error) {
return c.Client.Decr(ctx, key).Result()
}
// HSet 设置哈希字段
// HSet 设置哈希字段 - 封装版本
func (c *Client) HSet(ctx context.Context, key string, values ...interface{}) error {
return c.Client.HSet(ctx, key, values...).Err()
}
// HGet 获取哈希字段
// HGet 获取哈希字段 - 封装版本
func (c *Client) HGet(ctx context.Context, key, field string) (string, error) {
return c.Client.HGet(ctx, key, field).Result()
}
// HGetAll 获取所有哈希字段
// HGetAll 获取所有哈希字段 - 封装版本
func (c *Client) HGetAll(ctx context.Context, key string) (map[string]string, error) {
return c.Client.HGetAll(ctx, key).Result()
}
// HDel 删除哈希字段
// HDel 删除哈希字段 - 封装版本
func (c *Client) HDel(ctx context.Context, key string, fields ...string) error {
return c.Client.HDel(ctx, key, fields...).Err()
}
// SAdd 添加集合成员
// SAdd 添加集合成员 - 封装版本
func (c *Client) SAdd(ctx context.Context, key string, members ...interface{}) error {
return c.Client.SAdd(ctx, key, members...).Err()
}
// SMembers 获取集合所有成员
// SMembers 获取集合所有成员 - 封装版本
func (c *Client) SMembers(ctx context.Context, key string) ([]string, error) {
return c.Client.SMembers(ctx, key).Result()
}
// SRem 删除集合成员
// SRem 删除集合成员 - 封装版本
func (c *Client) SRem(ctx context.Context, key string, members ...interface{}) error {
return c.Client.SRem(ctx, key, members...).Err()
}
// SIsMember 检查是否是集合成员
// SIsMember 检查是否是集合成员 - 封装版本
func (c *Client) SIsMember(ctx context.Context, key string, member interface{}) (bool, error) {
return c.Client.SIsMember(ctx, key, member).Result()
}
// ZAdd 添加有序集合成员
// ZAdd 添加有序集合成员 - 封装版本
func (c *Client) ZAdd(ctx context.Context, key string, members ...redis.Z) error {
return c.Client.ZAdd(ctx, key, members...).Err()
}
// ZRange 获取有序集合范围内的成员
// ZRange 获取有序集合范围内的成员 - 封装版本
func (c *Client) ZRange(ctx context.Context, key string, start, stop int64) ([]string, error) {
return c.Client.ZRange(ctx, key, start, stop).Result()
}
// ZRem 删除有序集合成员
// ZRem 删除有序集合成员 - 封装版本
func (c *Client) ZRem(ctx context.Context, key string, members ...interface{}) error {
return c.Client.ZRem(ctx, key, members...).Err()
}
@@ -207,6 +358,7 @@ func (c *Client) TxPipeline() redis.Pipeliner {
return c.Client.TxPipeline()
}
// Nil 检查错误是否为Nilkey不存在
func (c *Client) Nil(err error) bool {
return errors.Is(err, redis.Nil)
}

View File

@@ -1,8 +1,12 @@
package utils
import (
"go.uber.org/zap"
"crypto/rand"
"encoding/hex"
"strings"
"github.com/google/uuid"
"go.uber.org/zap"
)
// FormatUUID 将UUID格式化为带连字符的标准格式
@@ -45,3 +49,49 @@ func FormatUUID(uuid string) string {
logger.Warn("[WARN] UUID格式无效: ", zap.String("uuid:", uuid))
return uuid
}
// GenerateUUID 生成无符号UUID32位十六进制字符串不带连字符
// 使用github.com/google/uuid库生成标准UUID然后移除连字符
// 返回格式示例: "123e4567e89b12d3a456426614174000"
func GenerateUUID() string {
return strings.ReplaceAll(uuid.New().String(), "-", "")
}
// FormatUUIDToNoDash 将带连字符的UUID转换为无符号格式移除连字符
// 输入: "123e4567-e89b-12d3-a456-426614174000"
// 输出: "123e4567e89b12d3a456426614174000"
// 如果输入已经是32位格式直接返回
// 如果输入格式无效,返回原值并记录警告
func FormatUUIDToNoDash(uuid string) string {
// 如果为空,直接返回
if uuid == "" {
return uuid
}
// 如果已经是32位格式无连字符直接返回
if len(uuid) == 32 {
return uuid
}
// 如果是36位标准格式移除连字符
if len(uuid) == 36 && uuid[8] == '-' && uuid[13] == '-' && uuid[18] == '-' && uuid[23] == '-' {
return strings.ReplaceAll(uuid, "-", "")
}
// 如果格式无效,记录警告并返回原值
var logger *zap.Logger
logger.Warn("[WARN] UUID格式无效无法转换为无符号格式: ", zap.String("uuid:", uuid))
return uuid
}
// RandomHex 生成指定长度的随机十六进制字符串
// 参数 n: 需要生成的十六进制字符数量每个字节生成2个十六进制字符
// 返回: 长度为 2*n 的十六进制字符串
// 示例: RandomHex(16) 返回 "a1b2c3d4e5f67890abcdef1234567890" (32字符)
func RandomHex(n uint) (string, error) {
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}

View File

@@ -241,7 +241,7 @@ curl -X POST http://localhost:8080/api/v1/user/yggdrasil-password/reset \\
-H 'Authorization: Bearer {user_info['jwt_token']}'
# 4. Yggdrasil认证
curl -X POST http://localhost:8080/api/v1/yggdrasil/authserver/authenticate \\
curl -X POST http://localhost:8080/api/yggdrasil/authserver/authenticate \\
-H 'Content-Type: application/json' \\
-d '{{
"username": "{user_info['username']}",