首页改介绍页;列表只服务当前软件;新增UUID追踪让提交人看得到未审核反馈

用户提的两个核心问题:

一、首页不该列全部问题
  场景:这套系统是给某个软件配反馈入口、把地址贴进那软件的「意见反馈」里。
  访客总是从某个软件过来的,看别的软件的问题对他没意义。
  ⇒ / 不带 sw 时渲染介绍页(特点介绍 + 「点右上角设置按钮选软件」+ 软件卡片)
     带 sw 才是该软件的问题列表,检索/筛选/分类全在本软件范围内

二、提交后未审核,用户点不进去(真实缺陷)
  邮件里给了「点击查看您刚才提交的反馈」,但 thread.php 只在 visible=1 时
  才给看,未审核的一律 404 / 只看到「等待审核」静态页 —— 链接等于白给。
  ⇒ threads 加 track_token(32位hex = 128位熵),提交时生成:
     - 邮件里所有链接一律带 ?t=<token>(不再是裸 id)
     - 带对 token:可看完整详情、可回复,哪怕未公开/已关闭
     - 没 token 或 token 错:对外一律 404,不泄露存在性
     - 公开列表仍用 id 链接(公开内容谁都能看,用于避免重复提交)

实现细节:
- threads.track_token + 唯一索引;幂等迁移,存量数据自动回填
- fb_track_new_token / fb_track_token_ok(hash_equals 防时序探测)
  / fb_thread_viewable / fb_thread_link(thread, personal)
- thread.php 权限从 fb_thread_public 改为 fb_thread_viewable
- 带 token 看自己未公开的反馈时,顶部给「私人跟踪链接」提示条
- 回复表单与回复后跳转都保留 token,否则回复完就失去访问权
- 提交完成后直接带 token 跳详情页,不再只给静态等待页
- close 类邮件补上「查看这条反馈」按钮(原来完全没链接)
- 顶栏「全部问题」改为「问题列表」且只在选中软件后显示
- 详情/提交页「返回问题列表」按所属软件返回,不再回介绍页
- new.php 无 sw 时不再默认落到第一个软件,改为让用户先选
  (默默替选会让反馈进错软件、通知发错人)

