## 一、用户报「我明明配过 SMTP,怎么全丢了、信也发不出」
**没有重置数据库**(installed_at 仍是 11-01 15:14,oss.*/site_name/admin.username 全在)。
真因是我上一轮「邮件拆独立 tab」挖的坑:站点设置与邮件通知**共用 act=settings**,
而保存函数把 smtp 字段无条件写库:
foreach ($smtpText as $k) {
fb_set_setting($k, trim((string)$post($k, \x27\x27))); // 没提交 → 空串 → 覆盖
}
在站点设置页保存附件设置时,该表单没有 smtp_* 字段,$post() 取到空串,
于是 host/user/from 被清成空。smtp.pass 因为单独做了「留空=不修改」而幸存,
所以现场是 pass 有值、其余全空 —— 这个组合本身就是 bug 的指纹。
修法:新增 $has() 判断「本次请求是否真的提交了该字段」,没提交就跳过。
按 tab 分两组保护:
- $isSiteForm:站点信息/页脚/附件/OSS,逐字段 has() 双保险
- $isMailForm:SMTP/通知开关,整段 + 逐字段
checkbox 尤其要保护:未提交会被当成「取消勾选」,
在站点设置页存一次就会把「有人回复时通知」全关掉。
## 二、notify.admin_new_thread 是假的(用户说「提交了反馈收不到邮件」)
这个设置项:白名单里有、后台有勾选框,**但没有任何代码实现它**。
grep 只命中三处(白名单/保存/界面),没有一处真正发信。
已在 new.php 提交成功后实现:查新帖 → 取所属软件名 → fb_notify_event(\x27new\x27)。
## 三、补齐全部事件,统一入口
此前只有 reply 和 close 两个事件,且两套模板风格不一致。
新增 fb_notify_event() 统一入口,处理「开关→收件人→节流→发信→日志」。
覆盖 8 类事件:reply / status / approve / hide / close / new。
- status 按状态给不同文案(待处理/处理中/已解决/关闭/不予考虑),
「不予考虑」语气克制、必须带理由并留申诉入口
- approve / hide 原先完全不发信,现在补上
- 状态没变化时不发信(不打扰用户)
## 四、邮件模板重做
抽出 fb_mail_layout() 统一外壳,所有事件共用:
- table + 内联样式(Outlook/QQ邮箱/Gmail 兼容)
- 600px 卡片、顶部主色条、圆角徽标、字段表、引用块、callout、CTA 按钮
- 响应式(已验 375px 无溢出、无裁切)
- preheader 摘要、纯文本兜底链接、收件人提示
## 五、后台状态按钮(用户要求)
问题详情页顶部一排按钮,点一下立刻生效并自动发信:
「通过并公开」「处理中」「已解决」「不予考虑」「撤下」
- 当前状态对应的按钮置灰
- 「不予考虑」展开内联面板填理由(不用 window.prompt)
- 新增 setstatus 动作;「通过并公开」会同时置可见
## 六、notify_log 加 reason 列
原表只有 reply_id,无法区分事件类型。加 reason 并幂等迁移;
老库 reply_id 是 NOT NULL,非回复事件用 0 表示。
## 验证
邮件专项 73/73:
- 8 类事件模板全部完整 HTML、table 布局、徽标、页脚、无 script
- 8 个主题互不相同;状态徽标文案正确
- 总开关关闭时 admin_new_thread 仍可独立生效
- 节流只作用于 reply/status,close 不受影响
- SMTP 未启用时留失败日志(含 reason 与原因)
- C1-C5:站点页保存后 smtp.host/user/notify.enabled 均未被清空;
邮件页保存后 oss.bucket 未被清空
反向验证:完整回退两层保护后 C1/C2/C3 全部 FAIL
(host 从 smtp.qiye.aliyun.com 变成空、notify.enabled 从 1 变 0),
证明测试能真正抓到该 bug,不是假通过。
回归:全新安装 33/33、老基线 25/25、附件 22/22。
226 lines
10 KiB
PHP
226 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* 发布新问题
|
|
*/
|
|
require __DIR__ . '/lib/bootstrap.php';
|
|
|
|
$pdo = fb_db();
|
|
$err = '';
|
|
$atts = []; // 校验失败时保留已上传的附件
|
|
|
|
/* ---- 软件:URL 带 sw 就用它,否则落到默认软件 ---- */
|
|
$swList = fb_softwares(true);
|
|
$sw = null;
|
|
$swKey = isset($_GET['sw']) ? (string)$_GET['sw'] : '';
|
|
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;
|
|
|
|
$cats = fb_categories($swId);
|
|
$catKeys = [];
|
|
foreach ($cats as $c) { $catKeys[] = (int)$c['id']; }
|
|
|
|
$form = [
|
|
'category_id' => $catKeys ? (string)$catKeys[0] : '',
|
|
'title' => '',
|
|
'body' => '',
|
|
'name' => '',
|
|
'email' => '',
|
|
'phone' => '',
|
|
];
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
foreach (['category_id', 'title', 'body', 'name', 'email', 'phone'] as $k) {
|
|
$form[$k] = trim(isset($_POST[$k]) ? (string)$_POST[$k] : '');
|
|
}
|
|
// POST 里带了 software_id 时以它为准(切换软件后表单会跟着变)
|
|
if (isset($_POST['software_id']) && ctype_digit((string)$_POST['software_id'])) {
|
|
$swPost = fb_software((int)$_POST['software_id']);
|
|
if ($swPost && (int)$swPost['enabled'] === 1) {
|
|
$sw = $swPost;
|
|
$swId = (int)$swPost['id'];
|
|
}
|
|
}
|
|
$capInput = isset($_POST['captcha']) ? (string)$_POST['captcha'] : '';
|
|
$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'] : '');
|
|
|
|
// 分类必须属于这个软件,否则视为未选
|
|
$curCatId = ctype_digit($form['category_id']) ? (int)$form['category_id'] : 0;
|
|
$curCat = $curCatId ? fb_category($curCatId) : null;
|
|
if (!$curCat || (int)$curCat['software_id'] !== $swId) { $curCat = null; $form['category_id'] = ''; }
|
|
|
|
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_len($form['title']) < 4 || fb_len($form['title']) > 80) {
|
|
$err = '标题请填写 4~80 个字符。';
|
|
} elseif (!fb_valid_body($form['body'])) {
|
|
$err = '问题描述请填写 2~5000 个字符。';
|
|
} elseif (!fb_valid_name($form['name'])) {
|
|
$err = '请填写昵称(1~24 个字符)。';
|
|
} elseif (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) {
|
|
$err = '请填写有效的邮箱地址(用于接收回复通知)。';
|
|
} elseif ($form['phone'] !== '' && !preg_match('/^[0-9+\-\s()]{5,30}$/', $form['phone'])) {
|
|
$err = '手机号格式不正确(可留空)。';
|
|
} elseif (!$attOk) {
|
|
$err = $attErr;
|
|
} elseif (!fb_rate_check('thread', (int)fb_cfg('rate_limit.thread_per_hour', 5))) {
|
|
$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();
|
|
// 审核:开启后新提交默认不公开(visible=0),管理员通过后才会出现在列表
|
|
$visible = fb_moderation_on() ? 0 : 1;
|
|
$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) VALUES(?,?,?,?,?,?,?,?,?,?,0,?,?,?)');
|
|
$ins->execute([
|
|
$form['title'], $form['body'],
|
|
$curCat ? (string)$curCat['slug'] : 'other', // 老列留个值,便于回退与检索
|
|
$curCat ? (int)$curCat['id'] : null,
|
|
$swId > 0 ? $swId : null,
|
|
$form['name'], $form['email'],
|
|
$form['phone'] !== '' ? $form['phone'] : null,
|
|
'open', $visible, $now, $now, fb_ip_hash(),
|
|
]);
|
|
$newId = (int)$pdo->lastInsertId();
|
|
fb_attach_save($pdo, $newId, null, $vList);
|
|
fb_rate_hit('thread');
|
|
|
|
// 通知管理员「有新反馈」(工单 #2)
|
|
// 此前 notify.admin_new_thread 这个设置项是**假的** ——
|
|
// 白名单里有、后台有勾选框,但没有任何代码实现它。
|
|
// 用户反馈「提交了反馈却收不到邮件」正是这个原因。
|
|
try {
|
|
$stNt = $pdo->prepare('SELECT * FROM threads WHERE id = ?');
|
|
$stNt->execute([$newId]);
|
|
$newThread = $stNt->fetch(PDO::FETCH_ASSOC);
|
|
if ($newThread) {
|
|
$swName = '';
|
|
if (!empty($swId)) {
|
|
$swRow = fb_software((int)$swId);
|
|
if ($swRow) { $swName = (string)$swRow['name']; }
|
|
}
|
|
fb_notify_event($pdo, 'new', $newThread, ['software_name' => $swName]);
|
|
}
|
|
} catch (Throwable $e) {
|
|
// 发信失败绝不能让用户提交失败 —— 反馈已经存库了
|
|
}
|
|
|
|
fb_redirect('thread.php?id=' . $newId . '&submitted=1');
|
|
}
|
|
}
|
|
}
|
|
|
|
fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]);
|
|
?>
|
|
|
|
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
|
|
|
|
<section class="narrow">
|
|
<h1>我要反馈</h1>
|
|
<p class="muted">请描述你遇到的问题或想要的功能。提交后会先进入审核,确认为有效问题后才会公开显示;<b>若经确认不是应用的问题,我们会把结论与说明通过邮件发送给你</b>(请填写常用邮箱)。</p>
|
|
|
|
<?php fb_flash_box($err, 'error'); ?>
|
|
|
|
<form method="post" action="new.php" class="card-form">
|
|
<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>
|
|
|
|
<?php if ($sw): ?>
|
|
<input type="hidden" name="software_id" value="<?php echo (int)$sw['id']; ?>">
|
|
<div class="field">
|
|
<span>所属软件 <b class="req">*</b><i class="hint">换软件会刷新下面的分类</i></span>
|
|
<div class="chips">
|
|
<?php foreach ($swList as $one): ?>
|
|
<a class="chip <?php echo ((int)$one['id'] === $swId) ? 'on' : ''; ?>"
|
|
href="new.php?sw=<?php echo e(urlencode((string)$one['slug'])); ?>"><?php echo e((string)$one['name']); ?></a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($sw): ?>
|
|
<input type="hidden" name="software_id" value="<?php echo (int)$sw['id']; ?>">
|
|
<?php endif; ?>
|
|
|
|
<?php if ($cats): ?>
|
|
<label class="field">
|
|
<span>问题分类 <b class="req">*</b></span>
|
|
<select name="category_id" required>
|
|
<?php foreach ($cats as $c): ?>
|
|
<option value="<?php echo (int)$c['id']; ?>" <?php echo $form['category_id'] === (string)$c['id'] ? 'selected' : ''; ?>><?php echo e((string)$c['name']); ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</label>
|
|
<?php endif; ?>
|
|
|
|
<label class="field">
|
|
<span>标题 <b class="req">*</b></span>
|
|
<input type="text" name="title" maxlength="80" value="<?php echo e($form['title']); ?>" placeholder="一句话概括你的问题" required>
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>详细描述 <b class="req">*</b></span>
|
|
<textarea name="body" rows="9" maxlength="5000" placeholder="请尽量说明:你做了什么、期望的结果、实际的结果;如是同步问题,可说明使用的邮箱 / 服务器类型。" required><?php echo e($form['body']); ?></textarea>
|
|
</label>
|
|
|
|
<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">
|
|
<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>手机号 <i class="hint">选填;仅用于必要时联系,不会公开</i></span>
|
|
<input type="tel" name="phone" maxlength="30" value="<?php echo e($form['phone']); ?>" placeholder="选填">
|
|
</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>
|
|
|
|
<div class="form-actions">
|
|
<button class="btn primary" type="submit">提交反馈</button>
|
|
</div>
|
|
</form>
|
|
|
|
<p class="muted small">提示:本页面不会公开展示你的邮箱与手机号。请勿在内容中填写密码等敏感信息。可上传截图或录屏帮助说明问题;文件会直接传到对象存储,不占用本站服务器空间。</p>
|
|
</section>
|
|
|
|
<?php
|
|
fb_layout_foot();
|