diff --git a/admin.php b/admin.php index 930ac77..c452e81 100644 --- a/admin.php +++ b/admin.php @@ -280,21 +280,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) { $msg = '#' . $id . ' 已删除。'; } elseif ($act === 'mail_test') { - // 发送测试邮件:只验证 SMTP 通不通,不改任何配置 + // 发送测试邮件:只验证 SMTP 通不通,不改任何配置。 + // 可选 sw=<软件id>:按那个软件的样子渲染(配色 / 图标 / 软件名), + // 便于在后台直接预览「不同软件的邮件长什么样」。 $to = trim((string)(isset($_POST['to']) ? $_POST['to'] : '')); if ($to === '') { $to = trim((string)fb_cfg('admin_email', '')); } + if ($to === '') { $to = trim((string)fb_cfg('smtp.user', '')); } if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { $msg = '请填写有效的收件邮箱。'; $msgType = 'error'; } else { $sc = (array)fb_cfg('smtp', []); $site = (string)fb_cfg('site_name', '轻反馈'); - $body = '

这是一封来自 ' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8') - . ' 的测试邮件。

' - . '

如果你收到了它,说明 SMTP 配置正确,回复通知可以正常发出。

' - . '

发送时间:' - . date('Y-m-d H:i:s') . '

'; - list($ok, $err) = fb_smtp_send($sc, $to, '【' . $site . '】邮件测试', $body); + + // 选中的软件(没选就用默认软件,保证测试邮件也带软件标识) + $swId = (int)(isset($_POST['sw']) ? $_POST['sw'] : 0); + $swRow = $swId > 0 ? fb_software($swId) : fb_default_software(); + + // 用真实的回执模板发,比一句「测试邮件」更能看出实际效果 + $fake = [ + 'id' => 0, + 'software_id' => $swRow ? (int)$swRow['id'] : 0, + 'title' => '这是一封测试邮件,用来检查排版与配色', + 'body' => "这是测试用的反馈正文。\n如果你收到这封信,说明 SMTP 配置正确,配色与图标也应当对应「" + . ($swRow ? (string)$swRow['name'] : '默认软件') . "」。\n\n发送时间:" . date('Y-m-d H:i:s'), + 'author_name' => '测试', + 'author_email' => $to, + 'author_phone' => '', + 'status' => 'open', + 'visible' => 1, + ]; + list($subject, $html) = fb_notify_render('receipt', $fake, + ['software' => $swRow], $to); + $subject = '【测试】' . $subject; + + list($ok, $err) = fb_smtp_send($sc, $to, $subject, $html); if ($ok) { $msg = '测试邮件已发出,请查收 ' . $to . '(也看一下垃圾箱)。'; $msgType = 'ok'; @@ -574,6 +594,7 @@ function fb_admin_save_settings() { $save('notify.enabled', (int)(bool)$post('notify.enabled')); $save('notify.on_every_reply', (int)(bool)$post('notify.on_every_reply')); $save('notify.admin_new_thread', (int)(bool)$post('notify.admin_new_thread')); + $save('notify.receipt_enabled', (int)(bool)$post('notify.receipt_enabled')); // 冷却秒数留空 = 用默认 300(config 里的出厂值)。注意 0 是合法值(表示不限制), // 所以要用「空串」而不是「0」来区分"没填"和"填了 0" @@ -1317,13 +1338,21 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
配好后先发一封测试邮件确认能通: + value=""> +
+

测试邮件用的是「提交回执」模板,会带上所选软件的图标与配色 —— 可以直接看出不同软件的邮件长什么样。

通知规则

+ $sw]);
-

+ +

+ +

全部问题

