'noindex,nofollow']); ?>
← 返回问题列表

管理登录

请输入管理员用户名与口令(当前口令仍是 config.php 里的明文,登录后请到「站点设置」改成加密存储)。

尚未配置管理员账号,且出厂占位口令已被禁用。请运行 install.php 完成初始化。
本站只设了口令、没有用户名(旧版安装)。请运行 install.php 补一个用户名;在那之前用户名可留空、只填口令。
prepare('SELECT * FROM threads WHERE id = ?'); $st->execute([(int)$id]); $t = $st->fetch(); return is_array($t) ? $t : null; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) { $cs = isset($_POST['csrf']) ? (string)$_POST['csrf'] : ''; if (!fb_csrf_ok($cs)) { $msg = '会话已过期,请重试。'; $msgType = 'error'; } else { $id = isset($_POST['id']) ? (int)$_POST['id'] : 0; $act = (string)$_POST['act']; $t = fb_admin_thread($pdo, $id); // 这些动作不针对某一条反馈(不依赖 $t),单独放行 $noThread = in_array($act, ['settings', 'password', 'mail_test'], true) || fb_admin_is_taxonomy_act($act); if (!$t && !$noThread) { $msg = '问题不存在。'; $msgType = 'error'; } elseif ($act === 'approve') { /* 「通过并公开」= 两件事一起做: * ① 可见性:visible=1 * ② 状态:『未公开』→『待处理』 * * 状态流转是用户在 2026-11-03 明确要求的:提交后先落「未公开」, * 通过了才变「待处理」;如果判定不是问题,直接走 close 变「已关闭」, * 中间不经过「待处理」。 * * 边界:如果这条本来就已经是别的状态(比如以前误操作改成了「处理中」), * 通过审核不该把状态倒退回去,所以只在「未公开」时才推进。 * 老数据可能还是 'open' + visible=0,也一并视作待推进。 */ $oldSt = (string)$t['status']; $newStatus = ($oldSt === 'hidden' || $oldSt === 'open') ? 'open' : $oldSt; $pdo->prepare('UPDATE threads SET visible = 1, status = ?, mod_note = NULL, moderated_at = ? WHERE id = ?') ->execute([$newStatus, time(), $id]); fb_mod_log('approve', $id, '通过审核并公开(状态:' . fb_status_label($newStatus) . ')'); // 通知提交人「已发布」(工单 #2) $errA = ''; try { // 用更新后的状态渲染邮件,否则信里还写着「未公开」 $fresh = fb_admin_thread($pdo, $id); list($okA, $errA) = fb_notify_event($pdo, 'approve', $fresh ? $fresh : $t); } catch (Throwable $e) { $errA = '发信异常'; } $msg = '#' . $id . ' 已通过审核并公开,状态为「' . fb_status_label($newStatus) . '」。' . ($errA === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errA . ')'); } elseif ($act === 'close') { $note = trim(isset($_POST['note']) ? (string)$_POST['note'] : ''); if ($note === '') { $msg = '关闭前请先填写给提问人的说明(这段内容会随邮件发出)。'; $msgType = 'error'; } else { $pdo->prepare("UPDATE threads SET status = 'closed', visible = 0, mod_note = ?, moderated_at = ? WHERE id = ?") ->execute([$note, time(), $id]); list($sent, $err) = fb_notify_closed($pdo, $t, $note); fb_mod_log('close', $id, '关闭并邮件告知:' . ($sent ? '已发信' : '未发信(' . $err . ')')); $msg = $sent ? ('#' . $id . ' 已关闭,说明邮件已发送给 ' . (string)$t['author_email'] . '。') : ('#' . $id . ' 已关闭,但邮件未发出:' . $err); $msgType = $sent ? 'ok' : 'error'; } } elseif ($act === 'hide') { $pdo->prepare('UPDATE threads SET visible = 0, moderated_at = ? WHERE id = ?')->execute([time(), $id]); fb_mod_log('hide', $id, '撤下(转为不公开)'); $noteH = trim((string)(isset($_POST['note']) ? $_POST['note'] : '')); $errH = ''; try { list($okH, $errH) = fb_notify_event($pdo, 'hide', $t, ['note' => $noteH]); } catch (Throwable $e) { $errH = '发信异常'; } $msg = '#' . $id . ' 已撤下(转为不公开)。' . ($errH === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errH . ')'); } elseif ($act === 'setstatus') { /* 问题详情页顶部那一排按钮走的动作。 * * 与 status 的区别: * - status 是「状态下拉框 + 公开勾选」的完整表单,需要手动选 * - setstatus 是「点一下立刻生效」,状态与可见性由按钮本身决定 * * 「通过并公开」= 可见 + 状态保持;其余状态只改状态不动可见性, * 避免误把已公开的问题撤下来(用户没这个意图)。 */ $statuses = fb_statuses(); $new = (string)(isset($_POST['status']) ? $_POST['status'] : ''); $note = trim((string)(isset($_POST['note']) ? $_POST['note'] : '')); $alsoShow = !empty($_POST['also_show']); // 「通过并公开」置 1 if (!isset($statuses[$new])) { $msg = '未知的状态。'; $msgType = 'error'; } else { $old = (string)$t['status']; $vis = $alsoShow ? 1 : (int)$t['visible']; $pdo->prepare('UPDATE threads SET status = ?, visible = ?, updated_at = ? WHERE id = ?') ->execute([$new, $vis, time(), $id]); fb_mod_log('status', $id, '一键改为 ' . $statuses[$new] . ($alsoShow ? ',并公开' : '') . ($note !== '' ? ',说明:' . fb_excerpt($note, 60) : '')); $errS = ''; if ($old !== $new || $alsoShow) { try { // 「通过并公开」用 approve 事件(文案是「已发布」) $reason = $alsoShow ? 'approve' : 'status'; list($okS, $errS) = fb_notify_event($pdo, $reason, $t, [ 'old_status' => $old, 'new_status' => $new, 'note' => $note, ]); } catch (Throwable $e) { $errS = '发信异常'; } } $msg = '#' . $id . ' 已改为「' . $statuses[$new] . '」' . ($alsoShow ? '并公开显示' : '') . '。' . ($old !== $new || $alsoShow ? ($errS === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errS . ')') : '状态未变化,未发信。'); $msgType = $errS === '' ? 'ok' : 'error'; } } elseif ($act === 'status') { $statuses = fb_statuses(); $new = (string)(isset($_POST['status']) ? $_POST['status'] : ''); if (!isset($statuses[$new])) { $msg = '未知的状态。'; $msgType = 'error'; } else { $vis = isset($_POST['visible']) ? 1 : 0; $old = (string)$t['status']; $note = trim((string)(isset($_POST['note']) ? $_POST['note'] : '')); $pdo->prepare('UPDATE threads SET status = ?, visible = ?, updated_at = ? WHERE id = ?') ->execute([$new, $vis, time(), $id]); fb_mod_log('status', $id, '状态改为 ' . $statuses[$new] . ',公开=' . $vis); // 状态变了才发信(没变就别打扰用户) $errS = ''; if ($old !== $new) { try { list($okS, $errS) = fb_notify_event($pdo, 'status', $t, [ 'old_status' => $old, 'new_status' => $new, 'note' => $note, ]); } catch (Throwable $e) { $errS = '发信异常'; } } $msg = '#' . $id . ' 已更新:状态「' . $statuses[$new] . '」,' . ($vis ? '公开' : '不公开') . '。' . ($old !== $new ? ($errS === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errS . ')') : '状态未变化,未发信。'); } } elseif ($act === 'reply') { $body = trim(isset($_POST['body']) ? (string)$_POST['body'] : ''); if (!fb_valid_body($body)) { $msg = '回复内容需要 2~5000 个字符。'; $msgType = 'error'; } else { $now = time(); $statuses = fb_statuses(); $new = (string)(isset($_POST['status']) ? $_POST['status'] : ''); $setStatus = isset($statuses[$new]) ? $new : null; $ins = $pdo->prepare('INSERT INTO replies(thread_id, body, author_name, author_email, is_admin, created_at, ip_hash) VALUES(?,?,?,?,?,?,?)'); $ins->execute([$id, $body, '官方', null, 1, $now, fb_ip_hash()]); $replyId = (int)$pdo->lastInsertId(); if ($setStatus !== null) { $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ?, status = ? WHERE id = ?') ->execute([$now, $setStatus, $id]); } else { $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?') ->execute([$now, $id]); } $replyRow = [ 'id' => $replyId, 'body' => $body, 'author_name' => '官方', 'author_email' => null, 'is_admin' => 1, ]; $err2 = fb_notify_reply($pdo, $t, $replyRow, true); fb_mod_log('reply', $id, '官方回复' . ($setStatus !== null ? ',状态改为 ' . $statuses[$setStatus] : '')); $msg = '#' . $id . ' 已以官方身份回复' . ($err2 === '' ? '' : '(邮件:' . $err2 . ')') . '。'; } } elseif ($act === 'delete') { $st = $pdo->prepare('SELECT oss_key FROM attachments WHERE thread_id = ?'); $st->execute([$id]); foreach ($st->fetchAll() as $r) { fb_attach_delete((string)$r['oss_key']); } $pdo->prepare('DELETE FROM threads WHERE id = ?')->execute([$id]); fb_mod_log('delete', $id, '删除问题:' . fb_excerpt((string)$t['title'], 40)); $msg = '#' . $id . ' 已删除。'; } elseif ($act === 'mail_test') { // 发送测试邮件:只验证 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', '轻反馈'); // 选中的软件(没选就用默认软件,保证测试邮件也带软件标识) $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'; } else { $msg = '发送失败:' . $err; $msgType = 'error'; } } } elseif (fb_admin_is_taxonomy_act($act)) { list($msg, $msgType) = fb_admin_taxonomy_action($pdo, $act, $_POST); } elseif ($act === 'settings') { $msg = fb_admin_save_settings(); } elseif ($act === 'password') { $old = (string)(isset($_POST['old_password']) ? $_POST['old_password'] : ''); $n1 = (string)(isset($_POST['new_password']) ? $_POST['new_password'] : ''); $n2 = (string)(isset($_POST['new_password2']) ? $_POST['new_password2'] : ''); // 从没设过口令(或仍是出厂占位值)时,跳过"当前口令"校验 —— 否则谁也进不来、又改不了 $firstSet = !fb_admin_password_is_set(); if (!$firstSet && !fb_admin_verify_password($old)) { $msg = '当前口令不正确。'; $msgType = 'error'; } elseif (fb_len($n1) < 8) { $msg = '新口令至少 8 个字符。'; $msgType = 'error'; } elseif ($n1 !== $n2) { $msg = '两次输入的新口令不一致。'; $msgType = 'error'; } else { fb_admin_set_password($n1); fb_mod_log('password', null, $firstSet ? '首次设定登录口令(bcrypt 哈希存库)' : '修改登录口令'); $msg = $firstSet ? '口令已设定,下次登录请使用它。' : '口令已修改,下次登录请使用新口令。'; } } } } /** * 保存站点设置(把表单值写进 settings 表;config.php 仍是出厂默认值) * @return string 提示语 */ function fb_admin_save_settings() { // PHP 会把表单字段名里的「.」替换成「_」,所以两种写法都读一遍 $post = function ($k, $def = null) { if (isset($_POST[$k])) { return $_POST[$k]; } $alt = str_replace('.', '_', $k); if (isset($_POST[$alt])) { return $_POST[$alt]; } return $def; }; /** * 这个字段本次请求到底有没有被提交? * * 🔴 为什么必须有这个:后台有多个 tab(站点设置 / 邮件通知), * 但它们**共用同一个保存处理**(都提交 act=settings)。 * 如果在站点设置页点保存,该表单里根本没有 smtp_* 字段, * `$post('smtp.host', '')` 会取到空串,然后被写进数据库 —— * **把用户之前填好的邮件配置清空**。 * * 同类事故还会发生在 checkbox 上:没提交 = 视为取消勾选, * 于是另一个 tab 保存一次就把通知开关全关掉。 * * 所以写库前一律先问 has():没提交就跳过,绝不写空值。 */ $has = function ($k) { if (array_key_exists($k, $_POST)) { return true; } return array_key_exists(str_replace('.', '_', $k), $_POST); }; /** * 🔴🔴 受守卫的写库:**字段没提交就一个字都不写**。 * * 这是本文件唯一允许写设置的入口(新增设置项也必须走它)。 * 为什么要把这件事做成强制: * 这个 bug 已经犯过三次 —— 每次都是「某个 tab 保存时, * 另一个 tab 的字段没提交,被当成空值写回去」。 * 根因不是某个字段忘了判断,而是**写入入口本身没有约束**: * 32 处 fb_set_setting 只有 8 处手写了 has(),漏一处就是一次事故。 * * 用法: * $save('oss.bucket', trim((string)$post('oss.bucket', ''))); * $save('oss.enabled', $storage === 'oss' ? 1 : 0); * 字段没提交 → 直接 return false,数据库不动。 * * ⚠️ 少数场景确实要在「字段没提交」时也写(例如用户主动清空某项), * 那时用 $saveForce(),但必须在注释里写清楚为什么。 */ $save = function ($k, $v) use ($has) { if (!$has($k)) { return false; } // ← 铁律:没提交就不动 return fb_set_setting($k, $v); }; /** 显式强制写入(字段没提交也写),仅在「清空某项」这类确有必要时用 */ $saveForce = function ($k, $v) { return fb_set_setting($k, $v); }; // ============ 站点设置组(含站点信息/页脚/附件/OSS)============ // 🔴 整组包在 has() 判断里:邮件页只提交邮件字段,没有 site_*/oss_*/upload_*, // 无条件写入会把它们全清空。反过来也一样。 $isSiteForm = $has('oss_endpoint') || $has('site_name') || $has('attachment_storage') || $has('site_desc'); if ($isSiteForm) { // 站点信息(对外展示,用于说明「本系统为哪个软件提供反馈服务」) // 留空 = 删除该设置项,回落到 config.php 的出厂值,便于恢复默认。 foreach (['site_name', 'site_desc'] as $k) { if (!$has($k)) { continue; } $v = trim((string)$post($k, '')); if ($v === '') { fb_unset_setting($k); } else { $save($k, $v); } } // 页脚:是否显示「由 FeedLite 驱动」这句版权,以及链接指向的开源仓库地址 $save('footer.show_credit', $post('footer_show_credit') ? 1 : 0); $curl = trim((string)$post('footer_credit_url', '')); // 只接受 http/https:这条会被直接渲染成 ,不能让 javascript:/data: 混进来 if ($curl !== '' && !preg_match('~^https?://~i', $curl)) { return '页脚链接地址需以 http:// 或 https:// 开头。'; } if ($curl === '' || $curl === fb_credit_url_default()) { fb_unset_setting('footer.credit_url'); } else { $save('footer.credit_url', $curl); } $ints = [ // 区间下限统一改 0:0 = 不限制(见 #4)。 // 只有 image_max_side 例外 —— 边长 0 没有意义,它是压缩目标不是限制。 'upload.image_max_per_post' => [0, 1000], 'upload.video_max_per_post' => [0, 1000], 'upload.retention_days' => [0, 36500], 'upload.upload_per_hour' => [0, 100000], 'upload.image_max_side' => [480, 4096], ]; foreach ($ints as $k => $range) { if (!$has($k)) { continue; } $v = (int)$post($k, 0); $v = max($range[0], min($range[1], $v)); $save($k, $v); } // 图片/视频上传总开关(0=不限个数后,开关必须独立存在) $save('upload.image_on', (int)(bool)$post('upload.image_on')); $save('upload.video_on', (int)(bool)$post('upload.video_on')); // MB → 字节 // 下限改 0 = 不限制(见 #4) $mbs = [ 'upload.image_max_bytes' => [0, 1024], 'upload.video_max_bytes' => [0, 10240], 'upload.total_cap_bytes' => [0, 1048576], ]; foreach ($mbs as $k => $range) { if (!$has($k)) { continue; } // 没提交就不动(防跨 tab 清空) $mb = (float)$post($k, 0); $mb = max($range[0], min($range[1], $mb)); $save($k, (int)round($mb * 1048576)); } // 附件存储方式:只有表单真的带了这个字段才处理。 // 没有它(比如只提交了 site_name 的局部表单)就整段跳过 —— // 否则下面 $post(..., 'off') 的默认值会把存储方式误改成「关闭」。 if ($has('attachment.storage')) { $storage = (string)$post('attachment.storage', 'off'); if (!in_array($storage, ['off', 'local', 'oss'], true)) { $storage = 'off'; } $save('attachment.storage', $storage); // OSS 模式下 oss.enabled 必须跟着开;其它模式关掉,避免误判。 // 🔴 这里必须用 $saveForce,不能用 $save: // oss.enabled 是【派生字段】—— 表单里根本没有名为 oss_enabled 的 input, // $save 的 $has() 守卫会判定"没提交"而直接 return,于是 storage 存成了 oss、 // enabled 却永远停在旧值 0。media.php 因此报「对象存储未配置」,全站图片 503。 // (2026-11-03 图片全挂的直接原因:守卫修好了跨 tab 清空,却把派生写入也挡掉了。) // 又因为它处在 $has('attachment.storage') 分支内,不会出现「没提交却强制写 0」的反向问题。 $saveForce('oss.enabled', $storage === 'oss' ? 1 : 0); } $texts = ['oss.endpoint', 'oss.bucket', 'oss.instance_role', 'oss.domain', 'oss.prefix', 'moderation.close_mail_subject']; foreach ($texts as $k) { if (!$has($k)) { continue; } // 没提交就不动 $v = trim((string)$post($k, '')); // 这两个留空都有"出厂默认"兜底,必须删键而不是存空串: // - oss.prefix 留空会让对象键变成 "/2026/...",默认是 'fb' // - close_mail_subject 留空会让邮件标题变成空字符串, // sprintf('') 出来的主题是空的,必须回落到默认模板 if (($k === 'oss.prefix' || $k === 'moderation.close_mail_subject') && $v === '') { fb_unset_setting($k); continue; } // RAM 角色留空 = 让程序自己去元数据服务探测,不写死一个空值 if ($k === 'oss.instance_role' && $v === '') { list($rOk, $rVal) = fb_oss_detect_instance_role(); if ($rOk) { $save($k, (string)$rVal); } continue; // 探测不到就保持原值不动 } $save($k, $v); } $src = (string)$post('oss.credential_source', 'instance_role'); $save('oss.credential_source', $src === 'static' ? 'static' : 'instance_role'); $save('oss.access_key', trim((string)$post('oss.access_key', ''))); $sk = trim((string)$post('oss.secret_key', '')); if ($sk !== '') { $save('oss.secret_key', $sk); } $save('oss.visibility', ((string)$post('oss.visibility', 'public')) === 'private' ? 'private' : 'public'); $save('oss.url_scheme', ((string)$post('oss.url_scheme', 'https')) === 'http' ? 'http' : 'https'); $save('oss.verify', $post('oss.verify') ? 1 : 0); $save('oss.image_process', $post('oss.image_process') ? 1 : 0); $save('oss.use_internal_endpoint', $post('oss.use_internal_endpoint') ? 1 : 0); } // ← end if ($isSiteForm) /* ---------------- 邮件通知(SMTP) ---------------- * * 🔴🔴 必须整段包在「本页到底有没有提交邮件表单」的判断里。 * * 事故:站点设置页和邮件通知页共用 act=settings。在站点设置页点保存时, * 该表单里没有 smtp_* 字段,$post() 取到空串,被无条件写进数据库 —— * **把用户已经填好的 SMTP 服务器/账号/发件地址全清空了**。 * 用户看到的现象就是「我明明配过邮件,怎么全没了、信也发不出」。 * * 判据用 smtp_enabled:它是邮件表单里的总开关,一定存在。 * 站点设置页没有这个字段 ⇒ 整段跳过,一个键都不动。 */ $isMailForm = $has('smtp_enabled') || $has('smtp.host') || $has('smtp_host'); if ($isMailForm) { $smtpText = ['smtp.host', 'smtp.user', 'smtp.from', 'smtp.from_name']; foreach ($smtpText as $k) { if (!$has($k)) { continue; } // 没提交就不动它 $save($k, trim((string)$post($k, ''))); } // 口令留空 = 不修改(避免每次保存都得重填) $sp = (string)(isset($_POST['smtp_pass']) ? $_POST['smtp_pass'] : ''); if (trim($sp) !== '') { $save('smtp.pass', $sp); } // 端口留空 = 用默认 465。删键回落,不要存成 0(0 端口根本连不上) $portRaw = trim((string)$post('smtp.port', '')); if ($portRaw === '') { fb_unset_setting('smtp.port'); } else { $smtpPort = (int)$portRaw; if ($smtpPort <= 0 || $smtpPort > 65535) { $smtpPort = 465; } $save('smtp.port', $smtpPort); } $sec = strtolower(trim((string)$post('smtp.secure', 'ssl'))); if (!in_array($sec, ['ssl', 'tls', ''], true)) { $sec = 'ssl'; } $save('smtp.secure', $sec); // 超时留空 = 用默认 15 $tmoRaw = trim((string)$post('smtp.timeout', '')); if ($tmoRaw === '') { fb_unset_setting('smtp.timeout'); } else { $save('smtp.timeout', max(5, min(60, (int)$tmoRaw))); } $save('smtp.verify_ssl', (int)(bool)$post('smtp.verify_ssl')); // 没填 from 时用 user 兜底(绝大多数 SMTP 要求两者一致) if ($has('smtp.from') && trim((string)$post('smtp.from', '')) === '') { $save('smtp.from', trim((string)fb_cfg('smtp.user', ''))); } // 只有 host 填了才允许开启,否则开着一个空配置只会每次发信都失败 $wantOn = (bool)$post('smtp.enabled'); $hostNow = trim((string)fb_cfg('smtp.host', '')); $save('smtp.enabled', ($wantOn && $hostNow !== '') ? 1 : 0); if ($wantOn && $hostNow === '') { $msg = '邮件通知未开启:请先填写 SMTP 服务器地址。'; $msgType = 'error'; } /* ---------------- 通知开关 ---------------- * 全是 checkbox:未提交会被当成"取消勾选"。 * 所以只有本页确实提交了邮件表单时才写,否则在站点设置页保存一次 * 就会把「有人回复时通知」这些开关全部关掉。 */ $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" if ($has('notify_cooldown_seconds')) { $cdRaw = trim((string)$post('notify.cooldown_seconds', '')); if ($cdRaw === '') { fb_unset_setting('notify.cooldown_seconds'); } else { $save('notify.cooldown_seconds', max(0, min(86400, (int)$cdRaw))); } } } // ← end if ($isMailForm) // 审核开关属于「站点设置」,只在站点设置页提交时才写 if ($has('moderation_enabled')) { $save('moderation.enabled', $post('moderation.enabled') ? 1 : 0); } fb_mod_log('settings', null, '保存站点设置:存储=' . $storage); return '设置已保存。'; } /* ================================================================== * 页面 * ================================================================== */ $tab = (string)(isset($_GET['tab']) ? $_GET['tab'] : 'pending'); if (!in_array($tab, ['pending', 'all', 'thread', 'taxonomy', 'statuses', 'settings', 'mail', 'log'], true)) { $tab = 'pending'; } fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']); ?>

管理后台

登录状态 小时内有效,超时自动退出。

退出登录
query( "SELECT * FROM threads WHERE status = 'hidden' OR (visible = 0 AND status = 'open') ORDER BY created_at DESC" )->fetchAll(); ?>

待审核 ()

当前没有待审核的反馈。

0) { $where[] = 'software_id = ?'; $args[] = $swF; } if ($cidF > 0) { $where[] = 'category_id = ?'; $args[] = $cidF; } if ($qF !== '') { $where[] = '(title LIKE ? OR body LIKE ?)'; $args[] = '%' . $qF . '%'; $args[] = '%' . $qF . '%'; } $wSql = $where ? ('WHERE ' . implode(' AND ', $where)) : ''; $cntSt = $pdo->prepare('SELECT COUNT(*) FROM threads ' . $wSql); $cntSt->execute($args); $total = (int)$cntSt->fetchColumn(); $pageSize = max(5, (int)fb_cfg('page_size', 20)); $pages = max(1, (int)ceil($total / $pageSize)); $page = min(max(1, (int)(isset($_GET['page']) ? $_GET['page'] : 1)), $pages); $offset = ($page - 1) * $pageSize; $lst = $pdo->prepare('SELECT * FROM threads ' . $wSql . ' ORDER BY updated_at DESC LIMIT ' . (int)$pageSize . ' OFFSET ' . (int)$offset); $lst->execute($args); $rows = $lst->fetchAll(); ?>
重置

全部问题 ( 条,第 / 页)

没有符合条件的问题。

#
未公开

· · 条回复 · 管理
1): ?>
1): ?>上一页 下一页

