Compare commits

..
1 Commits
Author SHA1 Message Date
labmem c812cfb663 wip(lyra): 保存带选项列表查询的未完成设计
- 添加通用查询选项、分类过滤选项及动态缓存键生成
- 新增分类创建与更新时间字段

当前实现尚未完成,留待后续继续设计:
- 明确通用查询选项、模型过滤条件与缓存策略的边界
- 补齐数据库回源,修正分类返回类型及过滤条件生效问题
- 修正分页偏移计算,增加参数边界校验和稳定排序
- 完善缓存故障降级、空结果缓存及写后失效策略
- 修正缓存键参数编码歧义,补齐查询名和版本前缀
- 补充测试并验证编译与查询行为
2026-10-21 00:11:37 +08:00
3 changed files with 166 additions and 5 deletions
+17
View File
@@ -2,6 +2,7 @@ package cache
import ( import (
"context" "context"
"crypto/md5"
"encoding/json/v2" "encoding/json/v2"
"errors" "errors"
"fmt" "fmt"
@@ -66,3 +67,19 @@ func (c *Cache) Set[T any](ctx context.Context, entry Entry[T]) error {
} }
return nil 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
}
+67 -5
View File
@@ -7,6 +7,7 @@ import (
"lyra/internal/cache" "lyra/internal/cache"
"lyra/internal/pkg/constant" "lyra/internal/pkg/constant"
"lyra/internal/pkg/query"
"lyra/internal/util" "lyra/internal/util"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
@@ -14,22 +15,26 @@ import (
) )
type Category struct { type Category struct {
ID int64 `db:"id"` ID int64 `db:"id"`
Slug string `db:"slug"` Slug string `db:"slug"`
Title string `db:"title"` Title string `db:"title"`
Sort int `db:"sort"` Sort int `db:"sort"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
} }
type CategoryModel interface { type CategoryModel interface {
FindAll(ctx context.Context) ([]*Category, error) FindAll(ctx context.Context) ([]*Category, error)
ListWithOptions(ctx context.Context, queryOpt query.Option, modelOpts ...ModelOption) (resp []*Link, err error)
} }
type defaultCategoryModel struct { type defaultCategoryModel struct {
db *sqlx.DB db *sqlx.DB
table string
cache *cache.Cache cache *cache.Cache
} }
func NewCategoryModel(db *sqlx.DB, cache *cache.Cache) CategoryModel { 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 const categoryListCacheTTL = 5 * time.Minute
@@ -68,3 +73,60 @@ func (m *defaultCategoryModel) FindAll(ctx context.Context) ([]*Category, error)
} }
return rows, nil 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})
}
}
@@ -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
}
}
}