症状:notify_log 全表为空 → 用户报「完全没发邮件」,后台查不到任何线索。 1. 前 4 个前置判断(开关/收件人/自己回自己/冷却)直接 return 不写日志, 等于「为什么没发」完全无痕。改为任何退出路径都写 notify_log, 并带上可读原因。 2. new 事件(新反馈通知管理员)收件人只取 admin_email,该值为空时整条 通知静默失效 —— 管理员永远收不到新反馈信,界面也无任何提示。 改为回落 smtp.user / smtp.from;后台勾了「通知管理员」却没填邮箱时 当场显示警告。 验证:走 fb_notify_event 全事件测试,6 个事件全部写日志、new 能送达。
967 lines
46 KiB
PHP
967 lines
46 KiB
PHP
<?php
|
||
/**
|
||
* 极简 SMTP 客户端(无第三方依赖)+ 回复通知逻辑
|
||
*/
|
||
if (!defined('FB_ROOT')) { http_response_code(403); exit('forbidden'); }
|
||
|
||
/** 读取一行 SMTP 响应(支持多行 250-... ) */
|
||
function fb_smtp_read($fp) {
|
||
$data = '';
|
||
while (($line = fgets($fp, 1024)) !== false) {
|
||
$data .= $line;
|
||
if (strlen($line) >= 4 && $line[3] === ' ') { break; }
|
||
}
|
||
return $data;
|
||
}
|
||
|
||
/** 发送命令并校验响应码 */
|
||
function fb_smtp_cmd($fp, $cmd, $codes, &$err, $hideLog = false) {
|
||
if ($cmd !== null && $cmd !== '') {
|
||
fwrite($fp, $cmd . "\r\n");
|
||
}
|
||
$resp = fb_smtp_read($fp);
|
||
$code = (int)substr(ltrim($resp), 0, 3);
|
||
$want = is_array($codes) ? $codes : [(int)$codes];
|
||
if (!in_array($code, $want, true)) {
|
||
$err = ($hideLog ? '(命令内容已隐藏) ' : '') . trim($resp);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/** 邮件头编码(UTF-8 -> =?UTF-8?B?...?=) */
|
||
function fb_mail_header($s) {
|
||
return '=?UTF-8?B?' . base64_encode((string)$s) . '?=';
|
||
}
|
||
|
||
/**
|
||
* 发送一封 HTML 邮件
|
||
* @return array [bool ok, string err]
|
||
*/
|
||
function fb_smtp_send(array $s, $to, $subject, $html) {
|
||
$err = '';
|
||
$host = (string)$s['host'];
|
||
$port = (int)$s['port'];
|
||
$secure = strtolower(isset($s['secure']) ? (string)$s['secure'] : '');
|
||
$timeout= (int)(isset($s['timeout']) ? $s['timeout'] : 15);
|
||
$verify = !isset($s['verify_ssl']) || $s['verify_ssl'];
|
||
|
||
if ($host === '' || $port <= 0) { return [false, 'SMTP 参数不完整']; }
|
||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { return [false, '收件地址非法']; }
|
||
|
||
$sslOpt = $verify
|
||
? ['verify_peer' => true, 'verify_peer_name' => true]
|
||
: ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true];
|
||
$ctx = stream_context_create(['ssl' => $sslOpt]);
|
||
$remote = (($secure === 'ssl' || $secure === 'smtps') ? 'ssl://' : '') . $host . ':' . $port;
|
||
|
||
$fp = @stream_socket_client($remote, $eno, $estr, $timeout, STREAM_CLIENT_CONNECT, $ctx);
|
||
if (!$fp) { return [false, '连接失败: ' . $estr . ' (' . $eno . ')']; }
|
||
stream_set_timeout($fp, $timeout);
|
||
|
||
$hello = isset($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : 'localhost';
|
||
$hello = preg_replace('/[^A-Za-z0-9\.\-]/', '', $hello);
|
||
if ($hello === '') { $hello = 'localhost'; }
|
||
|
||
if (!fb_smtp_cmd($fp, null, 220, $err)) { fclose($fp); return [false, '服务器问候失败: ' . $err]; }
|
||
if (!fb_smtp_cmd($fp, 'EHLO ' . $hello, 250, $err)) { fclose($fp); return [false, 'EHLO 失败: ' . $err]; }
|
||
|
||
if ($secure === 'tls' || $secure === 'starttls') {
|
||
if (!fb_smtp_cmd($fp, 'STARTTLS', 220, $err)) { fclose($fp); return [false, 'STARTTLS 失败: ' . $err]; }
|
||
if (!@stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
||
fclose($fp); return [false, 'TLS 握手失败'];
|
||
}
|
||
if (!fb_smtp_cmd($fp, 'EHLO ' . $hello, 250, $err)) { fclose($fp); return [false, 'EHLO(TLS) 失败: ' . $err]; }
|
||
}
|
||
|
||
$user = (string)(isset($s['user']) ? $s['user'] : '');
|
||
if ($user !== '') {
|
||
if (!fb_smtp_cmd($fp, 'AUTH LOGIN', 334, $err)) { fclose($fp); return [false, 'AUTH LOGIN 不受支持: ' . $err]; }
|
||
if (!fb_smtp_cmd($fp, base64_encode($user), 334, $err, true)) { fclose($fp); return [false, '用户名未被接受: ' . $err]; }
|
||
if (!fb_smtp_cmd($fp, base64_encode((string)$s['pass']), 235, $err, true)) { fclose($fp); return [false, '认证失败(账号 / 密码 / 授权码): ' . $err]; }
|
||
}
|
||
|
||
$from = !empty($s['from']) ? (string)$s['from'] : $user;
|
||
if (!filter_var($from, FILTER_VALIDATE_EMAIL)) { fclose($fp); return [false, '发件地址非法']; }
|
||
|
||
if (!fb_smtp_cmd($fp, 'MAIL FROM:<' . $from . '>', 250, $err)) { fclose($fp); return [false, 'MAIL FROM 失败: ' . $err]; }
|
||
if (!fb_smtp_cmd($fp, 'RCPT TO:<' . $to . '>', [250, 251], $err)) { fclose($fp); return [false, 'RCPT TO 失败: ' . $err]; }
|
||
if (!fb_smtp_cmd($fp, 'DATA', 354, $err)) { fclose($fp); return [false, 'DATA 失败: ' . $err]; }
|
||
|
||
$fromName = isset($s['from_name']) ? (string)$s['from_name'] : '';
|
||
$headers = [];
|
||
$headers[] = 'Date: ' . date('r');
|
||
$headers[] = 'From: ' . ($fromName !== '' ? fb_mail_header($fromName) . ' ' : '') . '<' . $from . '>';
|
||
$headers[] = 'To: <' . $to . '>';
|
||
$headers[] = 'Subject: ' . fb_mail_header($subject);
|
||
$headers[] = 'MIME-Version: 1.0';
|
||
$headers[] = 'Content-Type: text/html; charset=UTF-8';
|
||
$headers[] = 'Content-Transfer-Encoding: base64';
|
||
$headers[] = 'Message-ID: <' . bin2hex(random_bytes(12)) . '@' . $hello . '>';
|
||
$headers[] = 'X-Mailer: FeedLite';
|
||
|
||
// 正文用 base64,天然不含以 "." 开头的行,因此无需 dot-stuffing
|
||
$body = chunk_split(base64_encode($html), 76, "\r\n");
|
||
$data = rtrim(implode("\r\n", $headers) . "\r\n\r\n" . $body, "\r\n");
|
||
|
||
if (fwrite($fp, $data . "\r\n.\r\n") === false) { fclose($fp); return [false, '写入邮件正文失败']; }
|
||
if (!fb_smtp_cmd($fp, null, 250, $err)) { fclose($fp); return [false, '投递失败: ' . $err]; }
|
||
|
||
@fb_smtp_cmd($fp, 'QUIT', 221, $e2);
|
||
fclose($fp);
|
||
return [true, ''];
|
||
}
|
||
|
||
/** 渲染通知邮件正文 */
|
||
/**
|
||
* 统一的邮件模板引擎(工单 #2)
|
||
*
|
||
* 设计要点:
|
||
* 1. **表格布局 + 内联样式**。邮件客户端(Outlook / QQ 邮箱 / Gmail)对
|
||
* flex/grid/外部 CSS 的支持极不一致,只有 table + inline style 能稳定渲染。
|
||
* 2. **统一外壳**。此前 `fb_notify_html()` 和 `fb_notify_closed_html()` 各写一套
|
||
* 风格,颜色、间距、页脚都不一样。现在全部走 fb_mail_layout()。
|
||
* 3. **响应式**。桌面 600px,手机端靠 max-width:100% 自适应。
|
||
* 4. **暗色安全**。不用纯白背景配纯黑字,留出足够的对比度余量。
|
||
*
|
||
* 调用方式:
|
||
* echo fb_mail_layout([
|
||
* 'preheader' => '预览摘要(收件箱列表里显示的那行灰字)',
|
||
* 'badge' => ['text' => '处理中', 'color' => '#0a6cf5'],
|
||
* 'title' => '你的反馈已被受理',
|
||
* 'intro' => '老杨,你好:',
|
||
* 'blocks' => [ ... ], // 内容块,见下
|
||
* 'button' => ['text' => '查看详情', 'url' => 'https://...'],
|
||
* 'footer' => '此邮件由系统自动发送。',
|
||
* ]);
|
||
*/
|
||
|
||
if (!function_exists('fb_mail_esc')) {
|
||
/** 统一的 HTML 转义(模板里到处要用,短名字方便读) */
|
||
function fb_mail_esc($s) {
|
||
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_mail_brand')) {
|
||
/** 取站点主色,用于邮件头与按钮。跟随「软件主题色」体系,但邮件里只用主色与浅底 */
|
||
function fb_mail_brand() {
|
||
$c = fb_color_norm((string)fb_cfg('mail_brand_color', ''));
|
||
if ($c === '') { $c = '#0057B8'; }
|
||
return $c;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_mail_block')) {
|
||
/**
|
||
* 渲染一个内容块。
|
||
*
|
||
* 支持类型:
|
||
* quote —— 引用块(回复内容、官方说明),左侧色条 + 浅底
|
||
* field —— 一组「标签 / 值」行(状态变更、时间等元信息)
|
||
* text —— 普通段落
|
||
* callout —— 带图标的提示条(成功=绿、提醒=黄、信息=蓝)
|
||
* raw —— 已转义好的 HTML,原样输出(谨慎使用)
|
||
*/
|
||
function fb_mail_block(array $b) {
|
||
$type = isset($b['type']) ? (string)$b['type'] : 'text';
|
||
$brand = fb_mail_brand();
|
||
|
||
if ($type === 'text') {
|
||
return '<p style="margin:0 0 14px;font-size:15px;line-height:1.75;color:#333b46">'
|
||
. (isset($b['html']) ? $b['html'] : fb_mail_esc(isset($b['text']) ? $b['text'] : ''))
|
||
. '</p>';
|
||
}
|
||
|
||
if ($type === 'quote') {
|
||
$who = isset($b['who']) ? (string)$b['who'] : '';
|
||
$body = nl2br(fb_mail_esc(isset($b['text']) ? $b['text'] : ''));
|
||
return '<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"'
|
||
. ' style="margin:0 0 16px;border-collapse:separate">'
|
||
. '<tr><td style="padding:14px 18px;background:#f6f8fb;'
|
||
. 'border-left:3px solid ' . fb_mail_esc($brand) . ';border-radius:6px">'
|
||
. ($who !== ''
|
||
? '<div style="font-size:12.5px;color:#8a93a0;margin-bottom:8px">'
|
||
. fb_mail_esc($who) . '</div>' : '')
|
||
. '<div style="font-size:14.5px;line-height:1.75;color:#2f3742">' . $body . '</div>'
|
||
. '</td></tr></table>';
|
||
}
|
||
|
||
if ($type === 'field') {
|
||
$rows = isset($b['rows']) ? (array)$b['rows'] : [];
|
||
$out = '<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"'
|
||
. ' style="margin:0 0 18px;border-collapse:collapse;'
|
||
. 'background:#fafbfc;border:1px solid #eceff3;border-radius:8px">';
|
||
$i = 0;
|
||
foreach ($rows as $label => $value) {
|
||
$pad = ($i === 0) ? '12px 16px 6px' : '6px 16px';
|
||
if ($i === count($rows) - 1) { $pad = '6px 16px 12px'; }
|
||
if (count($rows) === 1) { $pad = '12px 16px'; }
|
||
$out .= '<tr>'
|
||
. '<td style="padding:' . $pad . ';font-size:13px;color:#8a93a0;'
|
||
. 'width:88px;vertical-align:top;white-space:nowrap">' . fb_mail_esc($label) . '</td>'
|
||
. '<td style="padding:' . $pad . ';font-size:14px;color:#2f3742;'
|
||
. 'line-height:1.6;vertical-align:top">' . fb_mail_esc($value) . '</td>'
|
||
. '</tr>';
|
||
$i++;
|
||
}
|
||
return $out . '</table>';
|
||
}
|
||
|
||
if ($type === 'callout') {
|
||
$tone = isset($b['tone']) ? (string)$b['tone'] : 'info';
|
||
$map = [
|
||
'ok' => ['bg' => '#f0f9f2', 'line' => '#b7dfc2', 'icon' => '✓', 'color' => '#1e7b3c'],
|
||
'warn' => ['bg' => '#fff9ec', 'line' => '#f0dfae', 'icon' => '!', 'color' => '#8a6a15'],
|
||
'info' => ['bg' => '#f2f7ff', 'line' => '#c8dbf7', 'icon' => 'i', 'color' => '#1a5fb4'],
|
||
'muted' => ['bg' => '#f7f8fa', 'line' => '#e6e9ee', 'icon' => '·', 'color' => '#5c6572'],
|
||
];
|
||
$m = isset($map[$tone]) ? $map[$tone] : $map['info'];
|
||
$body = isset($b['html']) ? $b['html']
|
||
: nl2br(fb_mail_esc(isset($b['text']) ? $b['text'] : ''));
|
||
return '<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"'
|
||
. ' style="margin:0 0 18px;border-collapse:separate">'
|
||
. '<tr><td style="padding:12px 16px;background:' . $m['bg'] . ';'
|
||
. 'border:1px solid ' . $m['line'] . ';border-radius:8px;font-size:14px;'
|
||
. 'line-height:1.7;color:' . $m['color'] . '">' . $body . '</td></tr></table>';
|
||
}
|
||
|
||
if ($type === 'raw') {
|
||
return isset($b['html']) ? (string)$b['html'] : '';
|
||
}
|
||
|
||
return '';
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_mail_layout')) {
|
||
/**
|
||
* 邮件统一外壳。所有通知都套这一个,保证视觉一致。
|
||
*
|
||
* @param array $o 见文件头注释
|
||
* @return string 完整 HTML(可直接作为 text/html 正文)
|
||
*/
|
||
function fb_mail_layout(array $o) {
|
||
$brand = fb_mail_brand();
|
||
$site = (string)fb_cfg('site_name', '轻反馈');
|
||
$base = fb_base_url();
|
||
|
||
$preheader = isset($o['preheader']) ? (string)$o['preheader'] : '';
|
||
$badge = isset($o['badge']) ? (array)$o['badge'] : null;
|
||
$title = isset($o['title']) ? (string)$o['title'] : '';
|
||
$intro = isset($o['intro']) ? (string)$o['intro'] : '';
|
||
$blocks = isset($o['blocks']) ? (array)$o['blocks'] : [];
|
||
$button = isset($o['button']) ? (array)$o['button'] : null;
|
||
$link = isset($o['link']) ? (string)$o['link'] : '';
|
||
$footer = isset($o['footer']) ? (string)$o['footer']
|
||
: '此邮件由系统自动发送,请勿直接回复。';
|
||
$recipient = isset($o['recipient']) ? (string)$o['recipient'] : '';
|
||
|
||
// ---- 内容块 ----
|
||
$body = '';
|
||
foreach ($blocks as $b) {
|
||
if (is_array($b)) { $body .= fb_mail_block($b); }
|
||
}
|
||
|
||
// ---- 徽标 ----
|
||
$badgeHtml = '';
|
||
if ($badge && !empty($badge['text'])) {
|
||
$bc = isset($badge['color']) ? (string)$badge['color'] : $brand;
|
||
$badgeHtml = '<div style="margin:0 0 10px">'
|
||
. '<span style="display:inline-block;padding:3px 11px;border-radius:99px;'
|
||
. 'background:' . fb_mail_esc($bc) . ';color:#ffffff;font-size:12px;'
|
||
. 'font-weight:600;letter-spacing:.3px">' . fb_mail_esc($badge['text']) . '</span>'
|
||
. '</div>';
|
||
}
|
||
|
||
// ---- 按钮 ----
|
||
$btnHtml = '';
|
||
if ($button && !empty($button['url']) && !empty($button['text'])) {
|
||
$url = (string)$button['url'];
|
||
$btnHtml = '<table role="presentation" cellpadding="0" cellspacing="0" border="0"'
|
||
. ' style="margin:6px 0 20px"><tr>'
|
||
. '<td style="background:' . fb_mail_esc($brand) . ';border-radius:8px">'
|
||
. '<a href="' . fb_mail_esc($url) . '" target="_blank"'
|
||
. ' style="display:inline-block;padding:12px 26px;font-size:15px;font-weight:600;'
|
||
. 'color:#ffffff;text-decoration:none;border-radius:8px">'
|
||
. fb_mail_esc($button['text']) . '</a></td></tr></table>';
|
||
}
|
||
|
||
// ---- 纯文本兜底链接 ----
|
||
$linkHtml = '';
|
||
if ($link !== '') {
|
||
$linkHtml = '<p style="margin:0 0 16px;font-size:12.5px;line-height:1.6;color:#9aa2ad">'
|
||
. '如果按钮无法点击,请复制下面的链接到浏览器打开:<br>'
|
||
. '<span style="color:' . fb_mail_esc($brand) . ';word-break:break-all">'
|
||
. fb_mail_esc($link) . '</span></p>';
|
||
}
|
||
|
||
// ---- 收件人问候 ----
|
||
$introHtml = '';
|
||
if ($intro !== '') {
|
||
$introHtml = '<p style="margin:0 0 16px;font-size:15px;line-height:1.75;color:#333b46">'
|
||
. fb_mail_esc($intro) . '</p>';
|
||
}
|
||
|
||
return '<!DOCTYPE html>'
|
||
. '<html lang="zh-CN"><head><meta charset="UTF-8">'
|
||
. '<meta name="viewport" content="width=device-width,initial-scale=1">'
|
||
. '<meta name="x-apple-disable-message-reformatting">'
|
||
. '<title>' . fb_mail_esc($title) . '</title></head>'
|
||
. '<body style="margin:0;padding:0;background:#eef1f5;'
|
||
. '-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%">'
|
||
|
||
// 预处理器:收件箱里显示的那行摘要,兼带隐藏占位防止被正文抢先
|
||
. ($preheader !== ''
|
||
? '<div style="display:none;font-size:1px;color:#eef1f5;line-height:1px;'
|
||
. 'max-height:0;max-width:0;opacity:0;overflow:hidden">'
|
||
. fb_mail_esc($preheader) . '</div>' : '')
|
||
|
||
// 外层留白
|
||
. '<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%"'
|
||
. ' style="background:#eef1f5"><tr><td align="center" style="padding:28px 14px">'
|
||
|
||
// 卡片
|
||
. '<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600"'
|
||
. ' style="width:100%;max-width:600px;background:#ffffff;border-radius:14px;'
|
||
. 'overflow:hidden;box-shadow:0 2px 10px rgba(20,30,50,.06)">'
|
||
|
||
// 顶部色条
|
||
. '<tr><td style="height:4px;background:' . fb_mail_esc($brand) . ';font-size:0;'
|
||
. 'line-height:0"> </td></tr>'
|
||
|
||
// 站名
|
||
. '<tr><td style="padding:22px 32px 0">'
|
||
. '<div style="font-size:13px;font-weight:700;letter-spacing:.6px;'
|
||
. 'color:' . fb_mail_esc($brand) . ';text-transform:uppercase">'
|
||
. fb_mail_esc($site) . '</div></td></tr>'
|
||
|
||
// 标题
|
||
. '<tr><td style="padding:12px 32px 4px">' . $badgeHtml
|
||
. '<h1 style="margin:0;font-size:21px;line-height:1.45;font-weight:700;'
|
||
. 'color:#1b2430">' . fb_mail_esc($title) . '</h1></td></tr>'
|
||
|
||
// 正文
|
||
. '<tr><td style="padding:16px 32px 8px">'
|
||
. $introHtml . $body . $btnHtml . $linkHtml
|
||
. '</td></tr>'
|
||
|
||
// 页脚
|
||
. '<tr><td style="padding:20px 32px 26px;border-top:1px solid #eef0f3">'
|
||
. '<p style="margin:0 0 6px;font-size:12.5px;line-height:1.65;color:#9aa2ad">'
|
||
. fb_mail_esc($footer) . '</p>'
|
||
. ($recipient !== ''
|
||
? '<p style="margin:0;font-size:11.5px;line-height:1.6;color:#b3b9c2">'
|
||
. '这封邮件发给了 ' . fb_mail_esc($recipient) . '。'
|
||
. '如果这不是你,忽略即可。</p>' : '')
|
||
. '<p style="margin:8px 0 0;font-size:11.5px;color:#b3b9c2">'
|
||
. '<a href="' . fb_mail_esc($base) . '" target="_blank"'
|
||
. ' style="color:#9aa2ad;text-decoration:none">'
|
||
. fb_mail_esc($base) . '</a></p>'
|
||
. '</td></tr>'
|
||
|
||
. '</table>'
|
||
. '</td></tr></table>'
|
||
. '</body></html>';
|
||
}
|
||
}
|
||
function fb_notify_html($thread, $reply, $isAdmin) {
|
||
$base = fb_base_url();
|
||
$url = $base . '/thread.php?id=' . (int)$thread['id'];
|
||
$site = (string)fb_cfg('site_name');
|
||
$name = (string)$thread['author_name'];
|
||
$title = (string)$thread['title'];
|
||
$rname = $isAdmin ? '官方' : (string)$reply['author_name'];
|
||
$rbody = nl2br(htmlspecialchars((string)$reply['body'], ENT_QUOTES, 'UTF-8'));
|
||
$safeName = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
|
||
$safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||
$safeRName = htmlspecialchars($rname, ENT_QUOTES, 'UTF-8');
|
||
|
||
return '<div style="font-family:-apple-system,Segoe UI,Microsoft YaHei,sans-serif;font-size:15px;line-height:1.7;color:#1f2329">'
|
||
. '<p>' . $safeName . ',你好:</p>'
|
||
. '<p>你在「' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8') . '」提交的问题 <strong>《' . $safeTitle . '》</strong> 有了新的回复:</p>'
|
||
. '<blockquote style="margin:12px 0;padding:12px 16px;border-left:3px solid #0a6cf5;background:#f5f8ff;border-radius:6px">'
|
||
. '<div style="color:#6b7280;font-size:13px;margin-bottom:6px">' . $safeRName . ' 回复:</div>'
|
||
. $rbody
|
||
. '</blockquote>'
|
||
. '<p><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" style="display:inline-block;background:#0a6cf5;color:#fff;text-decoration:none;padding:9px 18px;border-radius:8px">查看并回复</a></p>'
|
||
. '<p style="color:#6b7280;font-size:13px">若按钮无法点击,请复制链接到浏览器打开:<br>' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '</p>'
|
||
. '<hr style="border:none;border-top:1px solid #e5e7eb;margin:18px 0">'
|
||
. '<p style="color:#9aa2ad;font-size:12px">此邮件由系统自动发送,请勿直接回复。</p>'
|
||
. '</div>';
|
||
}
|
||
|
||
/**
|
||
* 有人回复后,给发帖人发送通知邮件
|
||
* @return string 空串=未发送/已发送成功;非空=失败原因
|
||
*/
|
||
function fb_notify_reply(PDO $pdo, array $thread, array $reply, $isAdmin) {
|
||
// 转调统一入口(工单 #2):旧调用点保持不改,但行为、模板、日志全部统一。
|
||
list($ok, $err) = fb_notify_event($pdo, 'reply', $thread, [
|
||
'reply' => $reply,
|
||
'is_admin' => (bool)$isAdmin,
|
||
]);
|
||
return $ok ? '' : $err;
|
||
}
|
||
|
||
/** 记录通知尝试 */
|
||
function fb_notify_log(PDO $pdo, array $thread, array $reply, $to, $ok, $err) {
|
||
try {
|
||
$st = $pdo->prepare('INSERT INTO notify_log(thread_id, reply_id, to_email, created_at, ok, err) VALUES(?,?,?,?,?,?)');
|
||
$st->execute([(int)$thread['id'], (int)$reply['id'], $to, time(), $ok ? 1 : 0, $err]);
|
||
} catch (Exception $ex) {
|
||
// 静默
|
||
}
|
||
}
|
||
|
||
/* ---------------- 关闭通知(判定"不是问题",写给提问人的说明邮件) ---------------- */
|
||
|
||
/** 关闭通知的邮件正文 */
|
||
function fb_notify_closed_html(array $thread, string $note) {
|
||
$site = (string)fb_cfg('site_name');
|
||
$name = htmlspecialchars((string)$thread['author_name'], ENT_QUOTES, 'UTF-8');
|
||
$title = htmlspecialchars((string)$thread['title'], ENT_QUOTES, 'UTF-8');
|
||
$body = nl2br(htmlspecialchars((string)$thread['body'], ENT_QUOTES, 'UTF-8'));
|
||
$noteH = nl2br(htmlspecialchars($note, ENT_QUOTES, 'UTF-8'));
|
||
$mail = htmlspecialchars((string)fb_cfg('admin_email'), ENT_QUOTES, 'UTF-8');
|
||
|
||
return '<div style="font-family:-apple-system,Segoe UI,Microsoft YaHei,sans-serif;font-size:15px;line-height:1.7;color:#1f2329">'
|
||
. '<p>' . $name . ',你好:</p>'
|
||
. '<p>感谢你反馈 <strong>《' . $title . '》</strong>。我们已经核实过,'
|
||
. '<strong>该情况并不是应用的问题</strong>,因此这条反馈已被关闭(不会公开显示)。</p>'
|
||
. '<div style="margin:14px 0;padding:12px 16px;border-left:3px solid #0a6cf5;background:#f5f8ff;border-radius:6px">'
|
||
. '<div style="color:#6b7280;font-size:13px;margin-bottom:6px">官方说明:</div>'
|
||
. $noteH
|
||
. '</div>'
|
||
. '<p style="color:#6b7280;font-size:13px">你当时反馈的内容:</p>'
|
||
. '<div style="margin:8px 0 16px;padding:10px 14px;background:#f7f8fa;border-radius:6px;color:#4b5563;font-size:14px">'
|
||
. $body
|
||
. '</div>'
|
||
. ($mail !== '' ? '<p>如果上面的说明没能解决你的疑问,可以直接回复本邮件,我们会继续跟进;'
|
||
. '也可以发邮件到 ' . $mail . ' 补充说明。</p>' : '<p>如果上面的说明没能解决你的疑问,可以直接回复本邮件,我们会继续跟进。</p>')
|
||
. '<hr style="border:none;border-top:1px solid #e5e7eb;margin:18px 0">'
|
||
. '<p style="color:#9aa2ad;font-size:12px">此邮件由「' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8') . '」自动发送。</p>'
|
||
. '</div>';
|
||
}
|
||
|
||
/**
|
||
* 关闭问题(判定"不是问题")时,把说明邮件发给提问人
|
||
* @return array [bool 是否发送成功, string 失败原因(空串=成功或未启用)]
|
||
*/
|
||
function fb_notify_closed(PDO $pdo, array $thread, string $note) {
|
||
// 转调统一入口(工单 #2)
|
||
return fb_notify_event($pdo, 'close', $thread, ['note' => $note]);
|
||
}
|
||
|
||
function fb_notify_log2(PDO $pdo, array $thread, $to, $reason, $ok, $err) {
|
||
// ⚠️ 老库的 notify_log.reply_id 是 NOT NULL,而「新反馈通知」没有回复 id。
|
||
// SQLite 不能 ALTER COLUMN 放宽约束,只能重建表 —— 为了不动用户已有数据,
|
||
// 这里用 reply_id=0 表示「非回复事件」,而不是 null。
|
||
try {
|
||
$pdo->prepare('INSERT INTO notify_log(thread_id, reply_id, to_email, reason, ok, err, created_at) '
|
||
. 'VALUES(?,?,?,?,?,?,?)')
|
||
->execute([
|
||
(int)$thread['id'],
|
||
0,
|
||
(string)$to,
|
||
(string)$reason,
|
||
(int)$ok,
|
||
$err === null ? null : (string)$err,
|
||
time(),
|
||
]);
|
||
} catch (Throwable $e) {
|
||
// reason 列可能不存在(老库未迁移),退化成原结构
|
||
try {
|
||
$pdo->prepare('INSERT INTO notify_log(thread_id, reply_id, to_email, ok, err, created_at) '
|
||
. 'VALUES(?,?,?,?,?,?)')
|
||
->execute([(int)$thread['id'], 0, (string)$to, (int)$ok,
|
||
$err === null ? null : (string)$err, time()]);
|
||
} catch (Throwable $e2) {
|
||
// 实在记不了就算了,不能因为记日志失败让用户的请求失败
|
||
}
|
||
}
|
||
}/**
|
||
* 各事件的邮件渲染(工单 #2)
|
||
*
|
||
* 每个事件返回 [主题, HTML]。
|
||
* 全部套 fb_mail_layout(),视觉统一;文案按事件差异化。
|
||
*/
|
||
|
||
/**
|
||
* 邮件通知事件(工单 #2)
|
||
*
|
||
* 全部事件:
|
||
* reply 有人回复了你的反馈 → 发帖人
|
||
* status 处理状态变了 → 发帖人
|
||
* approve 审核通过、已公开 → 发帖人
|
||
* hide 转为不公开 → 发帖人
|
||
* close 判定不是问题,关闭 → 发帖人(带说明)
|
||
* new 有新反馈提交 → 管理员
|
||
*
|
||
* 设计原则:
|
||
* 1. **一个总入口**。所有事件都走 fb_notify_event(),统一处理
|
||
* 「开关判断 → 收件人解析 → 节流 → 发信 → 记日志」,避免每个事件
|
||
* 各写一遍导致行为不一致(此前 close 和 reply 就是两套)。
|
||
* 2. **失败必留痕**。任何情况下都写 notify_log,便于后台排查
|
||
* 「为什么用户说没收到信」。
|
||
* 3. **不抛异常**。发信失败绝不能让用户的操作失败 —— 反馈已经存库了,
|
||
* 邮件只是附加动作。
|
||
*/
|
||
|
||
if (!function_exists('fb_notify_reason_label')) {
|
||
/** 事件的中文名,用于日志与模板徽标 */
|
||
function fb_notify_reason_label($reason) {
|
||
$map = [
|
||
'reply' => '有新回复',
|
||
'status' => '状态更新',
|
||
'approve' => '已发布',
|
||
'hide' => '转为不公开',
|
||
'close' => '已关闭',
|
||
'new' => '新反馈',
|
||
];
|
||
return isset($map[$reason]) ? $map[$reason] : '通知';
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_notify_enabled_for')) {
|
||
/**
|
||
* 这个事件在当前配置下要不要发?
|
||
*
|
||
* 注意区别:
|
||
* - notify.enabled 是「给提交人发信」的总开关
|
||
* - notify.admin_new_thread 只管「有新反馈时通知管理员」
|
||
* 它独立于前者 —— 就算关了给用户的信,管理员也可以想收通知
|
||
*/
|
||
function fb_notify_enabled_for($reason) {
|
||
if ($reason === 'new') {
|
||
return (bool)fb_cfg('notify.admin_new_thread', false);
|
||
}
|
||
return (bool)fb_cfg('notify.enabled', true);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_notify_throttled')) {
|
||
/**
|
||
* 节流:同一帖 + 同一收件人 + 同一原因,在冷却窗口内只发一次。
|
||
*
|
||
* ⚠️ 只对 reply / status 这类「可能短时间内反复触发」的事件生效;
|
||
* close / new 是一次性动作,不节流(否则用户会完全收不到)。
|
||
*
|
||
* @return bool true = 应该跳过(正在冷却中)
|
||
*/
|
||
function fb_notify_throttled(PDO $pdo, $threadId, $to, $reason) {
|
||
if (!in_array($reason, ['reply', 'status'], true)) { return false; }
|
||
$cd = (int)fb_cfg('notify.cooldown_seconds', 300);
|
||
if ($cd <= 0) { return false; }
|
||
|
||
$st = $pdo->prepare(
|
||
'SELECT created_at FROM notify_log WHERE thread_id=? AND to_email=? AND reason=? '
|
||
. 'ORDER BY id DESC LIMIT 1');
|
||
$st->execute([(int)$threadId, (string)$to, (string)$reason]);
|
||
$last = $st->fetchColumn();
|
||
if ($last === false) { return false; }
|
||
return (time() - (int)$last) < $cd;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_notify_event')) {
|
||
/**
|
||
* 发送一个通知事件。
|
||
*
|
||
* @param PDO $pdo
|
||
* @param string $reason reply|status|approve|hide|close|new
|
||
* @param array $thread 帖子行
|
||
* @param array $extra 附加信息:
|
||
* - reply 回复行(reply 事件用)
|
||
* - is_admin 是否官方操作
|
||
* - old_status / new_status (status 事件用)
|
||
* - note 说明文字(close / status 用)
|
||
* @return array [bool 是否发出, string 失败原因]
|
||
*/
|
||
function fb_notify_event(PDO $pdo, $reason, array $thread, array $extra = []) {
|
||
$reason = (string)$reason;
|
||
|
||
/* 🔴 铁律:任何一条退出路径都必须写 notify_log。
|
||
*
|
||
* 原先前 4 个判断(开关 / 收件人 / 自己回自己 / 冷却)直接 return 不记日志,
|
||
* 结果就是「用户说没收到信」时后台查不到任何记录 —— 因为压根就没进发信那一步。
|
||
* 2026-11-03 排查时 notify_log 全表为空,一度被误判成「发信链路坏了」,
|
||
* 实际是这些静默 return 造成的黑箱。日志必须回答「为什么没发」。
|
||
*/
|
||
$log = function ($to, $ok, $err) use ($pdo, $thread, $reason) {
|
||
fb_notify_log2($pdo, $thread, $to, $reason, $ok, $err);
|
||
};
|
||
|
||
// ---- 1. 开关 ----
|
||
if (!fb_notify_enabled_for($reason)) {
|
||
$log('', 0, '通知未启用(该事件的开关是关的)');
|
||
return [false, '通知未启用'];
|
||
}
|
||
|
||
// ---- 2. 收件人 ----
|
||
$isAdminNotice = ($reason === 'new');
|
||
if ($isAdminNotice) {
|
||
$to = trim((string)fb_cfg('admin_email', ''));
|
||
if ($to === '') { $to = trim((string)fb_cfg('admin.email', '')); }
|
||
// 还没填官方邮箱时回落到 SMTP 发件地址 —— 至少信能到管理员手上,
|
||
// 而不是整个「新反馈通知」功能静默失效(实际就发生过:管理员从不收信,
|
||
// 后台也没有任何提示,直到来测才发现)。
|
||
if ($to === '') { $to = trim((string)fb_cfg('smtp.user', '')); }
|
||
if ($to === '') { $to = trim((string)fb_cfg('smtp.from', '')); }
|
||
} else {
|
||
$to = trim((string)(isset($thread['author_email']) ? $thread['author_email'] : ''));
|
||
}
|
||
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||
$why = $isAdminNotice
|
||
? '收件邮箱无效:官方邮箱与 SMTP 发件地址都没填,新反馈通知发不出去'
|
||
: '收件邮箱无效:提交人没留邮箱或格式不对';
|
||
$log($to, 0, $why);
|
||
return [false, $why];
|
||
}
|
||
|
||
// ---- 3. 自己通知自己就别发了 ----
|
||
if (!$isAdminNotice && !empty($extra['is_admin']) === false
|
||
&& !empty($extra['reply']['author_email'])) {
|
||
if (strcasecmp(trim((string)$extra['reply']['author_email']), $to) === 0) {
|
||
$log($to, 0, '自己回复自己,不发');
|
||
return [false, '自己回复自己,不发'];
|
||
}
|
||
}
|
||
|
||
// ---- 4. 节流 ----
|
||
if (fb_notify_throttled($pdo, (int)$thread['id'], $to, $reason)) {
|
||
$log($to, 0, '冷却中跳过(notify.cooldown_seconds=' . (int)fb_cfg('notify.cooldown_seconds', 300) . ')');
|
||
return [false, '冷却中,跳过'];
|
||
}
|
||
|
||
// ---- 5. SMTP 就绪吗 ----
|
||
$smtp = (array)fb_cfg('smtp', []);
|
||
if (empty($smtp['enabled']) || trim((string)$smtp['host']) === '') {
|
||
$log($to, 0, 'SMTP 未启用或未配置服务器');
|
||
return [false, 'SMTP 未启用或未配置服务器'];
|
||
}
|
||
|
||
// ---- 6. 组装主题与正文 ----
|
||
list($subject, $html) = fb_notify_render($reason, $thread, $extra, $to);
|
||
|
||
// ---- 7. 发送 ----
|
||
list($ok, $err) = fb_smtp_send($smtp, $to, $subject, $html);
|
||
$log($to, $ok ? 1 : 0, $ok ? null : $err);
|
||
return [$ok, $ok ? '' : (string)$err];
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_notify_log2')) {
|
||
/**
|
||
* 记一条发信日志(带 reason)。
|
||
*
|
||
* 老表可能没有 reason 列 —— 用 try/catch 包一下,
|
||
* 万一列不存在就退化成不带 reason 的插入,绝不让记日志失败影响主流程。
|
||
*/
|
||
function fb_notify_log2(PDO $pdo, array $thread, $to, $reason, $ok, $err) {
|
||
try {
|
||
$pdo->prepare('INSERT INTO notify_log(thread_id, reply_id, to_email, reason, ok, err, created_at) '
|
||
. 'VALUES(?,?,?,?,?,?,?)')
|
||
->execute([
|
||
(int)$thread['id'],
|
||
null,
|
||
(string)$to,
|
||
(string)$reason,
|
||
(int)$ok,
|
||
$err === null ? null : (string)$err,
|
||
time(),
|
||
]);
|
||
} catch (Throwable $e) {
|
||
// 列可能不存在(老库未迁移),退化成原结构
|
||
try {
|
||
$pdo->prepare('INSERT INTO notify_log(thread_id, reply_id, to_email, ok, err, created_at) '
|
||
. 'VALUES(?,?,?,?,?,?)')
|
||
->execute([(int)$thread['id'], null, (string)$to, (int)$ok,
|
||
$err === null ? null : (string)$err, time()]);
|
||
} catch (Throwable $e2) {
|
||
// 实在记不了就算了,不能因此让用户的请求失败
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (!function_exists('fb_notify_thread_url')) {
|
||
function fb_notify_thread_url(array $thread) {
|
||
return fb_base_url() . '/thread.php?id=' . (int)$thread['id'];
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_notify_status_meta')) {
|
||
/**
|
||
* 各处理状态对应的「邮件语气」。
|
||
*
|
||
* 为什么单独抽出来:用户明确要求不同状态给不同文案 ——
|
||
* 「不予考虑」要礼貌说明、不让人觉得被敷衍;
|
||
* 「处理中」要让人知道在跟进;
|
||
* 「已解决」要引导他来验证。
|
||
*/
|
||
function fb_notify_status_meta($key) {
|
||
$map = [
|
||
'open' => [
|
||
'badge' => '待处理', 'color' => '#5c6572', 'tone' => 'muted',
|
||
'title' => '你的反馈已收到,正在等待处理',
|
||
'intro' => '我们已经收到你的反馈,会尽快安排处理。',
|
||
'tip' => null,
|
||
],
|
||
'doing' => [
|
||
'badge' => '处理中', 'color' => '#0a6cf5', 'tone' => 'info',
|
||
'title' => '你反馈的问题正在处理中',
|
||
'intro' => '我们已经开始跟进你反馈的问题,有进展会第一时间告诉你。',
|
||
'tip' => '感谢你的耐心等待,暂时不需要你做任何操作。',
|
||
],
|
||
'resolved' => [
|
||
'badge' => '已解决', 'color' => '#1e7b3c', 'tone' => 'ok',
|
||
'title' => '你反馈的问题已解决',
|
||
'intro' => '好消息,你反馈的问题我们已经处理完成。',
|
||
'tip' => '如果问题仍然存在,直接在本页回复即可,我们会继续跟进。',
|
||
],
|
||
'closed' => [
|
||
'badge' => '已关闭', 'color' => '#8a6a15', 'tone' => 'warn',
|
||
'title' => '你反馈的问题已关闭',
|
||
'intro' => '你反馈的问题已经关闭。',
|
||
'tip' => null,
|
||
],
|
||
'wontfix' => [
|
||
'badge' => '不予考虑', 'color' => '#b4553c', 'tone' => 'warn',
|
||
'title' => '关于你反馈的问题,我们的处理意见',
|
||
'intro' => '感谢你的反馈。经核实,这条反馈我们暂时不会改动,原因如下。',
|
||
'tip' => '如果你认为这个判断有误,或者有补充信息,直接在本页回复即可。',
|
||
],
|
||
];
|
||
return isset($map[$key]) ? $map[$key] : $map['open'];
|
||
}
|
||
}
|
||
|
||
if (!function_exists('fb_notify_render')) {
|
||
/**
|
||
* 渲染某个事件的 [主题, HTML]。
|
||
*
|
||
* @param string $reason
|
||
* @param array $thread
|
||
* @param array $extra
|
||
* @param string $to 收件人(用于页脚「这封邮件发给了…」)
|
||
* @return array [string subject, string html]
|
||
*/
|
||
function fb_notify_render($reason, array $thread, array $extra, $to = '') {
|
||
$site = (string)fb_cfg('site_name', '轻反馈');
|
||
$url = fb_notify_thread_url($thread);
|
||
$title = (string)$thread['title'];
|
||
$name = (string)(isset($thread['author_name']) ? $thread['author_name'] : '');
|
||
$short = fb_excerpt($title, 42);
|
||
$isAdmin = !empty($extra['is_admin']);
|
||
|
||
$subject = '';
|
||
$layout = [];
|
||
|
||
/* ---------------- 1. 新回复 ---------------- */
|
||
if ($reason === 'reply') {
|
||
$reply = (array)(isset($extra['reply']) ? $extra['reply'] : []);
|
||
$rname = $isAdmin ? '官方' : (string)(isset($reply['author_name']) ? $reply['author_name'] : '');
|
||
$rbody = (string)(isset($reply['body']) ? $reply['body'] : '');
|
||
$isOff = $isAdmin ? '官方回复' : '新回复';
|
||
|
||
$subject = '【' . $site . '】' . $isOff . ':' . $short;
|
||
|
||
$blocks = [
|
||
['type' => 'text', 'text' => '你在「' . $site . '」提交的反馈有新的回复。'],
|
||
['type' => 'field', 'rows' => ['反馈标题' => $title]],
|
||
['type' => 'quote', 'who' => $rname . ' 回复:', 'text' => $rbody],
|
||
];
|
||
if ($isAdmin) {
|
||
$blocks[] = ['type' => 'callout', 'tone' => 'info',
|
||
'text' => '这是官方回复。如果还有疑问,直接在本页继续回复即可。'];
|
||
}
|
||
|
||
$layout = [
|
||
'preheader' => $rname . ':' . fb_excerpt($rbody, 60),
|
||
'badge' => ['text' => $isOff, 'color' => $isAdmin ? '#0057B8' : '#5c6572'],
|
||
'title' => $isAdmin ? '官方回复了你的反馈' : '有人回复了你的反馈',
|
||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||
'blocks' => $blocks,
|
||
'button' => ['text' => '查看并回复', 'url' => $url],
|
||
'link' => $url,
|
||
'recipient' => $to,
|
||
];
|
||
}
|
||
|
||
/* ---------------- 2. 状态变更 ---------------- */
|
||
elseif ($reason === 'status') {
|
||
$newKey = (string)(isset($extra['new_status']) ? $extra['new_status'] : '');
|
||
$oldKey = (string)(isset($extra['old_status']) ? $extra['old_status'] : '');
|
||
$note = trim((string)(isset($extra['note']) ? $extra['note'] : ''));
|
||
|
||
$meta = fb_notify_status_meta($newKey);
|
||
$label = fb_status_label($newKey);
|
||
if ($label === '') { $label = $meta['badge']; }
|
||
|
||
$subject = '【' . $site . '】' . $short . ' —— ' . $label;
|
||
|
||
$rows = ['反馈标题' => $title];
|
||
if ($oldKey !== '' && $oldKey !== $newKey) {
|
||
$oldLabel = fb_status_label($oldKey);
|
||
if ($oldLabel !== '') { $rows['原状态'] = $oldLabel; }
|
||
}
|
||
$rows['当前状态'] = $label;
|
||
|
||
$blocks = [
|
||
['type' => 'text', 'text' => $meta['intro']],
|
||
['type' => 'field', 'rows' => $rows],
|
||
];
|
||
if ($note !== '') {
|
||
$blocks[] = ['type' => 'quote', 'who' => '官方说明:', 'text' => $note];
|
||
}
|
||
if (!empty($meta['tip'])) {
|
||
$blocks[] = ['type' => 'callout', 'tone' => $meta['tone'], 'text' => $meta['tip']];
|
||
}
|
||
|
||
$layout = [
|
||
'preheader' => '状态更新为「' . $label . '」',
|
||
'badge' => ['text' => $meta['badge'], 'color' => $meta['color']],
|
||
'title' => $meta['title'],
|
||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||
'blocks' => $blocks,
|
||
'button' => ['text' => '查看详情', 'url' => $url],
|
||
'link' => $url,
|
||
'recipient' => $to,
|
||
];
|
||
}
|
||
|
||
/* ---------------- 3. 审核通过 ---------------- */
|
||
elseif ($reason === 'approve') {
|
||
$subject = '【' . $site . '】你的反馈已发布:' . $short;
|
||
$layout = [
|
||
'preheader' => '你的反馈已经公开可见',
|
||
'badge' => ['text' => '已发布', 'color' => '#1e7b3c'],
|
||
'title' => '你的反馈已通过审核并公开',
|
||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||
'blocks' => [
|
||
['type' => 'text', 'text' => '你提交的反馈已通过审核,现在可以在公开列表中看到了。'],
|
||
['type' => 'field', 'rows' => ['反馈标题' => $title]],
|
||
['type' => 'callout', 'tone' => 'info',
|
||
'text' => '后续的处理进展与回复,我们会通过邮件通知你。'],
|
||
],
|
||
'button' => ['text' => '查看这条反馈', 'url' => $url],
|
||
'link' => $url,
|
||
'recipient' => $to,
|
||
];
|
||
}
|
||
|
||
/* ---------------- 4. 转为不公开 ---------------- */
|
||
elseif ($reason === 'hide') {
|
||
$note = trim((string)(isset($extra['note']) ? $extra['note'] : ''));
|
||
$subject = '【' . $site . '】你的反馈已转为不公开:' . $short;
|
||
|
||
$blocks = [
|
||
['type' => 'text', 'text' => '你提交的反馈已从公开列表中撤下,不再对其他人可见。'],
|
||
['type' => 'field', 'rows' => ['反馈标题' => $title]],
|
||
];
|
||
if ($note !== '') {
|
||
$blocks[] = ['type' => 'quote', 'who' => '官方说明:', 'text' => $note];
|
||
}
|
||
$blocks[] = ['type' => 'callout', 'tone' => 'muted',
|
||
'text' => '这条反馈本身仍然保留,你依然可以通过原链接访问和处理进展。'];
|
||
|
||
$layout = [
|
||
'preheader' => '这条反馈已不对其他人可见',
|
||
'badge' => ['text' => '不公开', 'color' => '#5c6572'],
|
||
'title' => '你的反馈已转为不公开',
|
||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||
'blocks' => $blocks,
|
||
'button' => ['text' => '查看这条反馈', 'url' => $url],
|
||
'link' => $url,
|
||
'recipient' => $to,
|
||
];
|
||
}
|
||
|
||
/* ---------------- 5. 关闭(判定不是问题) ---------------- */
|
||
elseif ($reason === 'close') {
|
||
$note = trim((string)(isset($extra['note']) ? $extra['note'] : ''));
|
||
$body = (string)$thread['body'];
|
||
$mail = trim((string)fb_cfg('admin_email', ''));
|
||
|
||
$subject = '【' . $site . '】关于你反馈的问题《' . $short . '》';
|
||
|
||
$blocks = [
|
||
['type' => 'text',
|
||
'text' => '感谢你的反馈。我们已经核实过,这个情况并不是应用本身的问题,因此这条反馈已关闭(不会公开显示)。'],
|
||
['type' => 'field', 'rows' => ['反馈标题' => $title]],
|
||
];
|
||
if ($note !== '') {
|
||
$blocks[] = ['type' => 'quote', 'who' => '官方说明:', 'text' => $note];
|
||
}
|
||
$blocks[] = ['type' => 'quote', 'who' => '你当时反馈的内容:', 'text' => $body];
|
||
$blocks[] = [
|
||
'type' => 'callout', 'tone' => 'info',
|
||
'text' => ($mail !== ''
|
||
? '如果上面的说明没能解决你的疑问,直接回复本邮件即可,我们继续跟进;也可以发到 ' . $mail . '。'
|
||
: '如果上面的说明没能解决你的疑问,直接回复本邮件即可,我们继续跟进。'),
|
||
];
|
||
|
||
$layout = [
|
||
'preheader' => '关于你反馈的问题,我们的说明',
|
||
'badge' => ['text' => '已关闭', 'color' => '#8a6a15'],
|
||
'title' => '关于你反馈的问题,我们的说明',
|
||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||
'blocks' => $blocks,
|
||
'recipient' => $to,
|
||
];
|
||
}
|
||
|
||
/* ---------------- 6. 新反馈(发给管理员) ---------------- */
|
||
elseif ($reason === 'new') {
|
||
$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'] : '');
|
||
|
||
$subject = '【' . $site . '】新反馈:' . $short;
|
||
|
||
$rows = ['标题' => $title];
|
||
if ($swName !== '') { $rows['所属软件'] = $swName; }
|
||
$rows['提交人'] = $name !== '' ? $name : '(未留名)';
|
||
if ($mail !== '') { $rows['邮箱'] = $mail; }
|
||
if ($phone !== '') { $rows['电话'] = $phone; }
|
||
|
||
$blocks = [
|
||
['type' => 'text', 'text' => '有新的反馈提交,请及时处理。'],
|
||
['type' => 'field', 'rows' => $rows],
|
||
['type' => 'quote', 'who' => '反馈内容:', 'text' => $body],
|
||
['type' => 'callout', 'tone' => 'warn',
|
||
'text' => '这条反馈还需要审核,审核通过后才会公开显示。'],
|
||
];
|
||
|
||
$layout = [
|
||
'preheader' => $name . ':' . fb_excerpt($body, 60),
|
||
'badge' => ['text' => '新反馈', 'color' => '#b4553c'],
|
||
'title' => '收到一条新反馈',
|
||
'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'],
|
||
'recipient' => $to,
|
||
'footer' => '此邮件由系统自动发送。想调整接收规则,请到后台「邮件通知」页修改。',
|
||
];
|
||
}
|
||
|
||
/* ---------------- 兜底 ---------------- */
|
||
else {
|
||
$subject = '【' . $site . '】' . $short;
|
||
$layout = [
|
||
'preheader' => '你有一条新的通知',
|
||
'title' => '你有一条新的通知',
|
||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||
'blocks' => [['type' => 'text', 'text' => '你关注的反馈有更新。']],
|
||
'button' => ['text' => '查看详情', 'url' => $url],
|
||
'link' => $url,
|
||
'recipient' => $to,
|
||
];
|
||
}
|
||
|
||
return [$subject, fb_mail_layout($layout)];
|
||
}
|
||
}
|