问题不存在。返回列表

prepare('SELECT * FROM replies WHERE thread_id = ? ORDER BY created_at ASC, id ASC'); $rs->execute([$id]); $replies = $rs->fetchAll(); $atts = fb_attach_for_thread($pdo, $id); ?>
← 返回全部问题
#
· · · ·
'doing', 'label' => '处理中', 'cls' => ''], ['key' => 'resolved', 'label' => '已解决', 'cls' => 'ok'], ['key' => 'wontfix', 'label' => '不予考虑', 'cls' => 'warn'], ]; ?>
处理
已公开显示
点一下立刻生效并自动发邮件通知提交人
官方 ·
true]); } ?>

归属

挂错软件或分类时在这里改正。换软件后分类选项会跟着变,需重新选择。

以官方身份回复

状态与可见性

关闭(判定不是应用问题,会发邮件给提问人)

站点信息

页脚

附件存储

填 0 表示不限制。'; ?>

阿里云 OSS(选「OSS」时才需要填)

私有桶 + 服务器代理:浏览器不直连桶,附件读写都经本站 upload.php / media.php 中转。Endpoint 填公网或内网形式都可以,程序按下方开关自动转换。

ECS 实例 RAM 角色
已绑定: (自动从实例元数据读取,无需手填)
自动探测到: 保存时若留空就自动用这个
手动指定角色名(一般不需要)

