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"
|
"errors"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
"github.com/truewhile/MeBox/internal/model"
|
"github.com/truewhile/MeBox/internal/model"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,17 @@ type ReaderRepository struct {
|
|||||||
db *gorm.DB
|
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 排序)。
|
// ListSources 用户的书源列表(书源按用户独立,按 customOrder 排序)。
|
||||||
func (r *ReaderRepository) ListSources(ctx context.Context, userID string) ([]model.ReaderBookSource, error) {
|
func (r *ReaderRepository) ListSources(ctx context.Context, userID string) ([]model.ReaderBookSource, error) {
|
||||||
var out []model.ReaderBookSource
|
var out []model.ReaderBookSource
|
||||||
@@ -153,7 +165,11 @@ func (r *ReaderRepository) SaveSourceState(ctx context.Context, st *model.Reader
|
|||||||
// ListBooks 用户书架(按 order 排序)。
|
// ListBooks 用户书架(按 order 排序)。
|
||||||
func (r *ReaderRepository) ListBooks(ctx context.Context, userID string) ([]model.ReaderBook, error) {
|
func (r *ReaderRepository) ListBooks(ctx context.Context, userID string) ([]model.ReaderBook, error) {
|
||||||
var out []model.ReaderBook
|
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
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +247,10 @@ func (r *ReaderRepository) SwitchBookOrigin(ctx context.Context, book *model.Rea
|
|||||||
// ListChapters 按序取章节。
|
// ListChapters 按序取章节。
|
||||||
func (r *ReaderRepository) ListChapters(ctx context.Context, bookID string) ([]model.ReaderChapter, error) {
|
func (r *ReaderRepository) ListChapters(ctx context.Context, bookID string) ([]model.ReaderChapter, error) {
|
||||||
var out []model.ReaderChapter
|
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
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +452,11 @@ func (r *ReaderRepository) SaveBookGroups(ctx context.Context, row *model.Reader
|
|||||||
// ListReplaceRules 用户替换规则(按 order 排序)。
|
// ListReplaceRules 用户替换规则(按 order 排序)。
|
||||||
func (r *ReaderRepository) ListReplaceRules(ctx context.Context, userID string) ([]model.ReaderReplaceRule, error) {
|
func (r *ReaderRepository) ListReplaceRules(ctx context.Context, userID string) ([]model.ReaderReplaceRule, error) {
|
||||||
var out []model.ReaderReplaceRule
|
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
|
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