fix templates v1

This commit is contained in:
tamakyi
2026-10-22 17:04:10 +08:00
parent 8fcb23f7b1
commit bd02eb7ed2
12 changed files with 293 additions and 12 deletions
+22
View File
@@ -44,6 +44,9 @@ type QuestionsStore interface {
SetPinned(ctx context.Context, id uint, pinned bool) error
SetRead(ctx context.Context, id uint) error
SetUnread(ctx context.Context, id uint) error
// NextUnread 取该用户最早的一条未读提问(未读 = is_read=false 且未回答)。
// excludeID 用于跳过正在查看的那条,避免在两条未读之间来回跳。
NextUnread(ctx context.Context, userID, excludeID uint) (*Question, error)
SearchByUserID(ctx context.Context, userID uint, keyword string) ([]*Question, error)
GetHotQuestions(ctx context.Context, limit int) ([]*Question, error)
@@ -502,6 +505,25 @@ func (db *questions) SetUnread(ctx context.Context, id uint) error {
return db.WithContext(ctx).Model(&Question{}).Where("id = ? AND answer = ''", id).Update("is_read", false).Error
}
// NextUnread 按创建时间正序取第一条未读提问(未读 = is_read=false 且未回答),无则返回 nil。
// 正序即「先来先处理」:反复取下一条就能把积压按先后顺序清空,不会跳过更早的提问。
func (db *questions) NextUnread(ctx context.Context, userID, excludeID uint) (*Question, error) {
var question Question
q := db.WithContext(ctx).
Model(&Question{}).
Where("user_id = ? AND is_read = ? AND answer = ''", userID, false)
if excludeID > 0 {
q = q.Where("id != ?", excludeID)
}
if err := q.Order("created_at ASC, id ASC").First(&question).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, errors.Wrap(err, "get next unread question")
}
return &question, nil
}
// SearchByUserID 全文搜索提问箱主人收到的提问(含私密,仅限本人管理后台使用)。
// 使用 LIKE 模糊匹配问题内容与回答;keyword 中的通配符 % _ \ 会被转义,避免模式滥用。
func (db *questions) SearchByUserID(ctx context.Context, userID uint, keyword string) ([]*Question, error) {
+53
View File
@@ -37,6 +37,7 @@ func TestQuestions(t *testing.T) {
{"Count", testQuestionsCount},
{"SetPrivate", testQuestionsSetPrivate},
{"SetPublic", testQuestionsSetPublic},
{"NextUnread", testQuestionsNextUnread},
} {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
@@ -493,3 +494,55 @@ func testQuestionsSetPublic(t *testing.T, ctx context.Context, db *questions) {
require.False(t, got.IsPrivate)
})
}
// NextUnread 必须满足三条:只取本人的提问、只取未读(is_read=false 且未回答)、按最早优先。
func testQuestionsNextUnread(t *testing.T, ctx context.Context, db *questions) {
create := func(userID uint, content string) *Question {
q, err := db.Create(ctx, CreateQuestionOptions{
FromIP: "114.5.1.4",
UserID: userID,
Content: content,
})
require.Nil(t, err)
return q
}
q1 := create(1, "Content - 1")
q2 := create(1, "Content - 2")
q3 := create(1, "Content - 3")
other := create(2, "Content - other")
t.Run("oldest first", func(t *testing.T) {
got, err := db.NextUnread(ctx, 1, 0)
require.Nil(t, err)
require.Equal(t, q1.ID, got.ID)
})
t.Run("exclude current", func(t *testing.T) {
got, err := db.NextUnread(ctx, 1, q1.ID)
require.Nil(t, err)
require.Equal(t, q2.ID, got.ID)
})
t.Run("only own questions", func(t *testing.T) {
got, err := db.NextUnread(ctx, 2, 0)
require.Nil(t, err)
require.Equal(t, other.ID, got.ID)
})
t.Run("skip read and answered", func(t *testing.T) {
require.Nil(t, db.SetRead(ctx, q1.ID))
require.Nil(t, db.SetRead(ctx, q2.ID))
require.Nil(t, db.AnswerByID(ctx, q3.ID, "Answer - 3"))
got, err := db.NextUnread(ctx, 1, 0)
require.Nil(t, err)
require.Nil(t, got, "已读与已回答都不算未读")
})
t.Run("empty", func(t *testing.T) {
got, err := db.NextUnread(ctx, 404, 0)
require.Nil(t, err)
require.Nil(t, got)
})
}