fix v13
This commit is contained in:
@@ -30,6 +30,12 @@ type QuestionsStore interface {
|
||||
GetByUserID(ctx context.Context, userID uint, opts GetQuestionsByUserIDOptions) ([]*Question, error)
|
||||
GetByAskUserID(ctx context.Context, userID uint, opts GetQuestionsByAskUserIDOptions) ([]*Question, error)
|
||||
AnswerByID(ctx context.Context, id uint, answer string) error
|
||||
// AnswerByIDPrivate 写入回答的同时把题目置为私密(站方 L2「收窄公开」)。
|
||||
//
|
||||
// 做成单条 UPDATE 而不是「先 SetPrivate 再 AnswerByID」:
|
||||
// - 两条语句之间存在「公开 + 已作答」的窗口,而 L2 的全部意义就是消灭这个窗口;
|
||||
// - 若第二条失败,题目会变成「私密 + 无回答」,箱主等于凭空丢了一条内容。
|
||||
AnswerByIDPrivate(ctx context.Context, id uint, answer string) error
|
||||
DeleteByID(ctx context.Context, id uint) error
|
||||
UpdateCensor(ctx context.Context, id uint, opts UpdateQuestionCensorOptions) error
|
||||
Count(ctx context.Context, userID uint, opts GetQuestionsCountOptions) (int64, error)
|
||||
@@ -371,6 +377,23 @@ func (db *questions) getBy(ctx context.Context, cursor *dbutil.Cursor, whereQuer
|
||||
return questions, nil
|
||||
}
|
||||
|
||||
// AnswerByIDPrivate 回答并强制私密。见接口注释:必须是一条 UPDATE。
|
||||
func (db *questions) AnswerByIDPrivate(ctx context.Context, id uint, answer string) error {
|
||||
var question Question
|
||||
if err := db.WithContext(ctx).First(&question, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrQuestionNotExist
|
||||
}
|
||||
return errors.Wrap(err, "get question by ID")
|
||||
}
|
||||
|
||||
if err := db.WithContext(ctx).Model(&question).Where("id = ?", id).
|
||||
Updates(map[string]interface{}{"answer": answer, "is_read": true, "is_private": true}).Error; err != nil {
|
||||
return errors.Wrap(err, "update question answer as private")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *questions) AnswerByID(ctx context.Context, id uint, answer string) error {
|
||||
var question Question
|
||||
if err := db.WithContext(ctx).First(&question, id).Error; err != nil {
|
||||
|
||||
@@ -31,6 +31,7 @@ func TestQuestions(t *testing.T) {
|
||||
{"GetByUserID", testQuestionsGetByUserID},
|
||||
{"GetByAskUserID", testQuestionsGetByAskUserID},
|
||||
{"AnswerByID", testQuestionsAnswerByID},
|
||||
{"AnswerByIDPrivate", testQuestionsAnswerByIDPrivate},
|
||||
{"DeleteByID", testQuestionsDeleteByID},
|
||||
{"UpdateCensor", testQuestionsUpdateCensor},
|
||||
{"Count", testQuestionsCount},
|
||||
@@ -322,6 +323,33 @@ func testQuestionsAnswerByID(t *testing.T, ctx context.Context, db *questions) {
|
||||
})
|
||||
}
|
||||
|
||||
// testQuestionsAnswerByIDPrivate 覆盖 L2「收窄公开」的写入语义:
|
||||
// 回答与 is_private 必须在同一次写入里生效,不能出现「公开 + 已作答」的中间态。
|
||||
func testQuestionsAnswerByIDPrivate(t *testing.T, ctx context.Context, db *questions) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
_, err := db.Create(ctx, CreateQuestionOptions{
|
||||
FromIP: "114.5.1.4",
|
||||
UserID: 1,
|
||||
Content: "Content - 1",
|
||||
AskerUserID: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
err = db.AnswerByIDPrivate(ctx, 1, "Answer - 1")
|
||||
require.Nil(t, err)
|
||||
|
||||
got, err := db.GetByID(ctx, 1)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "Answer - 1", got.Answer)
|
||||
require.True(t, got.IsPrivate)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
err := db.AnswerByIDPrivate(ctx, 404, "Answer - 1")
|
||||
require.Equal(t, ErrQuestionNotExist, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testQuestionsDeleteByID(t *testing.T, ctx context.Context, db *questions) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
_, err := db.Create(ctx, CreateQuestionOptions{
|
||||
|
||||
+18
-18
@@ -202,29 +202,29 @@ func (s *restrictions) RevokeActive(ctx context.Context, userID uint) (int64, er
|
||||
}
|
||||
|
||||
func (s *restrictions) ActiveLevel(ctx context.Context, userID uint) (RestrictionLevel, *UserRestriction, error) {
|
||||
levels, err := s.ActiveLevels(ctx, []uint{userID})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
level, ok := levels[userID]
|
||||
if !ok {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
var restriction UserRestriction
|
||||
// 只查一次:这个方法在每次内容提交时都会被调用(发布拦截 + 审核严格度),
|
||||
// 早先「先 ActiveLevels 再 First」是两次往返,1C1G 上没必要。
|
||||
var rows []*UserRestriction
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Where("level = ?", string(level)).
|
||||
Where("revoked_at IS NULL").
|
||||
Where("expires_at > ?", time.Now()).
|
||||
Order("created_at DESC").
|
||||
First(&restriction).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return level, nil, nil
|
||||
}
|
||||
return "", nil, errors.Wrap(err, "find active restriction")
|
||||
Find(&rows).Error; err != nil {
|
||||
return "", nil, errors.Wrap(err, "list active restrictions")
|
||||
}
|
||||
return level, &restriction, nil
|
||||
|
||||
// 正常情况最多一条(Create 是替换语义),这里取最高级别以防历史脏数据叠加。
|
||||
var best *UserRestriction
|
||||
bestRank := 0
|
||||
for _, row := range rows {
|
||||
if rank, ok := restrictionLevelRank[RestrictionLevel(row.Level)]; ok && rank > bestRank {
|
||||
best, bestRank = row, rank
|
||||
}
|
||||
}
|
||||
if best == nil {
|
||||
return "", nil, nil
|
||||
}
|
||||
return RestrictionLevel(best.Level), best, nil
|
||||
}
|
||||
|
||||
func (s *restrictions) ActiveLevels(ctx context.Context, userIDs []uint) (map[uint]RestrictionLevel, error) {
|
||||
|
||||
@@ -621,7 +621,12 @@ func (db *users) GetShowInHome(ctx context.Context, page, pageSize int, sort str
|
||||
var users []*User
|
||||
var count int64
|
||||
|
||||
if err := db.WithContext(ctx).Model(&User{}).Where("show_in_home = ?", true).Count(&count).Error; err != nil {
|
||||
// 计数必须与下面的列表用同一套过滤,否则总数包含被处置箱主、实际行数更少,
|
||||
// 首页会出现「最后一页空白」。
|
||||
if err := db.WithContext(ctx).Model(&User{}).
|
||||
Where("show_in_home = ?", true).
|
||||
Where("id NOT IN (?)", activeRestrictionsSubQuery(db.WithContext(ctx))).
|
||||
Count(&count).Error; err != nil {
|
||||
return nil, 0, errors.Wrap(err, "count show in home users")
|
||||
}
|
||||
|
||||
@@ -798,6 +803,8 @@ func (db *users) GetHotTags(ctx context.Context, limit int) ([]string, error) {
|
||||
if err := db.WithContext(ctx).
|
||||
Model(&User{}).
|
||||
Where("show_in_home = ? AND tags IS NOT NULL AND tags != ''", true).
|
||||
// 被处置箱主的标签不应再被推荐(热门标签是个显眼的曝光位)。
|
||||
Where("id NOT IN (?)", activeRestrictionsSubQuery(db.WithContext(ctx))).
|
||||
Pluck("tags", &tagsList).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "get user tags for hot tags")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user