fix(messages): sync exact channel pinned counts

This commit is contained in:
iamxvbaba 2026-07-27 00:17:21 +08:00
parent b742450938
commit e75fd04a28
9 changed files with 232 additions and 62 deletions

View file

@ -79,6 +79,24 @@ AND EXISTS (
baseArgs = append(baseArgs, filter.MinID)
base += fmt.Sprintf(" AND id > $%d", len(baseArgs))
}
out := domain.ChannelHistory{Channel: channel, Self: member, Channels: extraChannels}
needExactTotal := filter.NeedTotalCount || filter.CountOnly
exactTotal := 0
if needExactTotal {
// Exact totals are opt-in. messages.search first/count-only pages and
// messages.getSearchCounters need protocol-exact Count, while ordinary
// getHistory pages must stay on the single bounded page query.
if err := s.db.QueryRow(ctx,
"SELECT count(*)::int FROM channel_messages WHERE "+base,
baseArgs...,
).Scan(&exactTotal); err != nil {
return domain.ChannelHistory{}, fmt.Errorf("count channel history: %w", err)
}
out.Count = exactTotal
}
if filter.CountOnly {
return out, nil
}
scanList := func(sql string, queryArgs []any) ([]domain.ChannelMessage, error) {
rows, err := s.db.Query(ctx, sql, queryArgs...)
if err != nil {
@ -102,7 +120,6 @@ AND EXISTS (
// store 层二次钳制 add_offset 到 [-100,100](与私聊 ListMessagesByUser 对齐):
// 即便某个 caller 漏在 RPC 层钳制,也不会把客户端巨大值变成大 SQL OFFSET 跳扫。
addOffset := domain.ClampMessageHistoryAddOffset(filter.AddOffset)
out := domain.ChannelHistory{Channel: channel, Self: member, Channels: extraChannels}
hasMoreOlder := false
// 锚点条件offset_date 优先按日期、否则按消息 id对齐私聊
// 二者皆空时向更新方向退化为空、向更旧方向退化为全部(取最新)。
@ -199,9 +216,11 @@ AND EXISTS (
}
out.Messages = older
}
out.Count = len(out.Messages)
if hasMoreOlder {
out.Count = len(out.Messages) + 1
if !needExactTotal {
out.Count = len(out.Messages)
if hasMoreOlder {
out.Count = len(out.Messages) + 1
}
}
if err := s.populateChannelMessageReplies(ctx, s.db, viewerUserID, channel, out.Messages); err != nil {
return domain.ChannelHistory{}, err

View file

@ -3,6 +3,7 @@ package postgres
import (
"context"
"errors"
"strings"
"testing"
"telesrv/internal/domain"
@ -98,6 +99,70 @@ func TestChannelMultiPin(t *testing.T) {
t.Fatalf("filterPinned message %d lacks pinned flag", msg.ID)
}
}
// 普通历史热路径保留 bounded has-more hint不为每页额外 COUNT。
hint, err := channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, Limit: 1,
})
if err != nil {
t.Fatalf("filterPinned hint page: %v", err)
}
if len(hint.Messages) != 1 || hint.Count != 2 {
t.Fatalf("filterPinned hint messages/count = %d/%d, want 1/2", len(hint.Messages), hint.Count)
}
// messages.search 首屏显式请求精确总数;页大小不能污染 Count。
exact, err := channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, Limit: 1, NeedTotalCount: true,
})
if err != nil {
t.Fatalf("filterPinned exact page: %v", err)
}
if len(exact.Messages) != 1 || exact.Count != 3 {
t.Fatalf("filterPinned exact messages/count = %d/%d, want 1/3", len(exact.Messages), exact.Count)
}
// messages.getSearchCounters/limit=0 只计数,不加载消息及 reply/reaction companion。
countOnly, err := channels.ListChannelHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
ChannelID: channelID, PinnedOnly: true, NeedTotalCount: true, CountOnly: true,
})
if err != nil {
t.Fatalf("filterPinned count-only: %v", err)
}
if len(countOnly.Messages) != 0 || countOnly.Count != 3 {
t.Fatalf("filterPinned count-only messages/count = %d/%d, want 0/3", len(countOnly.Messages), countOnly.Count)
}
// 精确计数只扫描该频道的 pinned 部分索引,不能退化为全频道消息扫描。
tx, err := pool.Begin(ctx)
if err != nil {
t.Fatalf("begin pinned count explain: %v", err)
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, "SET LOCAL enable_seqscan = off"); err != nil {
t.Fatalf("disable seqscan for pinned count explain: %v", err)
}
rows, err := tx.Query(ctx, `EXPLAIN (COSTS OFF)
SELECT count(*)::int
FROM channel_messages
WHERE channel_id = $1 AND NOT deleted AND pinned`, channelID)
if err != nil {
t.Fatalf("explain pinned count: %v", err)
}
var plan strings.Builder
for rows.Next() {
var line string
if err := rows.Scan(&line); err != nil {
rows.Close()
t.Fatalf("scan pinned count plan: %v", err)
}
plan.WriteString(line)
plan.WriteByte('\n')
}
if err := rows.Err(); err != nil {
rows.Close()
t.Fatalf("read pinned count plan: %v", err)
}
rows.Close()
if !strings.Contains(plan.String(), "channel_messages_live_pinned_idx") {
t.Fatalf("pinned count plan misses partial index:\n%s", plan.String())
}
// 普通历史页的消息行直接携带 pinned 标志(多置顶都标,不只最新)。
page, err := channels.ListChannelHistory(ctx, member.ID, domain.ChannelHistoryFilter{ChannelID: channelID, Limit: 50})
if err != nil {

View file

@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
if err != nil {
t.Fatalf("migrate star gift lifecycle schema: %v", err)
}
if status.Dirty || status.Empty || status.Version != 148 {
t.Fatalf("migration status = %+v, want clean version 148", status)
if status.Dirty || status.Empty || status.Version != 149 {
t.Fatalf("migration status = %+v, want clean version 149", status)
}
}