fix(db): 「最后一个回答」不再被置顶/公开切换推到现在
`QuestionStats` 的 `last_a` 取的是 `MAX(updated_at)`,而 GORM 的 Update 一律刷新 updated_at:置顶、公开/私密切换、标记已读都会走 Update。 后果是箱主整理一下旧问题(一次回答都没发生),公开接口 `/api/v1/site-stats` 就说「刚刚有人回答了」。 实测(真库,临时测试已删):一条 9 天前回答的提问,置顶/公开切换后 LastAnswerAt 从 `10-15 10:23` 变成 `10-24 10:23`,偏差 216 小时。 改成 `COALESCE(answered_at, updated_at)`: - 新数据取 answered_at(首次回答时间,改写回答不重置); - 加列之前的历史行 answered_at 为空,退回 updated_at —— 与改动前口径 一致,不会让这些行从统计里消失(有测试锁住这一半)。 同时删掉 `GetHotQuestions`:全仓零调用点(只有接口声明 + 实现,route/ 里 Questions 的 15 个调用不含它),且同样按 updated_at 排序 —— 死代码 + 错误排序,留着就是给以后的人埋坑,要用时按 answered_at 重写。 影响面:`/api/v1/site-stats` 的 `last_answer_at` 字段与 `text` 里的 「最后一个回答发生在」,字段名与结构不变,只是值更准(更早)。无 schema 变更、无接口变更。性能:执行计划仍是 Seq Scan + Aggregate,5 万行实测 7.9ms vs 8.0ms,差异在噪声带内(0~1.8ms,折合每行约 30ns), 而该接口有 30 分钟进程内缓存。 新增两个回归测试: - TestQuestionStatsLastAnswerIgnoresLaterEdits(改前红、改后绿) - TestQuestionStatsLastAnswerFallsBackToUpdatedAt(历史行回退)
This commit is contained in:
@@ -48,7 +48,6 @@ type QuestionsStore interface {
|
||||
// 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)
|
||||
|
||||
// 全站统计
|
||||
QuestionStats(ctx context.Context) (*QuestionStats, error)
|
||||
@@ -59,7 +58,7 @@ type QuestionStats struct {
|
||||
TotalQuestions int64
|
||||
AnsweredQuestions int64
|
||||
LastQuestionAt *time.Time // 最后一个提问发生时间(questions.created_at 最大值)
|
||||
LastAnswerAt *time.Time // 最后一个回答发生时间(已回答问题的 updated_at 最大值)
|
||||
LastAnswerAt *time.Time // 最后一个回答发生时间(answered_at 最大值,历史行退回 updated_at)
|
||||
}
|
||||
|
||||
func NewQuestionsStore(db *gorm.DB) QuestionsStore {
|
||||
@@ -567,27 +566,14 @@ func escapeLike(s string) string {
|
||||
return r.Replace(s)
|
||||
}
|
||||
|
||||
// GetHotQuestions 返回"热门问题":最新活跃的公开已回答提问(按回答更新时间倒序)。
|
||||
// 提问箱没有点赞数据,用"最近被回答"作为热度近似,适合主页/探索页展示。
|
||||
func (db *questions) GetHotQuestions(ctx context.Context, limit int) ([]*Question, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
var questions []*Question
|
||||
if err := db.WithContext(ctx).
|
||||
Where("is_private = ? AND answer != ''", false).
|
||||
Order("updated_at DESC").
|
||||
Limit(limit).
|
||||
Find(&questions).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "get hot questions")
|
||||
}
|
||||
return questions, nil
|
||||
}
|
||||
|
||||
// QuestionStats 返回全站提问相关聚合统计。
|
||||
// 使用 gorm 查询构造器生成 COUNT / MAX,兼容 MySQL 与 PostgreSQL。
|
||||
// 最后一个回答时间取"已回答问题"的 updated_at 最大值——回答动作会更新 updated_at,
|
||||
// 与 crontab 旧逻辑(answer 非空记录中 updated_at 最大)一致。
|
||||
//
|
||||
// 最后一个回答时间取 answered_at,不取 updated_at:GORM 的 Update 一律刷新
|
||||
// updated_at,而置顶、公开/私密切换、标记已读都会走 Update,于是「最后一个回答」
|
||||
// 会被这些与回答无关的操作推到当下(实测一条 9 天前回答的提问,置顶后 LastAnswerAt
|
||||
// 变成"刚刚")。answered_at 为空的是加列之前的历史行,退回 updated_at,
|
||||
// 与改动前的口径一致,不会让这些行从统计里消失。
|
||||
func (db *questions) QuestionStats(ctx context.Context) (*QuestionStats, error) {
|
||||
var total, answered int64
|
||||
if err := db.WithContext(ctx).Model(&Question{}).Count(&total).Error; err != nil {
|
||||
@@ -602,7 +588,7 @@ func (db *questions) QuestionStats(ctx context.Context) (*QuestionStats, error)
|
||||
LastA *time.Time `gorm:"column:last_a"`
|
||||
}
|
||||
if err := db.WithContext(ctx).Model(&Question{}).
|
||||
Select("MAX(created_at) AS last_q, MAX(CASE WHEN answer != '' THEN updated_at ELSE NULL END) AS last_a").
|
||||
Select("MAX(created_at) AS last_q, MAX(CASE WHEN answer <> '' THEN COALESCE(answered_at, updated_at) END) AS last_a").
|
||||
Scan(&t).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "get last question/answer time")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user