+ $sw]);
+ href=" (string)$s['slug'], 'cat' => null, 'page' => null]) ?: ('?sw=' . e(urlencode((string)$s['slug'])))); ?>">
diff --git a/lib/admin_taxonomy.php b/lib/admin_taxonomy.php index 593b4e2..24a93c8 100644 --- a/lib/admin_taxonomy.php +++ b/lib/admin_taxonomy.php @@ -29,8 +29,10 @@ function fb_admin_taxonomy_action($pdo, $act, $post) { if (fb_software($slug)) { return ['英文标识「' . $slug . '」已被占用,换一个。', 'error']; } $sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM software')->fetchColumn(); $color = fb_color_norm(isset($post['color']) ? $post['color'] : ''); - $pdo->prepare('INSERT INTO software(slug,name,note,color,sort_order,enabled,created_at) VALUES(?,?,?,?,?,1,?)') - ->execute([$slug, $name, $note !== '' ? $note : null, $color, $sort, $now]); + $icon = fb_sw_icon_norm(isset($post['icon']) ? $post['icon'] : ''); + $dl = fb_sw_url_norm(isset($post['download_url']) ? $post['download_url'] : ''); + $pdo->prepare('INSERT INTO software(slug,name,note,color,icon,download_url,sort_order,enabled,created_at) VALUES(?,?,?,?,?,?,?,1,?)') + ->execute([$slug, $name, $note !== '' ? $note : null, $color, $icon, $dl, $sort, $now]); $newId = (int)$pdo->lastInsertId(); // 新软件默认给一组常见分类,省得空着没法发帖 $i = 0; @@ -54,8 +56,10 @@ function fb_admin_taxonomy_action($pdo, $act, $post) { $other = fb_software($slug); if ($other && (int)$other['id'] !== $id) { return ['英文标识「' . $slug . '」已被其他软件占用。', 'error']; } $color = fb_color_norm(isset($post['color']) ? $post['color'] : ''); - $pdo->prepare('UPDATE software SET name=?, slug=?, note=?, color=?, sort_order=? WHERE id=?') - ->execute([$name, $slug, $note !== '' ? $note : null, $color, + $icon = fb_sw_icon_norm(isset($post['icon']) ? $post['icon'] : ''); + $dl = fb_sw_url_norm(isset($post['download_url']) ? $post['download_url'] : ''); + $pdo->prepare('UPDATE software SET name=?, slug=?, note=?, color=?, icon=?, download_url=?, sort_order=? WHERE id=?') + ->execute([$name, $slug, $note !== '' ? $note : null, $color, $icon, $dl, (int)(isset($post['sort_order']) ? $post['sort_order'] : $sw['sort_order']), $id]); return ['软件「' . $name . '」已保存。', 'ok']; } @@ -264,7 +268,8 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) { echo '
'; list($swC, $swCd, $swCs) = fb_software_colors($sw); echo ' '; - echo ' ' . e((string)$sw['name']) . ''; + echo ' ' + . fb_software_badge($sw, 'sm') . ''; echo ' ' . e((string)$sw['slug']) . ''; if (!$on) { echo ' 已停用'; } echo ' ' . $nCat . ' 个分类 · ' . $nTh . ' 条反馈'; @@ -310,6 +315,23 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) { . ''; + // 图标:一个字符即可(emoji / 汉字 / 字母),配合主题色一起区分软件 + $curIcon = $esw ? (string)(isset($esw['icon']) ? $esw['icon'] : '') : ''; + $iconSug = ['📅', '🔐', '🔑', '📝', '🎵', '📷', '🌤', '🧭', '📦', '⚙️', '🗂', '💬']; + echo ' '; + + // 下载地址:状态改成「已解决」时,邮件里会给用户一个去下新版的按钮 + echo ' '; + // 主题色:前台按软件换色,用短链接进不同软件一眼就能区分 $curColor = $esw ? fb_color_norm((string)$esw['color']) : ''; $sugColor = $curColor !== '' ? $curColor : fb_color_default_for($esw ? (int)$esw['id'] : 0); diff --git a/lib/bootstrap.php b/lib/bootstrap.php index b4c5e8e..600dddb 100644 --- a/lib/bootstrap.php +++ b/lib/bootstrap.php @@ -8,7 +8,7 @@ if (!defined('FB_ROOT')) { define('FB_ROOT', dirname(__DIR__)); // 静态资源版本号:改 assets/ 下的 css/js 后把这个数字 +1, // 让浏览器与 CDN 缓存失效。全站只此一处,别再往页面里写死数字。 - if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 16); } + if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 17); } } /* ------------------------------------------------------------------ diff --git a/lib/db.php b/lib/db.php index fe76f79..9187322 100644 --- a/lib/db.php +++ b/lib/db.php @@ -324,6 +324,17 @@ SQL if (!isset($swNames['color'])) { $pdo->exec("ALTER TABLE software ADD COLUMN color TEXT NOT NULL DEFAULT ''"); } + /* ---- 软件增加图标(多软件并存时靠「图标 + 名字 + 颜色」三重区分)---- + * 存一个字符即可:优先 emoji(日历 📅 / 锁 🔐 / 密码 🔑),也接受单个汉字或字母。 + * 不存图片路径,避免再管一套上传 / 存储 / 权限 —— 邮件里也用同一个字符渲染, + * 前端与邮件天然一致,不会出现「页面有图标、邮件没有」的割裂。 */ + if (!isset($swNames['icon'])) { + $pdo->exec("ALTER TABLE software ADD COLUMN icon TEXT NOT NULL DEFAULT ''"); + } + /* ---- 软件增加下载地址(状态「已解决」的邮件里给用户一个去下新版的入口)---- */ + if (!isset($swNames['download_url'])) { + $pdo->exec("ALTER TABLE software ADD COLUMN download_url TEXT NOT NULL DEFAULT ''"); + } /* ---- threads 增加两列(老 category 文本列保留,不删)---- */ $cols = $pdo->query('PRAGMA table_info(threads)')->fetchAll(); diff --git a/lib/layout.php b/lib/layout.php index 703a179..d49cd23 100644 --- a/lib/layout.php +++ b/lib/layout.php @@ -51,10 +51,22 @@ function fb_layout_head($title, array $opt = []) {
- - - - + + + + + + + + + + +
全部问题 管理后台 diff --git a/lib/mailer.php b/lib/mailer.php index a613b05..4418ce2 100644 --- a/lib/mailer.php +++ b/lib/mailer.php @@ -144,14 +144,67 @@ if (!function_exists('fb_mail_esc')) { } if (!function_exists('fb_mail_brand')) { - /** 取站点主色,用于邮件头与按钮。跟随「软件主题色」体系,但邮件里只用主色与浅底 */ + /** + * 邮件主色。 + * + * 🔴 多软件站点的核心要求:邮件颜色必须跟「这条反馈属于哪个软件」一致, + * 否则用户在收件箱里根本分不清是哪个软件的来信。 + * 取值顺序: + * ① 调用方显式传入的软件行(fb_mail_layout(['software' => ...])) + * ② 当前全局软件($GLOBALS['__fb_mail_sw'],由 fb_notify_render 设置) + * ③ 配置里的 mail_brand_color(单软件站点的老配置,保持兼容) + * ④ 兜底蓝 + */ function fb_mail_brand() { + $sw = null; + if (isset($GLOBALS['__fb_mail_sw']) && is_array($GLOBALS['__fb_mail_sw'])) { + $sw = $GLOBALS['__fb_mail_sw']; + } + if ($sw) { + list($c) = fb_software_colors($sw); + if ($c !== '') { return $c; } + } $c = fb_color_norm((string)fb_cfg('mail_brand_color', '')); if ($c === '') { $c = '#0057B8'; } return $c; } } +if (!function_exists('fb_mail_sw_ctx')) { + /** 设置/读取「当前邮件所属软件」,返回之前的上下文(便于嵌套调用后还原) */ + function fb_mail_sw_ctx($sw = null, $set = false) { + $prev = isset($GLOBALS['__fb_mail_sw']) ? $GLOBALS['__fb_mail_sw'] : null; + if ($set) { $GLOBALS['__fb_mail_sw'] = $sw; } + return $prev; + } +} + +if (!function_exists('fb_mail_sw_header')) { + /** + * 邮件头部的「软件标识」:色块图标 + 软件名。 + * 用 table + 内联样式写,兼容 Outlook 等不认 flex 的客户端。 + */ + function fb_mail_sw_header($sw) { + if (!is_array($sw) || empty($sw['name'])) { return ''; } + list($c) = fb_software_colors($sw); + if ($c === '') { $c = '#0057B8'; } + $icon = fb_software_icon($sw); + $name = (string)$sw['name']; + + return '' + . '' + . '' + . '
' + . '' . fb_mail_esc($icon) . '' + . '' + . fb_mail_esc($name) . '
'; + } +} + if (!function_exists('fb_mail_block')) { /** * 渲染一个内容块。 @@ -242,6 +295,10 @@ if (!function_exists('fb_mail_layout')) { * @return string 完整 HTML(可直接作为 text/html 正文) */ function fb_mail_layout(array $o) { + // 软件上下文:显式传入优先,否则用全局上下文(fb_notify_render 会设好) + $sw = isset($o['software']) && is_array($o['software']) ? $o['software'] : null; + if ($sw) { fb_mail_sw_ctx($sw, true); } // 让本函数内 fb_mail_brand() 取到软件色 + $brand = fb_mail_brand(); $site = (string)fb_cfg('site_name', '轻反馈'); $base = fb_base_url(); @@ -330,11 +387,18 @@ if (!function_exists('fb_mail_layout')) { . ' ' - // 站名 + // 站名 / 软件标识 + // 有软件上下文时:上排是「图标 + 软件名」(这是用户最需要一眼认出的信息), + // 下排再补一行小字站点名。没有软件时退回原来的纯站名写法。 . '' - . '
' - . fb_mail_esc($site) . '
' + . (isset($o['software']) && is_array($o['software']) + ? fb_mail_sw_header($o['software']) + . '
' + . fb_mail_esc($site) . '
' + : '
' + . fb_mail_esc($site) . '
') + . '' // 标题 . '' . $badgeHtml @@ -512,6 +576,7 @@ if (!function_exists('fb_notify_reason_label')) { /** 事件的中文名,用于日志与模板徽标 */ function fb_notify_reason_label($reason) { $map = [ + 'receipt' => '已收到反馈', 'reply' => '有新回复', 'status' => '状态更新', 'approve' => '已发布', @@ -533,9 +598,15 @@ if (!function_exists('fb_notify_enabled_for')) { * 它独立于前者 —— 就算关了给用户的信,管理员也可以想收通知 */ function fb_notify_enabled_for($reason) { + // 「新反馈」按 admin_new_thread 独立开关;「提交回执」按 receipt_enabled 独立开关 + // (回执是发给提问人的,跟「有人回复时通知提交人」是两件不同的事, + // 有人可能只想要回执不想要回复通知,所以不能共用一个开关) if ($reason === 'new') { return (bool)fb_cfg('notify.admin_new_thread', false); } + if ($reason === 'receipt') { + return (bool)fb_cfg('notify.receipt_enabled', true); + } return (bool)fb_cfg('notify.enabled', true); } } @@ -714,10 +785,10 @@ if (!function_exists('fb_notify_status_meta')) { 'tip' => '感谢你的耐心等待,暂时不需要你做任何操作。', ], 'resolved' => [ - 'badge' => '已解决', 'color' => '#1e7b3c', 'tone' => 'ok', - 'title' => '你反馈的问题已解决', - 'intro' => '好消息,你反馈的问题我们已经处理完成。', - 'tip' => '如果问题仍然存在,直接在本页回复即可,我们会继续跟进。', + 'badge' => '已完成', 'color' => '#1e7b3c', 'tone' => 'ok', + 'title' => '你反馈的问题已完成并上线', + 'intro' => '好消息:你反馈的内容我们已经处理完成,并已发布上线。', + 'tip' => '建议更新到最新版本后试一下。如果问题仍然存在,直接在本页回复即可,我们会继续跟进。', ], 'closed' => [ 'badge' => '已关闭', 'color' => '#8a6a15', 'tone' => 'warn', @@ -754,6 +825,24 @@ if (!function_exists('fb_notify_render')) { $short = fb_excerpt($title, 42); $isAdmin = !empty($extra['is_admin']); + /* ---- 软件上下文:整封邮件的配色、头部标识、主题前缀都靠它 ---- + * 每条反馈都属于某个软件。邮件必须让用户一眼看出「这是哪个软件的来信」, + * 所以这里解析一次,后面所有分支共用: + * $swName 软件名(进主题行前缀) + * $sw 软件行(进邮件头标识与配色) + * + * 🔴 允许 $extra['software'] 显式指定软件行: + * ① 后台「发送测试邮件」可以指定「按某个软件的样子发」,便于预览配色; + * ② 测试脚本能在不往库里插数据的前提下验证配色切换; + * ③ 万一 threads.software_id 指向已被删除的软件,调用方仍能给出正确上下文。 + * 没有显式指定时,才按 software_id 回查、再回落默认软件。 */ + $sw = (isset($extra['software']) && is_array($extra['software'])) ? $extra['software'] : null; + if (!$sw) { $sw = fb_software_of_thread($thread); } + $swName = $sw ? (string)$sw['name'] : ''; + fb_mail_sw_ctx($sw, true); + // 主题统一加软件名前缀,收件箱列表里不点开就能分辨 + $pfx = ($swName !== '') ? '【' . $swName . '】' : ''; + $subject = ''; $layout = []; @@ -764,21 +853,30 @@ if (!function_exists('fb_notify_render')) { $rbody = (string)(isset($reply['body']) ? $reply['body'] : ''); $isOff = $isAdmin ? '官方回复' : '新回复'; - $subject = '【' . $site . '】' . $isOff . ':' . $short; + $subject = $pfx . $isOff . ':' . $short; + /* 像博客评论通知那样带上上下文: + * 用户可能同时提过好几条反馈,只给「回复内容」根本想不起来是哪一条。 + * 所以固定三段:① 你反馈了什么 ② 谁回复了什么 ③ 去哪里继续。 */ + $origBody = (string)(isset($thread['body']) ? $thread['body'] : ''); $blocks = [ - ['type' => 'text', 'text' => '你在「' . $site . '」提交的反馈有新的回复。'], + ['type' => 'text', 'text' => ($swName !== '' ? '你在「' . $swName . '」提交的反馈' : '你提交的反馈') + . '有新的回复。'], ['type' => 'field', 'rows' => ['反馈标题' => $title]], - ['type' => 'quote', 'who' => $rname . ' 回复:', 'text' => $rbody], ]; + if (trim($origBody) !== '') { + $blocks[] = ['type' => 'quote', 'who' => '你当时反馈的内容:', 'text' => fb_excerpt($origBody, 220)]; + } + $blocks[] = ['type' => 'quote', 'who' => $rname . ' 回复:', 'text' => $rbody]; if ($isAdmin) { $blocks[] = ['type' => 'callout', 'tone' => 'info', 'text' => '这是官方回复。如果还有疑问,直接在本页继续回复即可。']; } $layout = [ + 'software' => $sw, 'preheader' => $rname . ':' . fb_excerpt($rbody, 60), - 'badge' => ['text' => $isOff, 'color' => $isAdmin ? '#0057B8' : '#5c6572'], + 'badge' => ['text' => $isOff, 'color' => $isAdmin ? fb_mail_brand() : '#5c6572'], 'title' => $isAdmin ? '官方回复了你的反馈' : '有人回复了你的反馈', 'intro' => ($name !== '' ? $name . ',你好:' : '你好:'), 'blocks' => $blocks, @@ -798,9 +896,10 @@ if (!function_exists('fb_notify_render')) { $label = fb_status_label($newKey); if ($label === '') { $label = $meta['badge']; } - $subject = '【' . $site . '】' . $short . ' —— ' . $label; + $subject = $pfx . $short . ' —— ' . $label; $rows = ['反馈标题' => $title]; + if ($swName !== '') { $rows['所属软件'] = $swName; } if ($oldKey !== '' && $oldKey !== $newKey) { $oldLabel = fb_status_label($oldKey); if ($oldLabel !== '') { $rows['原状态'] = $oldLabel; } @@ -818,33 +917,50 @@ if (!function_exists('fb_notify_render')) { $blocks[] = ['type' => 'callout', 'tone' => $meta['tone'], 'text' => $meta['tip']]; } + /* 「已解决」要真的推用户一把去验证: + * 软件配了下载地址就给一个下载按钮 —— 只说「修好了」用户不知道去哪拿新版本。 */ + $dlUrl = $sw ? trim((string)(isset($sw['download_url']) ? $sw['download_url'] : '')) : ''; + $btnText = '查看详情'; + $btnUrl = $url; + if ($newKey === 'resolved' && $dlUrl !== '') { + $blocks[] = ['type' => 'callout', 'tone' => 'ok', + 'text' => '新版本已经发布,建议下载最新版后验证一下。']; + $btnText = '下载最新版本'; + $btnUrl = $dlUrl; + } + $layout = [ + 'software' => $sw, 'preheader' => '状态更新为「' . $label . '」', 'badge' => ['text' => $meta['badge'], 'color' => $meta['color']], 'title' => $meta['title'], 'intro' => ($name !== '' ? $name . ',你好:' : '你好:'), 'blocks' => $blocks, - 'button' => ['text' => '查看详情', 'url' => $url], - 'link' => $url, + 'button' => ['text' => $btnText, 'url' => $btnUrl], + // 下载按钮之外,始终保留「查看这条反馈」的原文链接 + 'link' => ($btnUrl === $url) ? $url + : ('下载地址:' . $dlUrl . "\n" . '查看反馈:' . $url), 'recipient' => $to, ]; } /* ---------------- 3. 审核通过 ---------------- */ elseif ($reason === 'approve') { - $subject = '【' . $site . '】你的反馈已发布:' . $short; + $subject = $pfx . '你的反馈已发布:' . $short; $layout = [ + 'software' => $sw, 'preheader' => '你的反馈已经公开可见', 'badge' => ['text' => '已发布', 'color' => '#1e7b3c'], 'title' => '你的反馈已通过审核并公开', 'intro' => ($name !== '' ? $name . ',你好:' : '你好:'), 'blocks' => [ - ['type' => 'text', 'text' => '你提交的反馈已通过审核,现在可以在公开列表中看到了。'], + ['type' => 'text', 'text' => '你提交的反馈已通过审核,现在可以在' + . ($swName !== '' ? '「' . $swName . '」的' : '') . '公开列表中看到了。'], ['type' => 'field', 'rows' => ['反馈标题' => $title]], ['type' => 'callout', 'tone' => 'info', 'text' => '后续的处理进展与回复,我们会通过邮件通知你。'], ], - 'button' => ['text' => '查看这条反馈', 'url' => $url], + 'button' => ['text' => '查看并跟踪这条反馈', 'url' => $url], 'link' => $url, 'recipient' => $to, ]; @@ -853,7 +969,7 @@ if (!function_exists('fb_notify_render')) { /* ---------------- 4. 转为不公开 ---------------- */ elseif ($reason === 'hide') { $note = trim((string)(isset($extra['note']) ? $extra['note'] : '')); - $subject = '【' . $site . '】你的反馈已转为不公开:' . $short; + $subject = $pfx . '你的反馈已转为不公开:' . $short; $blocks = [ ['type' => 'text', 'text' => '你提交的反馈已从公开列表中撤下,不再对其他人可见。'], @@ -866,6 +982,7 @@ if (!function_exists('fb_notify_render')) { 'text' => '这条反馈本身仍然保留,你依然可以通过原链接访问和处理进展。']; $layout = [ + 'software' => $sw, 'preheader' => '这条反馈已不对其他人可见', 'badge' => ['text' => '不公开', 'color' => '#5c6572'], 'title' => '你的反馈已转为不公开', @@ -883,11 +1000,12 @@ if (!function_exists('fb_notify_render')) { $body = (string)$thread['body']; $mail = trim((string)fb_cfg('admin_email', '')); - $subject = '【' . $site . '】关于你反馈的问题《' . $short . '》'; + $subject = $pfx . '关于你反馈的问题《' . $short . '》'; $blocks = [ ['type' => 'text', - 'text' => '感谢你的反馈。我们已经核实过,这个情况并不是应用本身的问题,因此这条反馈已关闭(不会公开显示)。'], + 'text' => '感谢你的反馈。我们已经核实过,这个情况并不是' + . ($swName !== '' ? '「' . $swName . '」' : '应用') . '本身的问题,因此这条反馈已关闭(不会公开显示)。'], ['type' => 'field', 'rows' => ['反馈标题' => $title]], ]; if ($note !== '') { @@ -902,6 +1020,7 @@ if (!function_exists('fb_notify_render')) { ]; $layout = [ + 'software' => $sw, 'preheader' => '关于你反馈的问题,我们的说明', 'badge' => ['text' => '已关闭', 'color' => '#8a6a15'], 'title' => '关于你反馈的问题,我们的说明', @@ -916,18 +1035,24 @@ if (!function_exists('fb_notify_render')) { $body = (string)$thread['body']; $mail = (string)(isset($thread['author_email']) ? $thread['author_email'] : ''); $phone = trim((string)(isset($thread['author_phone']) ? $thread['author_phone'] : '')); - $swName = (string)(isset($extra['software_name']) ? $extra['software_name'] : ''); + // 软件名优先取实际软件行(比 extra 里传进来的更可靠) + $swName = $swName !== '' ? $swName + : (string)(isset($extra['software_name']) ? $extra['software_name'] : ''); - $subject = '【' . $site . '】新反馈:' . $short; + $subject = $pfx . '新反馈:' . $short; - $rows = ['标题' => $title]; + $rows = []; if ($swName !== '') { $rows['所属软件'] = $swName; } + $rows['标题'] = $title; $rows['提交人'] = $name !== '' ? $name : '(未留名)'; if ($mail !== '') { $rows['邮箱'] = $mail; } if ($phone !== '') { $rows['电话'] = $phone; } + $adminUrl = fb_base_url() . '/admin.php?tab=thread&id=' . (int)$thread['id']; + $blocks = [ - ['type' => 'text', 'text' => '有新的反馈提交,请及时处理。'], + ['type' => 'text', 'text' => ($swName !== '' ? '「' . $swName . '」' : '') + . '有一条新的反馈提交,请到管理后台处理。'], ['type' => 'field', 'rows' => $rows], ['type' => 'quote', 'who' => '反馈内容:', 'text' => $body], ['type' => 'callout', 'tone' => 'warn', @@ -935,22 +1060,54 @@ if (!function_exists('fb_notify_render')) { ]; $layout = [ + 'software' => $sw, 'preheader' => $name . ':' . fb_excerpt($body, 60), 'badge' => ['text' => '新反馈', 'color' => '#b4553c'], - 'title' => '收到一条新反馈', + 'title' => ($swName !== '' ? $swName . ' 收到一条新反馈' : '收到一条新反馈'), 'intro' => '你好:', 'blocks' => $blocks, - 'button' => ['text' => '去后台处理', 'url' => fb_base_url() . '/admin.php?tab=thread&id=' . (int)$thread['id']], - 'link' => fb_base_url() . '/admin.php?tab=thread&id=' . (int)$thread['id'], + 'button' => ['text' => '去后台处理这条反馈', 'url' => $adminUrl], + 'link' => $adminUrl, 'recipient' => $to, 'footer' => '此邮件由系统自动发送。想调整接收规则,请到后台「邮件通知」页修改。', ]; } + /* ---------------- 7. 提交回执(发给提问人) ---------------- */ + elseif ($reason === 'receipt') { + $body = (string)$thread['body']; + + $subject = $pfx . '已收到你的反馈:' . $short; + + $blocks = [ + ['type' => 'text', 'text' => '我们已收到你在' + . ($swName !== '' ? '「' . $swName . '」' : '本站') + . '提交的反馈,感谢你的反馈!'], + ['type' => 'field', 'rows' => ['反馈标题' => $title]], + ['type' => 'quote', 'who' => '你提交的内容:', 'text' => fb_excerpt($body, 400)], + ['type' => 'callout', 'tone' => 'info', + 'text' => '这条反馈正在等待管理员审核,请耐心等待。审核通过后会在列表中公开显示。'], + ['type' => 'text', 'text' => '审核结果与后续处理进展,我们都会通过邮件通知你,无需反复回来查看。'], + ]; + + $layout = [ + 'software' => $sw, + 'preheader' => '已收到你的反馈,正在等待审核', + 'badge' => ['text' => '已收到', 'color' => fb_mail_brand()], + 'title' => '已收到你的反馈', + 'intro' => ($name !== '' ? $name . ',你好:' : '你好:'), + 'blocks' => $blocks, + 'button' => ['text' => '查看这条反馈', 'url' => $url], + 'link' => $url, + 'recipient' => $to, + ]; + } + /* ---------------- 兜底 ---------------- */ else { - $subject = '【' . $site . '】' . $short; + $subject = $pfx . $short; $layout = [ + 'software' => $sw, 'preheader' => '你有一条新的通知', 'title' => '你有一条新的通知', 'intro' => ($name !== '' ? $name . ',你好:' : '你好:'), diff --git a/lib/util.php b/lib/util.php index 39cb93a..801104d 100644 --- a/lib/util.php +++ b/lib/util.php @@ -171,6 +171,20 @@ function fb_default_software() { return $list ? $list[0] : null; } +/** + * 由帖子行取它所属的软件行(邮件/页面要显示「这是对哪个软件提交的」都用它)。 + * threads 只存 software_id,这里统一解析,避免各处自己写 SQL 漏掉。 + * 取不到时回落到默认软件,保证邮件里永远有软件名可显示。 + */ +function fb_software_of_thread(array $thread) { + $id = isset($thread['software_id']) ? (int)$thread['software_id'] : 0; + if ($id > 0) { + $sw = fb_software($id); + if ($sw) { return $sw; } + } + return fb_default_software(); +} + /** * 表单回显用:如果当前值等于「出厂默认」,就返回空串。 * @@ -263,6 +277,74 @@ function fb_software_colors($software) { return fb_color_variants($hex); } +/** + * 规范化软件图标字符。 + * 只留前几个字符,去掉换行与 HTML 尖括号 —— 这个值会直接输出到页面和邮件里, + * 放进 之间。虽然后面统一走 e() 转义,但源头收窄一层更稳。 + */ +function fb_sw_icon_norm($s) { + $s = trim(strip_tags((string)$s)); + $s = preg_replace('/[\x00-\x1F\x7F]+/u', '', $s); + if ($s === null || $s === '') { return ''; } + return mb_substr($s, 0, 4, 'UTF-8'); +} + +/** 规范化软件的下载地址:只接受 http/https,其它一律丢空 */ +function fb_sw_url_norm($s) { + $s = trim((string)$s); + if ($s === '') { return ''; } + if (!preg_match('~^https?://~i', $s)) { return ''; } + return mb_substr($s, 0, 300, 'UTF-8'); +} + +/** + * 某软件的图标字符(单字符:emoji / 汉字 / 字母)。 + * 没配就按软件名首字兜底 —— 多软件并存时,任何地方都不该出现「无图标」的空档。 + */ +function fb_software_icon($software) { + $ic = ''; + if (is_array($software)) { + $ic = trim((string)(isset($software['icon']) ? $software['icon'] : '')); + if ($ic === '') { + // 兜底:取名称首字(中文取第一个字,英文取首字母大写) + $nm = trim((string)(isset($software['name']) ? $software['name'] : '')); + if ($nm !== '') { + if (preg_match('/^[A-Za-z]/', $nm)) { $ic = strtoupper($nm[0]); } + else { $ic = mb_substr($nm, 0, 1, 'UTF-8'); } + } + } + } + if ($ic === '') { $ic = '·'; } + // 只允许极短内容,防止把整段 HTML 塞进图标位 + return mb_substr($ic, 0, 4, 'UTF-8'); +} + +/** + * 图标 + 名字的组合标签(HTML)。 + * 前端所有「这是哪个软件」的地方都用它,保证视觉一致。 + * + * @param array $sw 软件行 + * @param string $size 'sm' | 'md' | 'lg' + */ +function fb_software_badge($software, $size = 'md') { + if (!is_array($software) || empty($software['name'])) { return ''; } + list($c) = fb_software_colors($software); + if ($c === '') { $c = '#0057B8'; } + $icon = fb_software_icon($software); + $name = (string)$software['name']; + + $px = ['sm' => 20, 'md' => 26, 'lg' => 34]; + $fs = ['sm' => 11, 'md' => 14, 'lg' => 17]; + $box = isset($px[$size]) ? $px[$size] : 26; + $fnt = isset($fs[$size]) ? $fs[$size] : 14; + + return '' + . '' + . '' . e($name) . '' + . ''; +} + /** 某软件下的分类;不传软件则返回空数组 */ function fb_categories($softwareId = null, $onlyEnabled = true) { if ($softwareId === null) { return []; } @@ -490,6 +572,7 @@ function fb_setting_keys() { 'smtp.timeout', 'smtp.verify_ssl', 'notify.enabled', 'notify.on_every_reply', 'notify.cooldown_seconds', 'notify.admin_new_thread', + 'notify.receipt_enabled', ]; } diff --git a/new.php b/new.php index 70ddc58..547ebbc 100644 --- a/new.php +++ b/new.php @@ -119,6 +119,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $swRow = fb_software((int)$swId); if ($swRow) { $swName = (string)$swRow['name']; } } + + /* ① 先给提交人发「已收到」回执 —— 让他知道东西送达了、正在等审核。 + * 这一步是用户最在意的:提交完页面跳走了,没有回执就等于石沉大海。 + * 放在管理员通知之前,是因为回执对人的价值更高,且失败互不影响。 */ + fb_notify_event($pdo, 'receipt', $newThread, ['software_name' => $swName]); + + /* ② 再通知管理员「有新反馈」 */ fb_notify_event($pdo, 'new', $newThread, ['software_name' => $swName]); } } catch (Throwable $e) { @@ -137,6 +144,10 @@ fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]

我要反馈

+ +
+ 你正在为这个软件提交反馈
+

请描述你遇到的问题或想要的功能。提交后会先进入审核,确认为有效问题后才会公开显示;若经确认不是应用的问题,我们会把结论与说明通过邮件发送给你(请填写常用邮箱)。

@@ -155,7 +166,7 @@ fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]
+ href="new.php?sw=">
diff --git a/thread.php b/thread.php index afee292..91c9b73 100644 --- a/thread.php +++ b/thread.php @@ -161,7 +161,7 @@ fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]); $thSw = !empty($thread['software_id']) ? fb_software((int)$thread['software_id']) : null; $thCat = !empty($thread['category_id']) ? fb_category((int)$thread['category_id']) : null; ?> - +