fix templates v1
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,6 +144,9 @@ var enDict = map[string]string{
|
||||
"qitem.markRead": "Mark as read",
|
||||
"qitem.markUnread": "Mark as unread",
|
||||
"qitem.readLocked": "Answered, locked as read",
|
||||
"qitem.nextUnread": "Next unread",
|
||||
"qitem.nextUnreadTitle": "Mark this one as read and open the next unread question",
|
||||
"qitem.submitAndNext": "Answer & open next unread",
|
||||
|
||||
// Admin / profile page
|
||||
"admin.profile": "Profile",
|
||||
@@ -764,6 +767,7 @@ var enDict = map[string]string{
|
||||
"question.flash.operated": "Operation successful",
|
||||
"question.flash.markedRead": "Marked as read",
|
||||
"question.flash.markedUnread": "Marked as unread",
|
||||
"question.flash.noUnread": "No unread questions left",
|
||||
// 平台处置(L1–L4):被处置箱主在处置期内不能发布公开内容
|
||||
"question.errRestrictedPublish": "Publishing is suspended while this box is under moderation",
|
||||
"question.errRestrictedPublic": "Content cannot be made public while this box is under moderation",
|
||||
|
||||
@@ -144,6 +144,9 @@ var jaDict = map[string]string{
|
||||
"qitem.markRead": "既読にする",
|
||||
"qitem.markUnread": "未読にする",
|
||||
"qitem.readLocked": "回答済みのため既読に固定",
|
||||
"qitem.nextUnread": "次の未読",
|
||||
"qitem.nextUnreadTitle": "この質問を既読にして次の未読を開く",
|
||||
"qitem.submitAndNext": "回答して次の未読を開く",
|
||||
|
||||
// 個人設定ページ
|
||||
"admin.profile": "プロフィール",
|
||||
@@ -764,6 +767,7 @@ var jaDict = map[string]string{
|
||||
"question.flash.operated": "操作が完了しました",
|
||||
"question.flash.markedRead": "既読にしました",
|
||||
"question.flash.markedUnread": "未読にしました",
|
||||
"question.flash.noUnread": "未読の質問はありません",
|
||||
// 平台处置(L1–L4):被处置箱主在处置期内不能发布公开内容
|
||||
"question.errRestrictedPublish": "現在はプラットフォームによる措置期間中のため、回答の公開を停止しています",
|
||||
"question.errRestrictedPublic": "措置期間中はコンテンツを公開に設定できません",
|
||||
|
||||
@@ -144,6 +144,9 @@ var zhCNDict = map[string]string{
|
||||
"qitem.markRead": "标记为已读",
|
||||
"qitem.markUnread": "标记为未读",
|
||||
"qitem.readLocked": "已回答,已锁定为已读",
|
||||
"qitem.nextUnread": "下一条未读",
|
||||
"qitem.nextUnreadTitle": "将本条标记为已读,并打开下一条未读",
|
||||
"qitem.submitAndNext": "回答并进入下一条未读",
|
||||
|
||||
// 个人设置页
|
||||
"admin.profile": "个人信息",
|
||||
@@ -764,6 +767,7 @@ var zhCNDict = map[string]string{
|
||||
"question.flash.operated": "操作成功",
|
||||
"question.flash.markedRead": "已标记为已读",
|
||||
"question.flash.markedUnread": "已标记为未读",
|
||||
"question.flash.noUnread": "没有未读提问了",
|
||||
// 平台处置(L1–L4):被处置箱主在处置期内不能发布公开内容
|
||||
"question.errRestrictedPublish": "当前处于平台处置期,已暂停发布回答",
|
||||
"question.errRestrictedPublic": "当前处于平台处置期,不可将内容设为公开",
|
||||
|
||||
@@ -144,6 +144,9 @@ var zhTWDict = map[string]string{
|
||||
"qitem.markRead": "標記為已讀",
|
||||
"qitem.markUnread": "標記為未讀",
|
||||
"qitem.readLocked": "已回答,已鎖定為已讀",
|
||||
"qitem.nextUnread": "下一條未讀",
|
||||
"qitem.nextUnreadTitle": "將本條標記為已讀,並開啟下一條未讀",
|
||||
"qitem.submitAndNext": "回答並進入下一條未讀",
|
||||
|
||||
// 個人設定頁
|
||||
"admin.profile": "個人資訊",
|
||||
@@ -764,6 +767,7 @@ var zhTWDict = map[string]string{
|
||||
"question.flash.operated": "操作成功",
|
||||
"question.flash.markedRead": "已標記為已讀",
|
||||
"question.flash.markedUnread": "已標記為未讀",
|
||||
"question.flash.noUnread": "沒有未讀提問了",
|
||||
// 平台处置(L1–L4):被处置箱主在处置期内不能发布公开内容
|
||||
"question.errRestrictedPublish": "當前處於平台處置期,已暫停發布回答",
|
||||
"question.errRestrictedPublic": "當前處於平台處置期,不可將內容設為公開",
|
||||
|
||||
@@ -315,6 +315,8 @@ func New() *flamego.Flame {
|
||||
f.Post("/pin", question.TogglePin)
|
||||
f.Post("/read", question.SetRead)
|
||||
f.Post("/unread", question.SetUnread)
|
||||
// 「下一条未读」= 标记本条已读 + 跳到最早的一条未读(改变状态,故为 POST)
|
||||
f.Post("/next-unread", question.NextUnread)
|
||||
f.Post("/answer", reqUserSignIn, form.Bind(form.PublishAnswerQuestion{}), question.PublishAnswer)
|
||||
// 举报入口:公开、不要求登录(主要目击者是匿名访客),限流防刷。
|
||||
// 举报只写入 content_reports,不改变任何可见性;处置由箱主或 CLI 完成。
|
||||
|
||||
Reference in New Issue
Block a user