Files
FeedLite/thread.php
T
laoyang 738c6c09ff 多软件区分:邮件按软件换色+图标+软件名;补提交回执邮件
用户要求:多软件并存时,用「颜色 + 图标 + 名字」三重区分,
邮件也要跟着所属软件变,一眼能看出是哪款软件的来信。

1. software 表加 icon / download_url 字段(幂等迁移)
   - icon 存单个字符(emoji / 汉字 / 字母),不存图片路径:
     前端与邮件用同一个字符渲染,天然一致,不用再管一套上传与权限
   - 没配 icon 时取名首字兜底,任何地方都不出现空图标
   - 后台软件编辑加图标快选(12 个常用 emoji)

2. 邮件配色改为跟随软件(fb_mail_brand 取 fb_software_colors)
   - 头部色条、按钮、引用块色条、徽标全部跟着软件走
   - 邮件头部改为「图标色块 + 软件名」,下面再小字标站点名
   - 主题行统一加【软件名】前缀,不点开就能分辨

3. 新增 receipt 事件:提交后立即给提问人发「已收到、待审核、请耐心等待」
   - 独立开关 notify.receipt_enabled(与「回复通知」是两件事)
   - 新增 7 处调用前的静默 return 也一并写日志

4. reply 邮件补上下文:原提问摘要 + 本次回复,像博客评论通知
5. status=resolved 文案改为「已完成并上线」,配了下载地址时给下载按钮

验证:多软件配色/图标/前缀全部通过(含反向验证),XSS 转义通过,
空图标兜底通过;页面顶栏、页头、选择器图标渲染正确。
2026-11-03 08:00:50 +08:00

268 lines
12 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_status_keys();
$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);
$thSwForColor = !empty($thread['software_id']) ? fb_software((int)$thread['software_id']) : null;
fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
?>
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
<article class="subject">
<div class="item-top">
<?php
$thSw = !empty($thread['software_id']) ? fb_software((int)$thread['software_id']) : null;
$thCat = !empty($thread['category_id']) ? fb_category((int)$thread['category_id']) : null;
?>
<?php if ($thSw): ?><span class="badge sw"><?php echo fb_software_badge($thSw, 'sm'); ?></span><?php endif; ?>
<?php if ($thCat): ?><span class="badge cat"><?php echo e((string)$thCat['name']); ?></span><?php endif; ?>
<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();