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
+234
View File
@@ -0,0 +1,234 @@
-- ============================================================================
-- TamaBox「加载更多重复加载」根因诊断 v2(按生产真实结构定制)
-- 适用库: schema = nekobox,表 = nekobox.questions / nekobox.users
-- questions: id numeric, created_at/updated_at/deleted_at timestamp(6) WITHOUT time zone,
-- user_id bigint, answer text, is_private/is_pinned/is_read boolean NOT NULL
-- users: id numeric, domain text, ...
-- 脚本用途: 用纯 SQL 复刻 ListAPI(FilterAnswered+PrivateStatus=1+PinFirst, 每页20)的
-- 复合键集翻页,判定「DB 层数据自洽性」+「时区解释是否与线上游标一致」。
-- 覆盖范围: 只读查询 + 会话级临时表,无任何持久写。可放心在生产执行。
-- 使用: psql/pgAdmin/DBeaver 整段执行;若当前库 schema 不是 nekobox,改开头 SET 一行。
-- 判读: 见文件末尾说明;跑完把「查询结果 + NOTICE」整体贴回。
-- ============================================================================
-- ============ 0) 切换 schema(关键:默认 search_path 只有 public,所以之前找不到表) ============
SET search_path TO nekobox, public;
SELECT '连接确认' AS step,
current_database() AS db,
current_schema() AS effective_schema, -- 应显示 nekobox
current_setting('TimeZone') AS session_tz;
-- ============================================================================
-- PART A 环境 / 结构 / 目标集合核对
-- ============================================================================
SELECT 'PART A: 环境与结构核对' AS step;
-- 1) questions 关键列定义(确认 id 为 numeric、created_at 无时区精度6)
SELECT column_name, data_type, datetime_precision, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'nekobox' AND 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 相关索引(确认是否命中 idx_questions_user_private_created 复合索引)
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'nekobox' AND tablename = 'questions'
AND indexdef ILIKE '%user_id%';
-- 3) aoi 箱主定位 + 集合核对(answered_public_all 应=线上 API 总数,曾为 38)
SELECT u.id AS aoi_user_id,
u.domain,
count(*) FILTER (WHERE q.answer IS NOT NULL AND q.answer <> '' AND q.is_private = false)
AS answered_public_all,
count(*) FILTER (WHERE q.answer IS NOT NULL AND q.answer <> '' AND q.is_private = false AND q.is_pinned)
AS answered_public_pinned,
count(*) AS all_questions_rows
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) 该集合最新 6 条(含被删/私密剔除样本),关键看 created_at 的「墙钟」写法
-- -> 与线上 API JSON 的 created_at(如 2026-09-05T00:49:38.648502Z) 逐位比对:
-- 若 DB 墙钟==线上 Z 时间文本 => 读写按 UTC 对称(无时区偏移)
-- 若 DB 墙钟比线上 Z 大 8 小时 => 时区偏移实锤(上海墙钟被当 UTC / 反之)
SELECT q.id,
q.is_pinned,
q.created_at AS db_wall_clock, -- 无时区列原值
to_char(q.created_at, 'YYYY-MM-DD"T"HH24:MI:SS.US') AS db_wall_text,
(extract(epoch FROM q.created_at) * 1000000000)::numeric(30,0) AS nano_if_wall_is_utc,
q.answer <> '' AS has_answer,
q.is_private AS is_private,
q.deleted_at IS NOT NULL AS is_deleted
FROM questions q
WHERE q.user_id = (SELECT id FROM users WHERE domain = 'aoi')
ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC
LIMIT 6;
-- 5) 是否存在 created_at 完全相同的行(同刻多行若缺 id tiebreaker 会导致翻页跳行/重复)
SELECT q.created_at, count(*) AS n
FROM questions q
WHERE q.user_id = (SELECT id FROM users WHERE domain = 'aoi')
AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false
GROUP BY q.created_at
HAVING count(*) > 1
ORDER BY n DESC
LIMIT 10;
-- ============================================================================
-- PART B 纯 SQL 复刻 ListAPI 复合键集翻页(核心复现,DB 层数据自洽性判定)
-- 游标锚点直接用 DB 行自身的 created_at(墙钟) 值推进——不经过任何 Go/时区转换,
-- 专门回答: 「库里的数据 + 墙钟 keyset 排序」本身能否正确翻页、不重不漏。
-- ============================================================================
SELECT 'PART B: keyset 翻页复现' AS step;
DROP TABLE IF EXISTS pg_temp.diag_pages;
DO $$
DECLARE
v_uid numeric; -- users.id 也是 numeric
v_page int := 0;
v_rows int;
v_ids text;
v_prev_ids text := '';
v_pin boolean; -- 上一页末条锚点
v_ts timestamp; -- 上一页末条锚点(用 timestamp 变量,避免任何时区换算)
v_id numeric;
v_last_pin boolean;
v_last_ts timestamp;
v_last_id numeric;
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 IF NOT EXISTS diag_pages (page_no int, n_rows int, ids text);
DELETE FROM diag_pages;
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.deleted_at IS NULL
AND q.answer IS NOT NULL AND q.answer <> ''
AND q.is_private = false
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;
IF v_ids = v_prev_ids THEN
RAISE NOTICE '!!! 异常: 第 % 页与第 % 页完全重复,游标未前进(正是线上 bug 症状)', v_page, v_page - 1;
EXIT;
END IF;
v_prev_ids := v_ids;
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 diag_pages ORDER BY page_no;
-- 跨页重复检测: 无重复时只返回上面提示
SELECT '跨页重复检测: 若下方无第二张表结果则无重复' AS check_msg;
SELECT unnest(string_to_array(ids, ','))::numeric AS duplicated_question_id
FROM diag_pages
GROUP BY 1
HAVING count(*) > 1;
-- ============================================================================
-- PART C 时区/游标指纹核对 —— 区分「部署版本旧」vs「无时区列+时区解释错位」
-- 线上游标形如 0.1788566476678885000.3089(= pin=0, nano=1788566476678885000, id=3089)
-- 判读: 下面第 1 条若能精确命中 DB 行(3089) => 服务端按「DB墙钟==UTC」生成游标,
-- 与库值一致 => 无时区错位,问题指向部署版本/API 层;
-- 若命中不了 => nano 与实际库值对不上,时区解释有偏差。
-- 用法: 把 nano 数字换成你线上抓到的 next_cursor 中间段;id 换成对应行。
-- ============================================================================
SELECT 'PART C: 时区/游标指纹核对' AS step;
-- 1) nano 反向推导: 假定「墙钟==UTC」得到的 timestamp,是否正好等于 DB 中 3089 行的 created_at?
WITH c(nano) AS (VALUES (1788566476678885000::numeric)) -- <-- 替换为线上 next_cursor 中间段
SELECT c.nano,
(to_timestamp(c.nano / 1000000000.0) AT TIME ZONE 'UTC')::timestamp(6) AS derived_ts_if_wall_is_utc,
(SELECT q.created_at FROM questions q WHERE q.id = 3089) AS actual_db_ts_of_id_3089,
((to_timestamp(c.nano / 1000000000.0) AT TIME ZONE 'UTC')::timestamp(6)
= (SELECT q.created_at FROM questions q WHERE q.id = 3089)) AS match_3089
FROM c;
-- 2) 命中检测(通用版): nano 推导出的墙钟时刻在库中对应哪些行(正常应含线上游标那行)
WITH c(nano) AS (VALUES (1788566476678885000::numeric)) -- <-- 同上
SELECT q.id, q.created_at
FROM questions q, c
WHERE q.user_id = (SELECT id FROM users WHERE domain = 'aoi')
AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false
AND q.created_at = (to_timestamp(c.nano / 1000000000.0) AT TIME ZONE 'UTC')::timestamp(6)
ORDER BY q.id;
-- 3) 对照: 库中是否存在「把线上 JSON 的 Z 时刻原样解释为墙钟」对应的行
-- 即验证: 应用写库时是否把 UTC 时刻的墙钟数字直接存入无时区列(读写对称、无偏移)
SELECT q.id, q.created_at
FROM questions q
WHERE q.user_id = (SELECT id FROM users WHERE domain = 'aoi')
AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false
AND q.created_at IN ('2026-09-05 00:49:38.648502', '2026-09-05 00:01:16.678885') -- <-- 替换为线上首页首尾条的 created_at 文本
ORDER BY q.created_at DESC, q.id DESC;
-- ============================================================================
-- 判读指引
-- 1) PART B 第1页 id 应=线上首页20条;第2页应翻出剩余且无重复(DB 层自洽)
-- 2) PART B 若出现「第N页与第N-1页完全重复」=> 直接用 SQL 在 DB 层复现了 bug -> 数据/排序问题
-- 3) PART C.1 match_3089=true 且 C.3 能命中 => 读写按 UTC 对称,无时区错位,
-- DB keyset 正常时问题锁定在【部署版本/API层】;反之 => 无时区列+时区解释错位
-- 4) PART A.4 直接目视: DB 墙钟 vs 线上 JSON 的 Z 文本,差 8 小时即时区偏移实锤
-- ============================================================================