验证:首页 0 条问题卡片且未泄露任何标题、三个软件入口齐全;
未公开#4 无 token 404 / 错 token 404 / 正确 token 200 且显示完整详情
(不再卡在「等待审核」)、提示条与表单 token 都在;
已公开#2 无 token 可看;5 类邮件(回执/通过/回复/状态/关闭)链接全带 token;
两套回归各 30+ 项断言全过。
This commit is contained in:
2026-11-03 09:54:12 +08:00
parent 2f28b1e3a9
commit 8bcee46b8a
9 changed files with 411 additions and 32 deletions
+49 -12
View File
@@ -11,16 +11,34 @@ if ($id <= 0) { fb_redirect('index.php'); }
$st = $pdo->prepare('SELECT * FROM threads WHERE id = ?');
$st->execute([$id]);
$thread = $st->fetch();
/* 这条帖子所属的软件 —— 「返回问题列表」要回到它的列表,
* 而不是回到首页(首页现在是介绍页)。 */
$backUrl = fb_software_list_url(
$thread && !empty($thread['software_id']) ? fb_software((int)$thread['software_id']) : null
);
if (!$thread) {
http_response_code(404);
fb_layout_head('问题不存在');
echo '<div class="empty"><p>该问题不存在或已被删除。</p><p><a class="btn primary" href="index.php">返回问题列表</a></p></div>';
echo '<div class="empty"><p>该问题不存在或已被删除。</p><p><a class="btn primary" href="' . e($backUrl) . '">返回问题列表</a></p></div>';
fb_layout_foot();
exit;
}
// 审核(先审后显):未通过审核的问题对访客不可见
if (!fb_thread_public($thread)) {
/* 审核(先审后显)+ 提交人追踪:
*
* 可见性三档 ——
* ① 已公开:所有人可看
* ② 未公开 + 带对 token:提交人本人可看(这是他的私人进度页)
* ③ 未公开 + 没 token:对外一律装作「不存在」,不泄露标题也不泄露状态
*
* 🔴 关键:带对 token 时**绝不能**再走「提交成功,等待审核」那个提示页。
* 用户之前就卡在这里 —— 邮件里给了链接让他看进度,点进来却只看到
* 「等待审核」,等于链接白给。带 token 进来就应该看到完整详情。 */
$hasToken = isset($_GET['t']) ? trim((string)$_GET['t']) : '';
$isOwnerView = $hasToken !== '' && fb_track_token_ok($thread, $hasToken);
if (!fb_thread_viewable($thread)) {
$justSubmitted = (isset($_GET['submitted']) && (string)$_GET['submitted'] === '1');
if ($justSubmitted) {
fb_layout_head('提交成功', ['robots' => 'noindex,nofollow']);
@@ -29,15 +47,16 @@ if (!fb_thread_public($thread)) {
. '<p>您的反馈已收到(编号 #' . (int)$thread['id'] . '),我们会先确认它是否为应用问题:</p>'
. '<p>· 确认为问题 —— 审核通过后会公开显示,其他人也能查看与回复;<br>'
. '· 确认不是问题 —— 我们会关闭它,并把结论与说明发送到您留下的邮箱。</p>'
. '<p class="muted">审核通常在 1~2 个工作日内完成,请留意邮箱。</p>'
. '<p><a class="btn primary" href="index.php">返回问题列表</a></p>'
. '<p class="muted">审核通常在 1~2 个工作日内完成。'
. '我们会发一封邮件给您,里面的链接可以随时查看这条反馈的处理进度。</p>'
. '<p><a class="btn primary" href="' . e($backUrl) . '">返回问题列表</a></p>'
. '</div>';
fb_layout_foot();
exit;
}
http_response_code(404);
fb_layout_head('问题不存在');
echo '<div class="empty"><p>该问题不存在或已被删除。</p><p><a class="btn primary" href="index.php">返回问题列表</a></p></div>';
echo '<div class="empty"><p>该问题不存在或已被删除。</p><p><a class="btn primary" href="' . e($backUrl) . '">返回问题列表</a></p></div>';
fb_layout_foot();
exit;
}
@@ -49,11 +68,12 @@ $form = ['name' => '', 'email' => '', 'body' => '', 'captcha' => ''];
/* ---------------- 处理回复提交 ---------------- */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 审核中(未公开)的问题不允许回复
if (!fb_thread_public($thread)) {
/* 未公开的帖子,只有带对 token 的提交人(或管理员)能回复。
* 别人连看都看不到,自然也不该能回。 */
if (!fb_thread_public($thread) && !$isOwnerView) {
http_response_code(403);
fb_layout_head('暂不可回复');
echo '<div class="empty"><p>该问题还在审核中,暂时不能回复。</p><p><a class="btn primary" href="index.php">返回问题列表</a></p></div>';
echo '<div class="empty"><p>该问题还在审核中,暂时不能回复。</p><p><a class="btn primary" href="' . e($backUrl) . '">返回问题列表</a></p></div>';
fb_layout_foot();
exit;
}
@@ -131,7 +151,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
];
fb_notify_reply($pdo, $thread, $replyRow, $isAdmin);
fb_redirect('thread.php?id=' . $id . '#r' . $replyId);
/* 回复后跳回去时要带上 token,否则提交人对未公开反馈的访问权会当场丢掉 */
fb_redirect('thread.php?id=' . $id
. ($isOwnerView ? '&t=' . rawurlencode($hasToken) : '')
. '#r' . $replyId);
}
}
}
@@ -153,7 +176,18 @@ $thSwForColor = !empty($thread['software_id']) ? fb_software((int)$thread['softw
fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
?>
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
<div class="crumbs"><a href="<?php echo e($backUrl); ?>">← 返回问题列表</a></div>
<?php if ($isOwnerView && !fb_thread_public($thread)): ?>
<?php /* 提交人凭 token 进来看到自己的未公开反馈时,要明确告诉他:
这条还没公开、别人看不到,但他可以一直用这个链接跟踪。
否则他会以为「既然能打开,是不是已经公开了」。 */ ?>
<div class="owner-note">
<b>这是您的私人跟踪链接</b>
<span>这条反馈还没通过审核,其他人看不到。请保留本页链接(或邮件里的链接),
审核结果与后续进展都能在这里查看。</span>
</div>
<?php endif; ?>
<article class="subject">
<div class="item-top">
@@ -206,8 +240,11 @@ fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
<section class="reply-form">
<h2>我要回复</h2>
<?php fb_flash_box($err, 'error'); ?>
<form method="post" action="thread.php?id=<?php echo (int)$id; ?>">
<?php /* 表单要带上 token:否则提交后跳转回不带 t 的地址,
提交人会立刻失去对自己未公开反馈的访问权,体验上就是「回复完就看不见了」。 */ ?>
<form method="post" action="thread.php?id=<?php echo (int)$id; ?><?php echo $isOwnerView ? '&amp;t=' . e(rawurlencode($hasToken)) : ''; ?>">
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
<input type="hidden" name="t" value="<?php echo e($isOwnerView ? $hasToken : ''); ?>">
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
<input type="hidden" name="t0" value="<?php echo time(); ?>">
<div class="hp-field" aria-hidden="true">