diff --git a/go/projects/lyra/internal/cache/cache.go b/go/projects/lyra/internal/cache/cache.go index ea7459c..bcb90f8 100644 --- a/go/projects/lyra/internal/cache/cache.go +++ b/go/projects/lyra/internal/cache/cache.go @@ -2,6 +2,7 @@ package cache import ( "context" + "crypto/md5" "encoding/json/v2" "errors" "fmt" @@ -66,3 +67,19 @@ func (c *Cache) Set[T any](ctx context.Context, entry Entry[T]) error { } return nil } + +func GenCacheKey(table, sql string, args ...any) (key string, err error) { + prefix := fmt.Sprintf("lyra:cache:%s:", table) + h := md5.New() + // 写入查询语句 + h.Write([]byte(sql)) + // 序列化并写入每个参数 + for _, arg := range args { + jsonData, err := json.Marshal(arg) + if err != nil { + return "", fmt.Errorf("序列化参数失败:%w", err) + } + h.Write(jsonData) + } + return prefix + fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/go/projects/lyra/internal/model/category.go b/go/projects/lyra/internal/model/category.go index c780dd5..5684636 100644 --- a/go/projects/lyra/internal/model/category.go +++ b/go/projects/lyra/internal/model/category.go @@ -7,6 +7,7 @@ import ( "lyra/internal/cache" "lyra/internal/pkg/constant" + "lyra/internal/pkg/query" "lyra/internal/util" sq "github.com/Masterminds/squirrel" @@ -14,22 +15,26 @@ import ( ) type Category struct { - ID int64 `db:"id"` - Slug string `db:"slug"` - Title string `db:"title"` - Sort int `db:"sort"` + ID int64 `db:"id"` + Slug string `db:"slug"` + Title string `db:"title"` + Sort int `db:"sort"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type CategoryModel interface { FindAll(ctx context.Context) ([]*Category, error) + ListWithOptions(ctx context.Context, queryOpt query.Option, modelOpts ...ModelOption) (resp []*Link, err error) } type defaultCategoryModel struct { db *sqlx.DB + table string cache *cache.Cache } func NewCategoryModel(db *sqlx.DB, cache *cache.Cache) CategoryModel { - return &defaultCategoryModel{db: db, cache: cache} + return &defaultCategoryModel{db: db, table: "categories", cache: cache} } const categoryListCacheTTL = 5 * time.Minute @@ -68,3 +73,60 @@ func (m *defaultCategoryModel) FindAll(ctx context.Context) ([]*Category, error) } return rows, nil } + +type ModelOption func(*sq.SelectBuilder) + +func (m *defaultCategoryModel) ListWithOptions(ctx context.Context, queryOpt query.Option, modelOpts ...ModelOption) (resp []*Link, err error) { + // 1.先构建查询语句(还没有deleted字段) + // 2.根据queryOpt的参数,设置分页、排序等 + sqBuilder := sq.Select("id", "slug", "title", "sort"). + From("categories").Limit(uint64(queryOpt.PageSize)). + Offset(uint64(queryOpt.Page)). + PlaceholderFormat(sq.Dollar) + for _, opt := range modelOpts { + opt(new(sqBuilder)) + } + + query, args, err := sqBuilder.ToSql() + if err != nil { + return nil, fmt.Errorf("squirrel 构造SQL出错: %w", err) + } + cacheKey, err := cache.GenCacheKey(m.table, query, args...) + if err != nil { + return nil, err + } + result, hit, err := m.cache.Get[[]*Link](ctx, cacheKey) + if err != nil { + return nil, err + } + if hit { + return result, nil + } + if queryOpt.WithCacheWrite { + if err := m.cache.Set(ctx, cache.Entry[[]*Link]{ + Key: cacheKey, + Value: result, + TTL: queryOpt.CacheTTL, + }); err != nil { + return nil, err + } + } + return result, nil +} + +func WithCategorySlug(slug string) ModelOption { + return func(builder *sq.SelectBuilder) { + *builder = builder.Where(sq.Eq{"categories.slug": slug}) + } +} +func WithCategoryTitle(title string) ModelOption { + return func(builder *sq.SelectBuilder) { + *builder = builder.Where(sq.Eq{"categories.title": title}) + } +} + +func WithCategorySort(sort int) ModelOption { + return func(builder *sq.SelectBuilder) { + *builder = builder.Where(sq.Eq{"categories.sort": sort}) + } +} diff --git a/go/projects/lyra/internal/pkg/query/query.go b/go/projects/lyra/internal/pkg/query/query.go new file mode 100644 index 0000000..b52fabb --- /dev/null +++ b/go/projects/lyra/internal/pkg/query/query.go @@ -0,0 +1,82 @@ +package query + +import "time" + +const ( + defaultPage = 1 + defaultPageSize = 10 + defaultIncludeDeleted = false + defaultWithCacheWrite = false + defaultCacheTTL = 5 * time.Minute + defaultNotFoundCacheTTL = 1 * time.Minute +) + +type Option struct { + Page int + PageSize int + IncludeDeleted bool + WithCacheWrite bool + CacheTTL time.Duration + NotFoundCacheTTL time.Duration +} + +type OptionFunc func(*Option) + +func NewOption(opts ...OptionFunc) *Option { + queryOpntion := &Option{ + Page: defaultPage, + PageSize: defaultPageSize, + IncludeDeleted: defaultIncludeDeleted, + WithCacheWrite: defaultWithCacheWrite, + CacheTTL: defaultCacheTTL, + NotFoundCacheTTL: defaultNotFoundCacheTTL, + } + for _, opt := range opts { + opt(queryOpntion) + } + return queryOpntion +} + +func WithPage(page int) OptionFunc { + return func(o *Option) { + if page > 0 { + o.Page = page + } + } +} + +func WithPageSize(pageSize int) OptionFunc { + return func(o *Option) { + if pageSize > 0 { + o.PageSize = pageSize + } + } +} + +func WithIncludeDeleted(includeDeleted bool) OptionFunc { + return func(o *Option) { + o.IncludeDeleted = includeDeleted + } +} + +func WithCacheWrite(withCacheWrite bool) OptionFunc { + return func(o *Option) { + o.WithCacheWrite = withCacheWrite + } +} + +func WithCacheTTL(cacheTTL time.Duration) OptionFunc { + return func(o *Option) { + if cacheTTL >= 0 { + o.CacheTTL = cacheTTL + } + } +} + +func WithNotFoundCacheTTL(notFoundCacheTTL time.Duration) OptionFunc { + return func(o *Option) { + if notFoundCacheTTL >= 0 { + o.NotFoundCacheTTL = notFoundCacheTTL + } + } +}