审核

存储体检

当前存储方式: 上传目录: — 鉴权方式: 凭证状态: Bucket: Endpoint(配置): Endpoint(实际连接): — 连通性: ↳ 内网 Endpoint 跨地域不可达:请取消勾选上方「使用内网 Endpoint」后保存,或把桶换到与本机同地域。 测一下桶连通性 (会真实访问一次 OSS,跨地域不通时可能等待数秒) 已用附件: / 上限

登录口令

存储位置: 数据库 settings 表(键名 admin.password_hash) config.php 的 admin_password 尚未设置 是否加密: 是 —— 存的是 password_hash() 生成的 bcrypt 单向哈希($2y$…), 不可逆、无法还原明文;校验用 password_verify()。 否 —— 目前是明文写在 config.php 里。 在下面改一次口令后即会转存为数据库中的 bcrypt 哈希。 —— 还没有可用口令,当前后台无法登录。请用下方表单设定一个。 数据库文件在 data/feedback.db(已由 .htaccess 拒绝外部访问),且只存哈希,明文不落盘。

新口令以 bcrypt 哈希写入数据库,优先级高于 config.php 的 admin_password;改完立即生效,明文不会留在任何文件里。

邮件通知

配置 SMTP 后,反馈有变动会自动发信通知提交人。

邮件通知

高级选项
配好后先发一封测试邮件确认能通:

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

通知规则

← 回站点设置
query('SELECT * FROM mod_log ORDER BY created_at DESC, id DESC LIMIT 100')->fetchAll(); ?>

最近操作 ( 条)

还没有操作记录。

#

#

· ·