Files
FeedLite/thread.php
T
laoyang cede838b5e FeedLite 初始提交:通用轻量级反馈系统(PHP + SQLite,零依赖)
- 前台:问题列表 / 详情 / 发布与回复(趣味问答验证码、蜜罐、IP 限流)
- 后台:先审后显、站点设置(站点标题/描述、附件存储、OSS 参数)、操作日志
- 附件:关闭 / 本机硬盘 / 阿里云 OSS 浏览器直传(ECS 实例 RAM 角色)三种模式
- 品牌资源:全套图标(favicon.ico / SVG / PNG 各尺寸 / manifest)
- 安全:CSRF、session 加固、.htaccess 屏蔽敏感文件、口令仅存哈希
- 配置:config.example.php 为模板,config.php 不入库
2026-09-28 20:34:37 +08:00

262 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 问题详情 + 回复
*/
require __DIR__ . '/lib/bootstrap.php';
$pdo = fb_db();
$id = isset($_GET['id']) ? (int)$_GET['id'] : (isset($_POST['id']) ? (int)$_POST['id'] : 0);
if ($id <= 0) { fb_redirect('index.php'); }
$st = $pdo->prepare('SELECT * FROM threads WHERE id = ?');
$st->execute([$id]);
$thread = $st->fetch();
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>';
fb_layout_foot();
exit;
}
// 审核(先审后显):未通过审核的问题对访客不可见
if (!fb_thread_public($thread)) {
$justSubmitted = (isset($_GET['submitted']) && (string)$_GET['submitted'] === '1');
if ($justSubmitted) {
fb_layout_head('提交成功', ['robots' => 'noindex,nofollow']);
echo '<div class="empty">'
. '<h2 style="margin:0 0 10px">提交成功,等待审核</h2>'
. '<p>你的反馈已收到(编号 #' . (int)$thread['id'] . '),我们会先确认它是否为应用问题:</p>'
. '<p>· 确认为问题 —— 审核通过后会公开显示,其他人也能查看与回复;<br>'
. '· 确认不是问题 —— 我们会关闭它,并把结论与说明发送到你留下的邮箱。</p>'
. '<p class="muted">审核通常在 1~2 个工作日内完成,请留意邮箱。</p>'
. '<p><a class="btn primary" href="index.php">返回问题列表</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>';
fb_layout_foot();
exit;
}
$statuses = fb_statuses();
$err = '';
$atts = [];
$form = ['name' => '', 'email' => '', 'body' => '', 'captcha' => ''];
/* ---------------- 处理回复提交 ---------------- */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 审核中(未公开)的问题不允许回复
if (!fb_thread_public($thread)) {
http_response_code(403);
fb_layout_head('暂不可回复');
echo '<div class="empty"><p>该问题还在审核中,暂时不能回复。</p><p><a class="btn primary" href="index.php">返回问题列表</a></p></div>';
fb_layout_foot();
exit;
}
$form['name'] = trim(isset($_POST['name']) ? (string)$_POST['name'] : '');
$form['email'] = trim(isset($_POST['email']) ? (string)$_POST['email'] : '');
$form['body'] = trim(isset($_POST['body']) ? (string)$_POST['body'] : '');
$capInput = isset($_POST['captcha']) ? (string)$_POST['captcha'] : '';
$adminPw = isset($_POST['admin_password']) ? (string)$_POST['admin_password'] : '';
$hp = trim(isset($_POST['website']) ? (string)$_POST['website'] : '');
$t0 = isset($_POST['t0']) ? (int)$_POST['t0'] : 0;
list($attOk, $attList, $attErr) = fb_attach_parse(isset($_POST['attachments']) ? $_POST['attachments'] : '');
$isAdmin = ($adminPw !== '' && fb_admin_verify_password($adminPw));
if (!fb_csrf_ok(isset($_POST['csrf']) ? $_POST['csrf'] : '')) {
$err = '会话已过期,请重新提交。';
} elseif ($hp !== '') {
$err = '提交未通过校验,请重试。';
} elseif ($t0 > 0 && (time() - $t0) < max(0, (int)fb_cfg('post_min_seconds', 3))) {
$err = '提交过于频繁,请稍候再试。';
} elseif (!fb_valid_name($form['name']) && !$isAdmin) {
$err = '请填写昵称(1~24 个字符)。';
} elseif (!$isAdmin && !filter_var($form['email'], FILTER_VALIDATE_EMAIL)) {
$err = '请填写有效的邮箱地址(用于接收回复通知)。';
} elseif (!fb_valid_body($form['body'])) {
$err = '回复内容请填写 2~5000 个字符。';
} elseif (!$attOk) {
$err = $attErr;
} elseif (!fb_rate_check('reply', (int)fb_cfg('rate_limit.reply_per_hour', 20))) {
$err = '回复过于频繁,请稍后再试。';
} else {
list($vOk, $vList, $vErr) = fb_attach_verify($attList);
if (!$vOk) {
$err = $vErr;
$atts = $attList;
} elseif (!fb_attach_cap_ok($pdo, $vList)) {
$err = '附件存储空间已满,请减少附件后重试。';
$atts = $vList;
} elseif (!fb_captcha_check($capInput)) {
$err = '人机验证未通过,请重新作答。';
$atts = $vList;
} else {
$now = time();
$replyName = $isAdmin ? '官方' : $form['name'];
$replyMail = $isAdmin ? null : $form['email'];
$ins = $pdo->prepare('INSERT INTO replies(thread_id, body, author_name, author_email, is_admin, created_at, ip_hash) VALUES(?,?,?,?,?,?,?)');
$ins->execute([$id, $form['body'], $replyName, $replyMail, $isAdmin ? 1 : 0, $now, fb_ip_hash()]);
$replyId = (int)$pdo->lastInsertId();
fb_attach_save($pdo, $id, $replyId, $vList);
// 更新主题:回复数 +1、活跃时间;管理员可同时调整状态
$newStatus = null;
if ($isAdmin && isset($_POST['status']) && isset($statuses[(string)$_POST['status']])) {
$newStatus = (string)$_POST['status'];
}
if ($newStatus !== null) {
$upd = $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ?, status = ? WHERE id = ?');
$upd->execute([$now, $newStatus, $id]);
} else {
$upd = $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?');
$upd->execute([$now, $id]);
}
fb_rate_hit('reply');
// 发送通知邮件给发帖人
$replyRow = [
'id' => $replyId,
'body' => $form['body'],
'author_name' => $replyName,
'author_email' => $replyMail,
'is_admin' => $isAdmin ? 1 : 0,
];
fb_notify_reply($pdo, $thread, $replyRow, $isAdmin);
fb_redirect('thread.php?id=' . $id . '#r' . $replyId);
}
}
}
/* ---------------- 读取数据 ---------------- */
$st = $pdo->prepare('SELECT id, body, author_name, is_admin, created_at FROM replies WHERE thread_id = ? ORDER BY created_at ASC, id ASC');
$st->execute([$id]);
$replies = $st->fetchAll();
// 重新读取主题(可能在 POST 中更新过状态)
$st = $pdo->prepare('SELECT * FROM threads WHERE id = ?');
$st->execute([$id]);
$thread = $st->fetch();
// 附件(主题级 + 各回复)
$attMap = fb_attach_for_thread($pdo, $id);
fb_layout_head(fb_excerpt($thread['title'], 50));
?>
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
<article class="subject">
<div class="item-top">
<span class="badge cat"><?php echo e(fb_category_label($thread['category'])); ?></span>
<span class="badge st st-<?php echo e($thread['status']); ?>"><?php echo e(fb_status_label($thread['status'])); ?></span>
<h1 class="subject-title"><?php echo e($thread['title']); ?></h1>
<span class="item-id" title="问题编号">#<?php echo (int)$id; ?></span>
</div>
<div class="item-meta">
<span class="who"><?php echo e($thread['author_name']); ?></span>
<span class="dot">·</span>
<span><?php echo e(fb_fmt_time($thread['created_at'])); ?></span>
<span class="dot">·</span>
<span><?php echo (int)$thread['reply_count']; ?> 条回复</span>
</div>
<div class="subject-body"><?php echo nl2br(e($thread['body'])); ?></div>
<?php echo fb_attach_render($attMap['main']); ?>
</article>
<section class="replies">
<h2>全部回复(<?php echo count($replies); ?>)</h2>
<?php if (!$replies): ?>
<div class="empty small"><p>还没有回复,欢迎在下方参与讨论。</p></div>
<?php else: ?>
<?php foreach ($replies as $r): ?>
<div class="reply <?php echo $r['is_admin'] ? 'admin' : ''; ?>" id="r<?php echo (int)$r['id']; ?>">
<div class="reply-head">
<span class="who"><?php echo e($r['is_admin'] ? '官方' : $r['author_name']); ?></span>
<?php if ($r['is_admin']): ?><span class="badge official">官方答复</span><?php endif; ?>
<span class="dot">·</span>
<span class="time"><?php echo e(fb_fmt_time($r['created_at'])); ?></span>
</div>
<div class="reply-body"><?php echo nl2br(e($r['body'])); ?></div>
<?php
$rid = (int)$r['id'];
if (!empty($attMap['replies'][$rid])) {
echo fb_attach_render($attMap['replies'][$rid], ['compact' => true]);
}
?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</section>
<section class="reply-form">
<h2>我要回复</h2>
<?php fb_flash_box($err, 'error'); ?>
<form method="post" action="thread.php?id=<?php echo (int)$id; ?>">
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
<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">
<label>请勿填写此栏<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
</div>
<div class="row2">
<label class="field">
<span>昵称 <b class="req">*</b></span>
<input type="text" name="name" maxlength="24" value="<?php echo e($form['name']); ?>" placeholder="怎么称呼你" required>
</label>
<label class="field" id="emailField">
<span>邮箱 <b class="req">*</b><i class="hint">仅用于接收回复通知,不会公开</i></span>
<input type="email" name="email" maxlength="120" value="<?php echo e($form['email']); ?>" placeholder="you@example.com" required>
</label>
</div>
<label class="field">
<span>回复内容 <b class="req">*</b></span>
<textarea name="body" rows="6" maxlength="5000" placeholder="说说你的看法或补充…" required><?php echo e($form['body']); ?></textarea>
</label>
<?php fb_uploaders($atts); ?>
<div class="captcha">
<label class="field">
<span>人机验证 <b class="req">*</b> <i class="hint">防机器人,随手答一下</i></span>
<div class="captcha-line">
<span class="c-q"><?php echo e(fb_captcha_question()); ?></span>
<input type="text" name="captcha" value="" placeholder="答案" autocomplete="off" required>
<a class="reload" href="captcha.php" data-captcha-refresh>换一题</a>
</div>
</label>
</div>
<label class="field admin-pw">
<span>管理员口令 <i class="hint">选填;填写后将以「官方」身份回复</i></span>
<input type="password" name="admin_password" value="" autocomplete="off" placeholder="管理员专用">
</label>
<div class="field admin-status" hidden>
<span>处理状态 <i class="hint">仅管理员可见</i></span>
<select name="status">
<?php foreach ($statuses as $k => $v): ?>
<option value="<?php echo e($k); ?>" <?php echo $thread['status'] === $k ? 'selected' : ''; ?>><?php echo e($v); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-actions">
<button class="btn primary" type="submit">提交回复</button>
</div>
</form>
</section>
<?php
fb_layout_foot();