diff --git a/assets/style.css b/assets/style.css
index dea9d37..84395ab 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -150,6 +150,75 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
/* ---------- 页头 ---------- */
.page-head{display:flex; align-items:flex-start; gap:16px; margin:6px 0 18px}
.page-head .btn{margin-left:auto; flex:none; margin-top:4px}
+
+/* ---------- 首页:轻反馈介绍页 ----------
+ * 用户 2026-11-03:首页不列问题,只做介绍 + 指路。
+ * 因为访客总是从某个软件过来的,他要的是「怎么进自己那个软件的反馈」,
+ * 而不是看别人的问题列表。 */
+.home{max-width:820px; margin:0 auto; padding:8px 0 40px}
+.home-hero{text-align:center; padding:26px 0 30px}
+.home-hero h1{
+ font-size:30px; line-height:1.3; margin:0 0 12px;
+ letter-spacing:-.4px;
+}
+.home-lead{
+ font-size:15.5px; line-height:1.75; color:var(--muted);
+ margin:0 auto; max-width:600px;
+}
+
+.home-feats{
+ display:grid; grid-template-columns:repeat(2, minmax(0,1fr));
+ gap:12px; margin:0 0 34px;
+}
+@media (max-width:600px){ .home-feats{grid-template-columns:1fr} }
+.home-feat{
+ background:var(--card); border:1px solid var(--line);
+ border-radius:12px; padding:16px 18px;
+}
+.home-feat-ic{font-size:20px; line-height:1; margin-bottom:8px}
+.home-feat h3{margin:0 0 6px; font-size:15px; font-weight:700}
+.home-feat p{margin:0; font-size:13.5px; line-height:1.7; color:var(--muted)}
+
+.home-enter{
+ background:var(--card); border:1px solid var(--line);
+ border-radius:14px; padding:22px 24px;
+}
+.home-enter h2{margin:0 0 10px; font-size:17px}
+.home-enter-tip{
+ font-size:14px; color:var(--muted); line-height:1.8; margin:0 0 16px;
+ display:flex; align-items:center; flex-wrap:wrap; gap:6px;
+}
+.home-enter-tip b{color:var(--fg); font-weight:600}
+/* 小号三横线示意,让「右上角那个设置按钮」有具体形象可对照 */
+.home-menu-demo{
+ display:inline-flex; flex-direction:column; justify-content:center; gap:2.5px;
+ width:22px; height:22px; padding:4px; box-sizing:border-box;
+ border:1px solid var(--line-strong); border-radius:6px;
+ background:var(--bg, #f6f8fb);
+}
+.home-menu-demo i{display:block; width:100%; height:1.5px; background:var(--fg); border-radius:2px}
+
+.home-sw-list{display:flex; flex-direction:column; gap:8px}
+.home-sw{
+ display:flex; align-items:center; gap:12px;
+ padding:11px 14px; border-radius:11px;
+ border:1px solid var(--line); background:var(--card);
+ color:var(--fg);
+}
+.home-sw:hover{
+ text-decoration:none; color:var(--fg);
+ border-color:var(--swc); background:var(--bg, #f6f8fb);
+}
+.home-sw-ic{
+ width:36px; height:36px; flex:none; border-radius:9px;
+ display:flex; align-items:center; justify-content:center;
+ font-size:22px; line-height:1;
+}
+.home-sw-ic img{width:36px; height:36px; object-fit:contain; display:block}
+.home-sw-nm{font-size:15px; font-weight:600; color:var(--swc)}
+.home-sw-go{margin-left:auto; font-size:13px; color:var(--muted); flex:none}
+.home-sw:hover .home-sw-go{color:var(--swc)}
+.home-enter-alt{margin:12px 0 0}
.page-head h1{margin-bottom:4px}
.page-head .muted{font-size:14px; margin:0}
@@ -386,6 +455,17 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
.item-meta{display:flex; flex-wrap:wrap; align-items:center; gap:6px; font-size:13px; color:var(--muted)}
.item-meta .who{color:#3a424d; font-weight:600}
+/* 提交人凭 token 看自己未公开反馈时的提示条 */
+.owner-note{
+ display:flex; flex-direction:column; gap:4px;
+ margin:0 0 16px; padding:12px 14px;
+ background:var(--warn-soft, #fff8e6);
+ border:1px solid #f0d9a8; border-left:3px solid #d99b1f;
+ border-radius:10px; font-size:13.5px; line-height:1.65;
+ color:#6b5313;
+}
+.owner-note b{font-size:14px; color:#5a4610}
+
/* ---------- 后台「待审核」列表 ----------
* 列表式:一屏能看多条,右侧固定一个「通过并公开」。
* 详情/关闭说明留在管理页里填,列表页不塞表单。 */
diff --git a/index.php b/index.php
index 51c610c..66d2f97 100644
--- a/index.php
+++ b/index.php
@@ -1,22 +1,44 @@
$s
+
+
+ 轻量级反馈系统
+ 给每个软件一个独立的反馈入口:用户不用注册,填个昵称和邮箱就能提问题;
+ 所有进展都会自动发邮件通知他。
+
+
+
+
+
📮
+
不用注册
+
填昵称和常用邮箱即可提交。后续每一条进展都会发邮件通知,不用反复回来刷。
+
+
+
🔗
+
私人追踪链接
+
提交后会收到一条专属链接,凭它随时查看进度——哪怕这条反馈还没审核通过。
+
+
+
🗂
+
按软件分开
+
每个软件一套独立的问题列表和分类,互不干扰。同一软件的其他人也能看到已解决的问题。
+
+
+
💬
+
公开讨论
+
已公开的问题任何人都能查看和回复。提交前先搜一下,避免重复提同样的问题。
+
+
+
+
+ 进入某个软件的反馈系统
+
+ 管理员还没有配置软件。
+
+
+ 点击右上角的
+ 设置按钮,从列表里选择您要反馈的软件即可。
+
+
+
+ 也可以直接点上方的软件卡片进入。
+
+
+
+
+ exec('CREATE INDEX IF NOT EXISTS idx_threads_updated ON threads(updated_at DESC)');
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_threads_cat ON threads(category)');
+ /* ---- threads.track_token:提交人的「私人追踪钥匙」 ----
+ * 因为本站不做用户登录,没办法认人;但每个人都必须能跟踪自己提交的反馈,
+ * 哪怕是还没审核通过的。做法:提交时生成一串随机 token 只发到他的邮箱,
+ * 链接带 ?t= 就能看 / 能回复,别人不知道这串东西就看不到。
+ *
+ * 🔴 不要用 id 代替:id 是自增的,任何人改个数字就能翻别人的私有反馈。
+ * token 必须是不可猜的随机值(这里用 32 位 hex = 128 位熵)。 */
+ $thCols = $pdo->query('PRAGMA table_info(threads)')->fetchAll();
+ $thNames = [];
+ foreach ($thCols as $c4) { if (isset($c4['name'])) { $thNames[(string)$c4['name']] = true; } }
+ if (!isset($thNames['track_token'])) {
+ $pdo->exec("ALTER TABLE threads ADD COLUMN track_token TEXT NOT NULL DEFAULT ''");
+ }
+ // 存量回填:给还没有 token 的帖子补一个,保证老数据的邮件链接也能用
+ $need = $pdo->query("SELECT id FROM threads WHERE track_token IS NULL OR track_token = ''")->fetchAll();
+ if ($need) {
+ $upd = $pdo->prepare('UPDATE threads SET track_token = ? WHERE id = ?');
+ foreach ($need as $r) { $upd->execute([bin2hex(random_bytes(16)), (int)$r['id']]); }
+ }
+ $pdo->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_threads_token ON threads(track_token)');
+
$pdo->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS replies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
diff --git a/lib/layout.php b/lib/layout.php
index 33f6fef..9d34701 100644
--- a/lib/layout.php
+++ b/lib/layout.php
@@ -83,9 +83,15 @@ function fb_layout_head($title, array $opt = []) {
- 全部问题
+
+ 问题列表
+
管理后台
- 我要反馈
+ 我要反馈
1): ?>
[r,g,b],非法返回 null */
function fb_hex_to_rgb($hex) {
$h = ltrim((string)$hex, '#');
- if (strlen($h) === 3) { $h = $h[0].$h[0].$h[1].$h[1].$h[2].$h[2]; }
+ // 三位简写只接受纯十六进制字符(#abc -> #aabbcc);
+ // 先做字符校验再展开,否则 "bad" 这种非色值会被当成 #bbaadd
+ if (strlen($h) === 3 && preg_match('/^[0-9a-fA-F]{3}$/', $h)) {
+ $h = $h[0].$h[0].$h[1].$h[1].$h[2].$h[2];
+ }
if (!preg_match('/^[0-9a-fA-F]{6}$/', $h)) { return null; }
return [hexdec(substr($h, 0, 2)), hexdec(substr($h, 2, 2)), hexdec(substr($h, 4, 2))];
}
@@ -628,6 +632,78 @@ function fb_thread_public(array $thread) {
return fb_is_admin();
}
+/**
+ * 生成一个新的追踪 token(提交反馈时调用)。
+ * 32 位 hex = 128 位熵,猜不出来;这个值只存在自己库里、只发给提问人邮箱。
+ */
+function fb_track_new_token() {
+ return bin2hex(random_bytes(16));
+}
+
+/** 这个请求带来的 token 是否与帖子的匹配(常数时间比较,防时序探测) */
+function fb_track_token_ok(array $thread, $given) {
+ $given = trim((string)$given);
+ if ($given === '') { return false; }
+ $real = isset($thread['track_token']) ? trim((string)$thread['track_token']) : '';
+ if ($real === '') { return false; }
+ return hash_equals($real, $given);
+}
+
+/**
+ * 当前访问者是否「有权跟踪」这条帖子 —— 即带对了 token。
+ * 审核后台的管理员也算有权(不然审核时看不了)。
+ */
+function fb_thread_trackable(array $thread, $given = null) {
+ if ($given === null) { $given = isset($_GET['t']) ? (string)$_GET['t'] : ''; }
+ if (fb_track_token_ok($thread, $given)) { return true; }
+ return fb_is_admin();
+}
+
+/**
+ * 当前访问者能否【查看】这条帖子。
+ *
+ * 公开的(visible=1)所有人可看;
+ * 未公开的只有两种人能看:审核后台管理员、拿着正确 token 的提交人。
+ * 这就是「不登录也能跟踪自己那条」的实现方式,不靠任何登录态。
+ */
+function fb_thread_viewable(array $thread, $given = null) {
+ if (fb_thread_public($thread)) { return true; }
+ return fb_thread_trackable($thread, $given);
+}
+
+/**
+ * 回「某个软件的问题列表」的链接。
+ *
+ * 首页现在是介绍页,不带 sw 的回首页等于把用户扔回介绍页 ——
+ * 从详情/提交页点「返回问题列表」应该回到**当前软件**的列表。
+ */
+function fb_software_list_url($sw) {
+ if (!is_array($sw) || empty($sw['slug'])) { return 'index.php'; }
+ return 'index.php?sw=' . urlencode((string)$sw['slug']);
+}
+
+/**
+ * 这条帖子的对外链接。
+ *
+ * 🔴 用 token 不用 id:
+ * - 邮件里发给提问人的链接一律带 ?t=,他才能看未公开的那条;
+ * - 公开列表里给所有人看的链接照旧用 id(公开内容本来就谁都能看)。
+ * 早期所有链接都是 thread.php?id=N,导致「未审核就点不进去」——
+ * id 是自增的,用它当权限等于没有权限。
+ *
+ * @param array $thread 帖子行
+ * @param bool $personal true = 带 token 的私人链接;false = 公开链接
+ */
+function fb_thread_link(array $thread, $personal = false) {
+ $id = (int)(isset($thread['id']) ? $thread['id'] : 0);
+ $url = fb_base_url() . '/thread.php?id=' . $id;
+ if ($personal) {
+ $tok = isset($thread['track_token']) ? trim((string)$thread['track_token']) : '';
+ if ($tok !== '') { $url .= '&t=' . rawurlencode($tok); }
+ }
+ return $url;
+}
+
/* ---------------- 安装状态 ---------------- */
/** FeedLite 开源仓库地址(页脚版权链接的默认值,可在后台改) */
diff --git a/new.php b/new.php
index 1340971..ce76bd9 100644
--- a/new.php
+++ b/new.php
@@ -8,7 +8,11 @@ $pdo = fb_db();
$err = '';
$atts = []; // 校验失败时保留已上传的附件
-/* ---- 软件:URL 带 sw 就用它,否则落到默认软件 ---- */
+/* ---- 软件:URL 带 sw 就用它 ----
+ * 🔴 不再「没带 sw 就默认落到第一个软件」:
+ * 首页现在是介绍页,直接访问 new.php 没有软件上下文,
+ * 默默替他选一个软件会让反馈进错地方、发错通知邮件。
+ * 没有软件时给一句引导,让用户先选软件。 */
$swList = fb_softwares(true);
$sw = null;
$swKey = isset($_GET['sw']) ? (string)$_GET['sw'] : '';
@@ -16,8 +20,32 @@ if ($swKey !== '') {
$sw = fb_software($swKey);
if (!$sw || (int)$sw['enabled'] !== 1) { $sw = null; }
}
-if ($sw === null && $swList) { $sw = $swList[0]; }
-$swId = $sw ? (int)$sw['id'] : 0;
+if ($sw === null) {
+ fb_layout_head('请先选择软件', ['robots' => 'noindex,follow']);
+ ?>
+
+
请先选择要给哪个软件提反馈
+
不同软件的问题列表是分开的,请先选一个。
+
+
返回首页
+
+ prepare('INSERT INTO threads(title, body, category, category_id, software_id, author_name, author_email, author_phone, status, visible, reply_count, created_at, updated_at, ip_hash) VALUES(?,?,?,?,?,?,?,?,?,?,0,?,?,?)');
+ /* track_token:提交人的私人追踪钥匙,只发到他的邮箱。
+ * 有了它,即使这条还没审核通过,他也能凭链接看到进度。 */
+ $trackToken = fb_track_new_token();
+ $ins = $pdo->prepare('INSERT INTO threads(title, body, category, category_id, software_id, author_name, author_email, author_phone, status, visible, reply_count, created_at, updated_at, ip_hash, track_token) VALUES(?,?,?,?,?,?,?,?,?,?,0,?,?,?,?)');
$ins->execute([
$form['title'], $form['body'],
$curCat ? (string)$curCat['slug'] : 'other', // 老列留个值,便于回退与检索
@@ -105,7 +136,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$swId > 0 ? $swId : null,
$form['name'], $form['email'],
$form['phone'] !== '' ? $form['phone'] : null,
- $initStatus, $visible, $now, $now, fb_ip_hash(),
+ $initStatus, $visible, $now, $now, fb_ip_hash(), $trackToken,
]);
$newId = (int)$pdo->lastInsertId();
fb_attach_save($pdo, $newId, null, $vList);
@@ -138,7 +169,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 发信失败绝不能让用户提交失败 —— 反馈已经存库了
}
- fb_redirect('thread.php?id=' . $newId . '&submitted=1');
+ /* 提交完成后直接带 token 跳到详情页:
+ * 这样用户当场就能看到自己刚提交的那条(哪怕还没审核),
+ * 而不是只看到一个「等待审核」的静态提示页。 */
+ fb_redirect('thread.php?id=' . $newId
+ . '&t=' . rawurlencode($trackToken)
+ . '&submitted=1');
}
}
}
@@ -146,7 +182,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]);
?>
-
+
我要反馈
diff --git a/thread.php b/thread.php
index ee36629..0c7f209 100644
--- a/thread.php
+++ b/thread.php
@@ -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 '';
+ echo '';
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)) {
. '您的反馈已收到(编号 #' . (int)$thread['id'] . '),我们会先确认它是否为应用问题:
'
. '· 确认为问题 —— 审核通过后会公开显示,其他人也能查看与回复;
'
. '· 确认不是问题 —— 我们会关闭它,并把结论与说明发送到您留下的邮箱。
'
- . '审核通常在 1~2 个工作日内完成,请留意邮箱。
'
- . '返回问题列表
'
+ . '审核通常在 1~2 个工作日内完成。'
+ . '我们会发一封邮件给您,里面的链接可以随时查看这条反馈的处理进度。
'
+ . '返回问题列表
'
. '';
fb_layout_foot();
exit;
}
http_response_code(404);
fb_layout_head('问题不存在');
- echo '';
+ echo '';
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 '';
+ echo '';
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]);
?>
-
+
+
+
+
+
+ 这是您的私人跟踪链接
+ 这条反馈还没通过审核,其他人看不到。请保留本页链接(或邮件里的链接),
+ 审核结果与后续进展都能在这里查看。
+
+
@@ -206,8 +240,11 @@ fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);