feat(lyra)!: 接入 Redis 并添加缓存连通性实验
- 封装缓存读写、JSON 编解码、过期时间与操作超时 - 集中组装 Redis 依赖,在测试接口验证分类缓存读写 - 更新依赖并移除原 handler 测试 BREAKING CHANGE: 配置节由 redis 改为 cache,新增 db 和 timeout 配置项
This commit is contained in:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -13,6 +13,8 @@ port = 5432
|
||||
user = "admin"
|
||||
name = "lab"
|
||||
|
||||
[redis]
|
||||
[cache]
|
||||
host = "localhost"
|
||||
port = 6379
|
||||
db = 0
|
||||
timeout = 200
|
||||
@@ -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
|
||||
)
|
||||
|
||||
+20
-12
@@ -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=
|
||||
|
||||
+68
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user