chore: baseline snapshot before moderation-strictness feature

Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
This commit is contained in:
tamakyi
2026-09-10 10:56:27 +08:00
commit e3e59e7c69
296 changed files with 67458 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
-- ============================================================================
-- TamaBox「加载更多重复加载」根因排查 SQL(PostgreSQL)
-- 目标箱: aoi(/api/v1/user/aoi/questions,/_/aoi/3105 底部历史列表同款数据)
-- 依据: internal/db/questions.go GetByUserID (PinFirst) + route/question/page.go ListAPI
-- ListAPI 等价条件: user_id=aoi箱 AND answer<>'' AND is_private=false AND deleted_at IS NULL
-- 排序: is_pinned DESC, created_at DESC, id DESC;每页 20 条
-- 游标: "<isPinned>.<createdAtUnixNano>.<id>"(末条编码,下一页取严格更小的行)
-- 兼容: psql / pgAdmin / DBeaver(避免 psql 专属元命令;NOTICE 各客户端均可看到)
-- 若提示语法错误 -> 说明当前连接不是 PostgreSQL,请改连 PG 实例
-- 使用: 按 PART 顺序整段复制执行,把「结果 + NOTICE 消息」原样粘贴回来
-- ============================================================================
-- ============================================================================
-- PART A 环境 / 表结构 / 集合核对
-- 目的: 核对列类型与精度、索引,确认下方 SQL 筛选后条数与线上 API 总数一致
-- (线上 page_size=99 曾返回 38 条 -> 这里 answered_public_all 应=38,
-- 若不一致说明还有隐藏过滤或连接的不是同一库)
-- ============================================================================
SELECT 'PART A: 环境与结构核对' AS step;
SELECT current_setting('server_version') AS server_version,
current_setting('TimeZone') AS session_tz,
version() AS full_version;
-- 1) questions 关键列定义(看 created_at 是 timestamptz 还是 timestamp、精度几位)
SELECT column_name, data_type, datetime_precision, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'questions'
AND column_name IN ('id','created_at','updated_at','deleted_at',
'user_id','answer','is_private','is_pinned','is_read')
ORDER BY ordinal_position;
-- 2) user_id 相关索引(若只有单列 user_id 索引即可;看是否缺复合索引影响性能,不影响正确性)
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'questions' AND indexdef ILIKE '%user_id%';
-- 3) 目标集合核对:应=线上 API 总数(曾为 38)
SELECT u.id AS aoi_user_id,
u.domain AS domain,
count(*) FILTER (WHERE q.answer <> '' AND q.is_private = false)
AS answered_public_all,
count(*) FILTER (WHERE q.answer <> '' AND q.is_private = false AND q.is_pinned)
AS answered_public_pinned,
count(*) AS all_rows_for_user
FROM users u
LEFT JOIN questions q ON q.user_id = u.id AND q.deleted_at IS NULL
WHERE u.domain = 'aoi'
GROUP BY u.id, u.domain;
-- 4) 该集合最新 5 条:把 created_at 按 UTC 与 UnixNano 打出来,
-- 便于和线上 API JSON / next_cursor(形如 0.1788566476678885000.3089)逐位目视比对
SELECT q.id,
q.is_pinned,
q.created_at,
to_char(q.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US') AS created_at_utc_text,
(extract(epoch FROM q.created_at) * 1000000000)::numeric(30,0) AS created_at_unix_nano
FROM questions q
JOIN users u ON u.id = q.user_id AND u.domain = 'aoi'
WHERE q.answer <> '' AND q.is_private = false AND q.deleted_at IS NULL
ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC
LIMIT 5;
-- 5) 是否存在 created_at 完全相同的行(同一时间多行时若旧版漏 id tiebreaker 会翻页跳行/重复)
SELECT q.created_at, count(*) AS n
FROM questions q
JOIN users u ON u.id = q.user_id AND u.domain = 'aoi'
WHERE q.answer <> '' AND q.is_private = false AND q.deleted_at IS NULL
GROUP BY q.created_at
HAVING count(*) > 1
ORDER BY n DESC
LIMIT 10;
-- ============================================================================
-- PART B 用纯 SQL 完整复刻 ListAPI 的复合键集翻页(核心复现)
-- 目的: 逐页打印每页的 id 序列。若 DB 层正常: 第1页 20 条、第2页 18 条后结束,
-- 且无任何一行与上一页重复(总 38 条不重不漏)。
-- 若 DB 层有 bug: 会出现重复页 / 跳页 / 死循环(脚本会检测并提前退出)。
-- 判读:
-- [DB 层正常] 第1页id集合与线上首页一致,第2页正常翻出剩余 -> 问题在应用层
-- (部署版本旧 / GORM 参数转换),与 DB 无关,见 PART C 指纹进一步定位。
-- [DB 层异常] 某页与上一页完全重复 / 提前空页 / 顺序错乱 -> 问题在数据或库配置,
-- 把输出贴回即可看到错在哪一页。
-- ============================================================================
SELECT 'PART B: keyset 翻页复现' AS step;
DROP TABLE IF EXISTS pg_temp.diag_pages;
DO $$
DECLARE
v_uid bigint;
v_page int := 0;
v_rows int;
v_ids text;
v_prev_ids text := '';
v_pin boolean; -- 上一页末条 is_pinned(游标锚点)
v_ts timestamptz; -- 上一页末条 created_at(游标锚点)
v_id bigint; -- 上一页末条 id(游标锚点)
v_last_pin boolean;
v_last_ts timestamptz;
v_last_id bigint;
v_note text;
r record;
BEGIN
SELECT id INTO v_uid FROM users WHERE domain = 'aoi' LIMIT 1;
IF v_uid IS NULL THEN
RAISE EXCEPTION 'users 表中不存在 domain=aoi 的用户,请把脚本里的 aoi 换成你的箱 domain';
END IF;
CREATE TEMP TABLE diag_pages (page_no int, n_rows int, ids text);
LOOP
v_page := v_page + 1;
v_ids := '';
v_rows := 0;
v_last_pin := NULL; v_last_ts := NULL; v_last_id := NULL;
FOR r IN
SELECT q.id, q.is_pinned, q.created_at
FROM questions q
WHERE q.user_id = v_uid
AND q.answer <> ''
AND q.is_private = false
AND q.deleted_at IS NULL
AND (
CASE
WHEN v_page = 1 THEN TRUE -- 首页无游标
WHEN v_pin THEN -- 游标位于置顶段: 置顶段更小 + 全部非置顶
(q.is_pinned = true AND (q.created_at < v_ts OR (q.created_at = v_ts AND q.id < v_id)))
OR q.is_pinned = false
ELSE -- 游标位于非置顶段: 非置顶段内更小
q.is_pinned = false AND (q.created_at < v_ts OR (q.created_at = v_ts AND q.id < v_id))
END
)
ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC
LIMIT 20
LOOP
v_rows := v_rows + 1;
v_ids := v_ids || r.id::text || ',';
v_last_pin := r.is_pinned;
v_last_ts := r.created_at;
v_last_id := r.id;
END LOOP;
IF v_rows = 0 THEN
RAISE NOTICE '第 % 页: 0 行 -> 翻页正常结束(空页,等价 has_more=false)', v_page;
EXIT;
END IF;
v_ids := left(v_ids, -1);
INSERT INTO diag_pages VALUES (v_page, v_rows, v_ids);
RAISE NOTICE '第 % 页: % 行 ids=[%]', v_page, v_rows, v_ids;
-- 死循环防护: 本页与上一页逐字相同(key set 未前进)则提前退出并标记异常
IF v_ids = v_prev_ids THEN
RAISE NOTICE '!!! 异常: 第 % 页与第 % 页内容完全重复,游标未前进(复现线上 bug 的症状)', v_page, v_page - 1;
EXIT;
END IF;
v_prev_ids := v_ids;
-- 末页判定: 本页不足 20 行 -> 已无下一页(等价 ListAPI 的 has_more=false)
IF v_rows < 20 THEN
RAISE NOTICE '本页不足 20 行 -> 已到末页(等价 has_more=false),翻页完成';
EXIT;
END IF;
IF v_page >= 30 THEN
RAISE NOTICE '!!! 异常: 已达 30 页上限仍未收敛,疑似死循环';
EXIT;
END IF;
-- 前进游标锚点 = 本页末条
v_pin := v_last_pin;
v_ts := v_last_ts;
v_id := v_last_id;
END LOOP;
RAISE NOTICE '--- 汇总: 共 % 页 ---', (SELECT count(*) FROM diag_pages);
END $$;
-- 同一会话内再以表格形式输出一次(若客户端 NOTICE 显示不全,看这个即可)
SELECT page_no, n_rows, ids
FROM pg_temp.diag_pages
ORDER BY page_no;
-- 重复检测: 跨页是否存在重复 id(正常应为 0 行,无重复时只显示上一条提示)
SELECT '跨页重复检测: 若下方无第二张表结果则无重复' AS check_msg;
SELECT unnest(string_to_array(ids, ','))::bigint AS duplicated_question_id
FROM pg_temp.diag_pages
GROUP BY 1
HAVING count(*) > 1;
-- ============================================================================
-- PART C created_at 精度指纹核对(可选,用于区分「部署版本旧」vs「DB 环境问题」)
-- 目的: 若线上 next_cursor 里的 UnixNano(如 0.1788566476678885000.3089 中的
-- 1788566476678885000)能在下方查询里精确命中对应 id 的 created_at_unix_nano,
-- 说明 API 下发的游标与 DB 行完全对应 -> 问题几乎锁定在部署版本/API 层。
-- 若命中不了 / 差着末几位 -> 说明 DB 存值与线上 JSON 时间对不上,是库环境问题。
-- 用法: 把下方 id 列表替换成你想核对的线上行(默认最新几条 + 3089/3105)。
-- ============================================================================
SELECT 'PART C: created_at 精度指纹' AS step;
SELECT q.id,
q.is_pinned,
to_char(q.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US') AS created_at_utc_text,
(extract(epoch FROM q.created_at) * 1000000000)::numeric(30,0) AS created_at_unix_nano,
(extract(epoch FROM q.created_at) * 1000000000)::numeric(30,0) % 1000 AS nano_tail3 -- 应为0: 微秒精度*1000
FROM questions q
WHERE q.id IN (3109, 3105, 3089, 3088) -- 替换成线上首页前几条 id
ORDER BY q.id DESC;
-- ============================================================================
-- 判读指引(复制上面的 PART A/B 即可,C 按需):
-- 1) PART A.3 的 answered_public_all 应与线上 API 总数一致(曾为 38)
-- 2) PART B 第1页应=线上首页20条;第2页应翻出剩余且与第1页无重复 ->
-- DB 层 keyset 自洽 = 问题在应用层(部署版本/参数转换)
-- 3) 若 PART B 出现「某页与上一页完全相同」= 直接在 DB 层复现了 bug
-- 4) PART C 的 unix_nano 与线上 next_cursor 中间段逐位一致 = 版本/API 层问题
-- ============================================================================