diff --git a/go/projects/lyra/cmd/lyra/dependency.go b/go/projects/lyra/cmd/lyra/dependency.go index 6d5e05c..9bad222 100644 --- a/go/projects/lyra/cmd/lyra/dependency.go +++ b/go/projects/lyra/cmd/lyra/dependency.go @@ -1,29 +1,34 @@ package main import ( + "lyra/internal/cache" "lyra/internal/config" "lyra/internal/handler" "lyra/internal/model" "lyra/internal/router" + "time" "github.com/jmoiron/sqlx" + "github.com/redis/go-redis/v9" ) type Dependencies struct { Config *config.Config + Cache *cache.Cache CategoryModel model.CategoryModel } // buildHandlers 集中组装各路由的处理器,入口启动时调用一次。 func buildHandlers(dpd *Dependencies) router.Handlers { return router.Handlers{ - TestHandler: handler.NewTestHandler(dpd.CategoryModel).Hello(), + TestHandler: handler.NewTestHandler(dpd.CategoryModel, dpd.Cache).Hello(), } } -func buildDependencies(cfg *config.Config, db *sqlx.DB) *Dependencies { +func buildDependencies(cfg *config.Config, db *sqlx.DB, cacheClient *redis.Client) *Dependencies { return &Dependencies{ Config: cfg, + Cache: cache.NewCache(cacheClient, time.Duration(cfg.Cache.Timeout)*time.Millisecond), CategoryModel: model.NewCategoryModel(db), } } diff --git a/go/projects/lyra/cmd/lyra/main.go b/go/projects/lyra/cmd/lyra/main.go index a32c01d..7d16383 100644 --- a/go/projects/lyra/cmd/lyra/main.go +++ b/go/projects/lyra/cmd/lyra/main.go @@ -7,10 +7,13 @@ import ( "log" "lyra/internal/config" "lyra/internal/router" + "net" + "strconv" "github.com/gin-gonic/gin" "github.com/jmoiron/sqlx" "github.com/joho/godotenv" + "github.com/redis/go-redis/v9" // 注册 pgx 驱动,供 sqlx 以 "pgx" 驱动名使用。 _ "github.com/jackc/pgx/v5/stdlib" @@ -41,7 +44,12 @@ func main() { } defer db.Close() - deps := buildDependencies(cfg, db) + cache := redis.NewClient(&redis.Options{ + Addr: net.JoinHostPort(cfg.Cache.Host, strconv.Itoa(cfg.Cache.Port)), + DB: cfg.Cache.DB, + }) + defer cache.Close() + deps := buildDependencies(cfg, db, cache) handlers := buildHandlers(deps) router.RegisterRoutes(ginEngine, handlers) addr := fmt.Sprintf("%s:%d", cfg.HTTP.Host, cfg.HTTP.Port) diff --git a/go/projects/lyra/etc/lyra.toml b/go/projects/lyra/etc/lyra.toml index 0e71a8d..342dbc0 100644 --- a/go/projects/lyra/etc/lyra.toml +++ b/go/projects/lyra/etc/lyra.toml @@ -13,6 +13,8 @@ port = 5432 user = "admin" name = "lab" -[redis] +[cache] host = "localhost" -port = 6379 \ No newline at end of file +port = 6379 +db = 0 +timeout = 200 \ No newline at end of file diff --git a/go/projects/lyra/go.mod b/go/projects/lyra/go.mod index 2485f58..c0f0b46 100644 --- a/go/projects/lyra/go.mod +++ b/go/projects/lyra/go.mod @@ -3,17 +3,20 @@ module lyra go 1.27.1 require ( + github.com/Masterminds/squirrel v1.5.4 github.com/gin-gonic/gin v1.12.0 github.com/jackc/pgx/v5 v5.11.0 github.com/jmoiron/sqlx v1.4.0 + github.com/joho/godotenv v1.5.1 + github.com/redis/go-redis/v9 v9.23.0 github.com/spf13/viper v1.21.0 ) require ( - github.com/Masterminds/squirrel v1.5.4 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect @@ -27,9 +30,8 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -48,12 +50,13 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.uber.org/atomic v1.12.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/sys v0.48.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go/projects/lyra/go.sum b/go/projects/lyra/go.sum index 1a71135..672d9d9 100644 --- a/go/projects/lyra/go.sum +++ b/go/projects/lyra/go.sum @@ -2,12 +2,18 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +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.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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -56,8 +62,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -81,12 +87,13 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/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.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.23.0 h1:/TTVdJa8BRNsybwwGGnmiYktHNlUXtbOQxlWpHBPL3s= +github.com/redis/go-redis/v9 v9.23.0/go.mod h1:EWP2UUk+XDaI1s5nQB5mablZN6vccXlmBQTyfnF87D0= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= @@ -112,20 +119,24 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 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= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE= +go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= @@ -135,15 +146,12 @@ golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= diff --git a/go/projects/lyra/internal/cache/cache.go b/go/projects/lyra/internal/cache/cache.go new file mode 100644 index 0000000..ea7459c --- /dev/null +++ b/go/projects/lyra/internal/cache/cache.go @@ -0,0 +1,68 @@ +package cache + +import ( + "context" + "encoding/json/v2" + "errors" + "fmt" + "time" + + "github.com/redis/go-redis/v9" +) + +type Entry[T any] struct { + Key string + Value T + TTL time.Duration +} + +type Cache struct { + client *redis.Client + operationTimeout time.Duration +} + +func NewCache(client *redis.Client, timeout time.Duration) *Cache { + return &Cache{ + client: client, + operationTimeout: timeout, + } +} + +func (c *Cache) Get[T any](ctx context.Context, key string) (T, bool, error) { + var zero T + operationCtx, cancel := context.WithTimeout(ctx, c.operationTimeout) + defer cancel() + + payload, err := c.client.Get(operationCtx, key).Result() + cancel() + if errors.Is(err, redis.Nil) { + return zero, false, nil + } + if err != nil { + return zero, false, fmt.Errorf("读取缓存失败:%w", err) + } + var value T + if err := json.Unmarshal([]byte(payload), &value); err != nil { + return zero, false, fmt.Errorf("缓存解码失败:%w", err) + } + return value, true, nil +} + +func (c *Cache) Set[T any](ctx context.Context, entry Entry[T]) error { + if entry.TTL < 0 { + return fmt.Errorf("缓存过期时间必须大于等于零: %v", entry.TTL) + } + if err := ctx.Err(); err != nil { + return fmt.Errorf("写入缓存失败:%w", err) + } + payload, err := json.Marshal(entry.Value) + if err != nil { + return fmt.Errorf("缓存编码失败:%w", err) + } + operationCtx, cancel := context.WithTimeout(ctx, c.operationTimeout) + defer cancel() + if err := c.client.Set(operationCtx, entry.Key, payload, entry.TTL).Err(); err != nil { + return fmt.Errorf("写入缓存失败:%w", err) + } + return nil +} diff --git a/go/projects/lyra/internal/config/config.go b/go/projects/lyra/internal/config/config.go index 5f199e5..be90f7a 100644 --- a/go/projects/lyra/internal/config/config.go +++ b/go/projects/lyra/internal/config/config.go @@ -10,7 +10,7 @@ type Config struct { App AppConfig `mapstructure:"app"` HTTP HTTPConfig `mapstructure:"http"` Database DatabaseConfig `mapstructure:"database"` - Redis RedisConfig `mapstructure:"redis"` + Cache CacheConfig `mapstructure:"cache"` } type AppConfig struct { @@ -32,9 +32,11 @@ type DatabaseConfig struct { Password string `mapstructure:"password"` } -type RedisConfig struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` +type CacheConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + DB int `mapstructure:"db"` + Timeout int `mapstructure:"timeout"` } func Load(path string) (*Config, error) { diff --git a/go/projects/lyra/internal/handler/test.go b/go/projects/lyra/internal/handler/test.go index f52828c..33b2bcb 100644 --- a/go/projects/lyra/internal/handler/test.go +++ b/go/projects/lyra/internal/handler/test.go @@ -1,30 +1,56 @@ package handler import ( + "fmt" + "log/slog" "lyra/internal/api" + "lyra/internal/cache" "lyra/internal/httpx" "lyra/internal/model" "net/http" + "time" + "github.com/gin-gonic/gin" ) type TestHandler struct { categoryModel model.CategoryModel + cache *cache.Cache } -func NewTestHandler(categoryModel model.CategoryModel) *TestHandler { - return &TestHandler{categoryModel: categoryModel} +func NewTestHandler(categoryModel model.CategoryModel, cache *cache.Cache) *TestHandler { + return &TestHandler{categoryModel: categoryModel, cache: cache} } func (h *TestHandler) Hello() gin.HandlerFunc { - return func(c *gin.Context) { - categories, err := h.categoryModel.FindAll(c.Request.Context()) + return func(ctx *gin.Context) { + categories, err := h.categoryModel.FindAll(ctx.Request.Context()) if err != nil { - httpx.Fail(c, http.StatusInternalServerError, api.CodeInternalError, "获取分类列表失败") + httpx.Fail(ctx, http.StatusInternalServerError, api.CodeInternalError, "获取分类列表失败") return } - httpx.OK(c, toCategoryResponses(categories)) + if err := h.cache.Set(ctx.Request.Context(), cache.Entry[[]*model.Category]{ + Key: "test:categories", + Value: categories, + TTL: 5 * time.Minute, + }); err != nil { + slog.ErrorContext(ctx.Request.Context(), "缓存分类列表失败") + return + } + testResult, _, err := h.cache.Get[[]*model.Category](ctx.Request.Context(), "test:categories") + if err != nil { + slog.ErrorContext(ctx.Request.Context(), "获取缓存分类列表失败") + } + if len(testResult) == 0 { + slog.ErrorContext(ctx.Request.Context(), "缓存分类列表为空") + } else { + for _, v := range testResult { + fmt.Printf("%+v\n", v) + } + } + + httpx.OK(ctx, toCategoryResponses(categories)) } } diff --git a/go/projects/lyra/internal/handler/test_test.go b/go/projects/lyra/internal/handler/test_test.go deleted file mode 100644 index dc9bbd0..0000000 --- a/go/projects/lyra/internal/handler/test_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package handler_test - -import ( - "context" - "errors" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "lyra/internal/handler" - "lyra/internal/model" - - "github.com/gin-gonic/gin" -) - -type fakeCategoryModel struct { - categories []*model.Category - err error - calls int - gotCtx context.Context -} - -func (f *fakeCategoryModel) FindAll(ctx context.Context) ([]*model.Category, error) { - f.calls++ - f.gotCtx = ctx - return f.categories, f.err -} - -func serveHello(t *testing.T, fake *fakeCategoryModel, req *http.Request) *httptest.ResponseRecorder { - t.Helper() - gin.SetMode(gin.TestMode) - engine := gin.New() - engine.GET("/test", handler.NewTestHandler(fake).Hello()) - recorder := httptest.NewRecorder() - engine.ServeHTTP(recorder, req) - return recorder -} - -func newHelloRequest() *http.Request { - return httptest.NewRequest(http.MethodGet, "/test", nil) -} - -func TestHelloMapsCategoriesToResponse(t *testing.T) { - fake := &fakeCategoryModel{categories: []*model.Category{ - {ID: 1, Slug: "go-basics", Title: "Go 基础", Sort: 1}, - {ID: 2, Slug: "sql-practice", Title: "SQL 练习", Sort: 2}, - }} - - recorder := serveHello(t, fake, newHelloRequest()) - - if fake.calls != 1 { - t.Fatalf("模型调用次数 = %d, want 1", fake.calls) - } - if recorder.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK) - } - want := `{"success":true,"data":[{"id":1,"slug":"go-basics","title":"Go 基础","sort":1},{"id":2,"slug":"sql-practice","title":"SQL 练习","sort":2}]}` - if got := recorder.Body.String(); got != want { - t.Fatalf("body = %q, want %q", got, want) - } -} - -func TestHelloReturnsEmptyArrayForNoCategories(t *testing.T) { - tests := []struct { - name string - categories []*model.Category - }{ - {name: "空切片", categories: []*model.Category{}}, - {name: "nil结果", categories: nil}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fake := &fakeCategoryModel{categories: tt.categories} - - recorder := serveHello(t, fake, newHelloRequest()) - - if fake.calls != 1 { - t.Fatalf("模型调用次数 = %d, want 1", fake.calls) - } - if recorder.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK) - } - want := `{"success":true,"data":[]}` - if got := recorder.Body.String(); got != want { - t.Fatalf("body = %q, want %q", got, want) - } - }) - } -} - -func TestHelloReturnsInternalErrorOnModelFailure(t *testing.T) { - modelErr := errors.New("数据库连接被拒绝: 10.0.0.8:5432") - fake := &fakeCategoryModel{err: modelErr} - - recorder := serveHello(t, fake, newHelloRequest()) - - if fake.calls != 1 { - t.Fatalf("模型调用次数 = %d, want 1", fake.calls) - } - if recorder.Code != http.StatusInternalServerError { - t.Fatalf("status = %d, want %d", recorder.Code, http.StatusInternalServerError) - } - want := `{"success":false,"error":{"code":10002,"message":"获取分类列表失败"}}` - if got := recorder.Body.String(); got != want { - t.Fatalf("body = %q, want %q", got, want) - } - if strings.Contains(recorder.Body.String(), modelErr.Error()) { - t.Fatalf("响应泄漏了内部错误信息: %q", recorder.Body.String()) - } -} - -type contextKey struct{} - -func TestHelloPassesRequestContextToModel(t *testing.T) { - fake := &fakeCategoryModel{categories: []*model.Category{}} - reqCtx := context.WithValue(context.Background(), contextKey{}, "req-42") - req := newHelloRequest().WithContext(reqCtx) - - serveHello(t, fake, req) - - if fake.gotCtx != reqCtx { - t.Fatalf("模型收到非请求 context: %v", fake.gotCtx) - } - if got, _ := fake.gotCtx.Value(contextKey{}).(string); got != "req-42" { - t.Fatalf("context 值 = %q, want %q", got, "req-42") - } -}