This commit is contained in:
truewhile
2026-09-30 19:22:24 +08:00
parent fa41936095
commit c1ff55ffbf
28 changed files with 3793 additions and 201 deletions
+39 -2
View File
@@ -3,6 +3,7 @@ package repository
import (
"context"
"errors"
"gorm.io/gorm"
@@ -50,9 +51,45 @@ func (r *ReaderRepository) UpdateSource(ctx context.Context, src *model.ReaderBo
return r.db.WithContext(ctx).Save(src).Error
}
// DeleteSource 删除书源。
// DeleteSource 删除书源(连带清理其会话状态)。
func (r *ReaderRepository) DeleteSource(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&model.ReaderBookSource{}, "id = ?", id).Error
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
src := &model.ReaderBookSource{}
if err := tx.First(src, "id = ?", id).Error; err == nil && src.SourceURL != "" {
if err := tx.Delete(&model.ReaderSourceState{}, "source_url = ?", src.SourceURL).Error; err != nil {
return err
}
}
return tx.Delete(&model.ReaderBookSource{}, "id = ?", id).Error
})
}
// GetSourceState 取书源会话状态;不存在返回 (nil, nil)。
func (r *ReaderRepository) GetSourceState(ctx context.Context, sourceURL string) (*model.ReaderSourceState, error) {
var out model.ReaderSourceState
err := r.db.WithContext(ctx).First(&out, "source_url = ?", sourceURL).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &out, nil
}
// SaveSourceState 覆盖保存书源会话状态(不存在则新建)。
func (r *ReaderRepository) SaveSourceState(ctx context.Context, st *model.ReaderSourceState) error {
var existing model.ReaderSourceState
err := r.db.WithContext(ctx).First(&existing, "source_url = ?", st.SourceURL).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return r.db.WithContext(ctx).Create(st).Error
}
if err != nil {
return err
}
st.ID = existing.ID
st.CreatedAt = existing.CreatedAt
return r.db.WithContext(ctx).Save(st).Error
}
// ListBooks 用户书架(按 order 排序)。
+128
View File
@@ -0,0 +1,128 @@
package repository
import (
"errors"
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"github.com/truewhile/MeBox/internal/database"
"github.com/truewhile/MeBox/internal/model"
)
func newReaderTestRepo(t *testing.T) *ReaderRepository {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := database.AutoMigrate(db); err != nil {
t.Fatalf("migrate: %v", err)
}
return New(db).Reader
}
// TestSourceStateRoundTrip 验证会话状态按书源 URL 存取。
func TestSourceStateRoundTrip(t *testing.T) {
repo := newReaderTestRepo(t)
ctx := t.Context()
// 不存在时返回 (nil, nil),调用方据此走默认值
got, err := repo.GetSourceState(ctx, "https://a.example.com")
if err != nil || got != nil {
t.Fatalf("首次读取应为空: got=%v err=%v", got, err)
}
st := &model.ReaderSourceState{
SourceURL: "https://a.example.com",
Variable: `{"线路":"v1"}`,
LoginInfo: `{"邮箱":"u@e.com"}`,
LoginHeader: `{"X-Token":"t"}`,
Cookies: `{"example.com":"qttoken=abc"}`,
}
if err := repo.SaveSourceState(ctx, st); err != nil {
t.Fatal(err)
}
got, err = repo.GetSourceState(ctx, "https://a.example.com")
if err != nil {
t.Fatal(err)
}
if got.LoginInfo != st.LoginInfo || got.Variable != st.Variable {
t.Fatalf("读回不一致: %+v", got)
}
// 再次保存应为更新而非插入(同 URL 唯一)
st.LoginInfo = `{"邮箱":"new@e.com"}`
if err := repo.SaveSourceState(ctx, st); err != nil {
t.Fatal(err)
}
var count int64
if err := repo.db.Model(&model.ReaderSourceState{}).
Where("source_url = ?", "https://a.example.com").Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 1 {
t.Fatalf("同一书源应只有一行状态,实际 %d", count)
}
got, _ = repo.GetSourceState(ctx, "https://a.example.com")
if got.LoginInfo != `{"邮箱":"new@e.com"}` {
t.Fatalf("更新未生效: %+v", got)
}
}
// TestDeleteSourceAlsoClearsState 删除书源应连带清理登录态。
func TestDeleteSourceAlsoClearsState(t *testing.T) {
repo := newReaderTestRepo(t)
ctx := t.Context()
src := &model.ReaderBookSource{Name: "源", SourceURL: "https://b.example.com", Enabled: true}
if err := repo.CreateSource(ctx, src); err != nil {
t.Fatal(err)
}
if err := repo.SaveSourceState(ctx, &model.ReaderSourceState{
SourceURL: "https://b.example.com", LoginInfo: `{"a":"b"}`,
}); err != nil {
t.Fatal(err)
}
if err := repo.DeleteSource(ctx, src.ID); err != nil {
t.Fatal(err)
}
st, err := repo.GetSourceState(ctx, "https://b.example.com")
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatal(err)
}
if st != nil {
t.Fatalf("书源删除后登录态应一并清理,实际仍在: %+v", st)
}
}
// TestImportUpdateKeepsSourceState 重新导入书源(更新 RawJSON)不应丢失登录态。
func TestImportUpdateKeepsSourceState(t *testing.T) {
repo := newReaderTestRepo(t)
ctx := t.Context()
src := &model.ReaderBookSource{Name: "源", SourceURL: "https://c.example.com", RawJSON: `{"v":1}`}
if err := repo.CreateSource(ctx, src); err != nil {
t.Fatal(err)
}
if err := repo.SaveSourceState(ctx, &model.ReaderSourceState{
SourceURL: "https://c.example.com", LoginInfo: `{"k":"keep"}`,
}); err != nil {
t.Fatal(err)
}
// 模拟导入更新:只改书源自身字段
src.RawJSON = `{"v":2}`
if err := repo.UpdateSource(ctx, src); err != nil {
t.Fatal(err)
}
st, err := repo.GetSourceState(ctx, "https://c.example.com")
if err != nil {
t.Fatal(err)
}
if st == nil || st.LoginInfo != `{"k":"keep"}` {
t.Fatalf("更新书源后登录态丢失: %+v", st)
}
}