mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-12 16:26:36 +08:00
优化
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/truewhile/MeBox/internal/model"
|
||||
)
|
||||
@@ -15,6 +16,17 @@ type ReaderRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// readerOrderBy 组装方言安全的 ORDER BY。
|
||||
//
|
||||
// 用 clause.Column 让 GORM 按当前方言给列名加引号(SQLite 反引号 / PostgreSQL 双引号),
|
||||
// 而不是把引号硬编码进排序字符串。reader_books.order、reader_chapters.index、
|
||||
// reader_replace_rules.order 都撞上了 SQL 关键字(order 在 PostgreSQL 里是保留字):
|
||||
// 之前写成 .Order("`order` ASC"),SQLite 下正常,PostgreSQL 下反引号被词法分析成
|
||||
// 运算符,直接报 `syntax error at or near "order"`,书架 / 目录 / 替换规则整页 500。
|
||||
func readerOrderBy(columns ...clause.OrderByColumn) clause.OrderBy {
|
||||
return clause.OrderBy{Columns: columns}
|
||||
}
|
||||
|
||||
// ListSources 用户的书源列表(书源按用户独立,按 customOrder 排序)。
|
||||
func (r *ReaderRepository) ListSources(ctx context.Context, userID string) ([]model.ReaderBookSource, error) {
|
||||
var out []model.ReaderBookSource
|
||||
@@ -153,7 +165,11 @@ func (r *ReaderRepository) SaveSourceState(ctx context.Context, st *model.Reader
|
||||
// ListBooks 用户书架(按 order 排序)。
|
||||
func (r *ReaderRepository) ListBooks(ctx context.Context, userID string) ([]model.ReaderBook, error) {
|
||||
var out []model.ReaderBook
|
||||
err := r.db.WithContext(ctx).Where("user_id = ?", userID).Order("`order` ASC, updated_at DESC").Find(&out).Error
|
||||
err := r.db.WithContext(ctx).Where("user_id = ?", userID).
|
||||
Order(readerOrderBy(
|
||||
clause.OrderByColumn{Column: clause.Column{Name: "order"}},
|
||||
clause.OrderByColumn{Column: clause.Column{Name: "updated_at"}, Desc: true},
|
||||
)).Find(&out).Error
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -231,7 +247,10 @@ func (r *ReaderRepository) SwitchBookOrigin(ctx context.Context, book *model.Rea
|
||||
// ListChapters 按序取章节。
|
||||
func (r *ReaderRepository) ListChapters(ctx context.Context, bookID string) ([]model.ReaderChapter, error) {
|
||||
var out []model.ReaderChapter
|
||||
err := r.db.WithContext(ctx).Where("book_id = ?", bookID).Order("`index` ASC").Find(&out).Error
|
||||
err := r.db.WithContext(ctx).Where("book_id = ?", bookID).
|
||||
Order(readerOrderBy(
|
||||
clause.OrderByColumn{Column: clause.Column{Name: "index"}},
|
||||
)).Find(&out).Error
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -433,7 +452,11 @@ func (r *ReaderRepository) SaveBookGroups(ctx context.Context, row *model.Reader
|
||||
// ListReplaceRules 用户替换规则(按 order 排序)。
|
||||
func (r *ReaderRepository) ListReplaceRules(ctx context.Context, userID string) ([]model.ReaderReplaceRule, error) {
|
||||
var out []model.ReaderReplaceRule
|
||||
err := r.db.WithContext(ctx).Where("user_id = ?", userID).Order("`order` DESC, created_at ASC").Find(&out).Error
|
||||
err := r.db.WithContext(ctx).Where("user_id = ?", userID).
|
||||
Order(readerOrderBy(
|
||||
clause.OrderByColumn{Column: clause.Column{Name: "order"}, Desc: true},
|
||||
clause.OrderByColumn{Column: clause.Column{Name: "created_at"}},
|
||||
)).Find(&out).Error
|
||||
return out, err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
gormlogger "gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// sqlCaptureLogger 只记录 GORM 生成的 SQL,不打印任何日志。
|
||||
type sqlCaptureLogger struct {
|
||||
mu sync.Mutex
|
||||
lines []string
|
||||
}
|
||||
|
||||
func (l *sqlCaptureLogger) LogMode(gormlogger.LogLevel) gormlogger.Interface { return l }
|
||||
func (l *sqlCaptureLogger) Info(context.Context, string, ...interface{}) {}
|
||||
func (l *sqlCaptureLogger) Warn(context.Context, string, ...interface{}) {}
|
||||
func (l *sqlCaptureLogger) Error(context.Context, string, ...interface{}) {}
|
||||
|
||||
func (l *sqlCaptureLogger) Trace(_ context.Context, _ time.Time, fc func() (string, int64), _ error) {
|
||||
sql, _ := fc()
|
||||
l.mu.Lock()
|
||||
l.lines = append(l.lines, sql)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *sqlCaptureLogger) all() string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return strings.Join(l.lines, "\n")
|
||||
}
|
||||
|
||||
// newPostgresDryRunRepos 返回使用 PostgreSQL 方言的 repository 容器:
|
||||
// DryRun 只构建 SQL 不执行,DisableAutomaticPing 让它无需真实数据库也能离线运行,
|
||||
// 于是可以直接断言生成语句在 PostgreSQL 下是否合法。
|
||||
func newPostgresDryRunRepos(t *testing.T) (*Container, *sqlCaptureLogger) {
|
||||
t.Helper()
|
||||
rec := &sqlCaptureLogger{}
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{
|
||||
DSN: "postgres://mebox:mebox@127.0.0.1:5432/mebox?sslmode=disable",
|
||||
}), &gorm.Config{
|
||||
DryRun: true,
|
||||
DisableAutomaticPing: true,
|
||||
Logger: rec,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open dry-run postgres: %v", err)
|
||||
}
|
||||
return New(db), rec
|
||||
}
|
||||
|
||||
// TestReaderListsUseDialectQuotedReservedColumns 守住阅读子系统列表查询在
|
||||
// PostgreSQL 下的合法性。reader_books.order / reader_chapters.index /
|
||||
// reader_replace_rules.order 都是关键字级列名(order 在 PostgreSQL 里是保留字)。
|
||||
//
|
||||
// 排序条件曾经写成硬编码反引号的原始字符串,例如 .Order("`order` ASC"):
|
||||
// SQLite 正常,PostgreSQL 会把反引号词法分析成运算符,直接报
|
||||
// `syntax error at or near "order"` (SQLSTATE 42601),书架 / 目录 / 替换规则整页 500。
|
||||
// 现在改用 clause.Column,由 GORM 按方言加引号(SQLite 反引号 / PostgreSQL 双引号)。
|
||||
func TestReaderListsUseDialectQuotedReservedColumns(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cases := []struct {
|
||||
name string
|
||||
run func(repos *Container) error
|
||||
wants []string
|
||||
}{
|
||||
{
|
||||
name: "books",
|
||||
run: func(repos *Container) error { _, err := repos.Reader.ListBooks(ctx, "user-1"); return err },
|
||||
wants: []string{`"order"`, `"updated_at" DESC`},
|
||||
},
|
||||
{
|
||||
name: "chapters",
|
||||
run: func(repos *Container) error { _, err := repos.Reader.ListChapters(ctx, "book-1"); return err },
|
||||
wants: []string{`"index"`},
|
||||
},
|
||||
{
|
||||
name: "replace rules",
|
||||
run: func(repos *Container) error { _, err := repos.Reader.ListReplaceRules(ctx, "user-1"); return err },
|
||||
wants: []string{`"order" DESC`, `"created_at"`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
repos, rec := newPostgresDryRunRepos(t)
|
||||
if err := tc.run(repos); err != nil {
|
||||
t.Fatalf("build query: %v", err)
|
||||
}
|
||||
sql := rec.all()
|
||||
if strings.TrimSpace(sql) == "" {
|
||||
t.Fatal("no SQL captured from the repository call")
|
||||
}
|
||||
if strings.Contains(sql, "`") {
|
||||
t.Fatalf("SQL uses backtick quoting, which PostgreSQL rejects:\n%s", sql)
|
||||
}
|
||||
if !strings.Contains(sql, "ORDER BY") {
|
||||
t.Fatalf("SQL has no ORDER BY clause:\n%s", sql)
|
||||
}
|
||||
for _, want := range tc.wants {
|
||||
if !strings.Contains(sql, want) {
|
||||
t.Fatalf("SQL missing %q:\n%s", want, sql)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user