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

管理登录

请输入管理员口令(初始口令在 config.php 的 admin_password,登录后在「站点设置」里可以改掉它)。

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); if (!$t && $act !== 'settings' && $act !== 'password') { $msg = '问题不存在。'; $msgType = 'error'; } elseif ($act === 'approve') { $newStatus = ((string)$t['status'] === 'closed') ? 'open' : (string)$t['status']; $pdo->prepare('UPDATE threads SET visible = 1, status = ?, mod_note = NULL, moderated_at = ? WHERE id = ?') ->execute([$newStatus, time(), $id]); fb_mod_log('approve', $id, '通过审核并公开'); $msg = '#' . $id . ' 已通过审核,现已公开显示。'; } 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, '撤下(转为不公开)'); $msg = '#' . $id . ' 已撤下(转为不公开)。'; } 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; $pdo->prepare('UPDATE threads SET status = ?, visible = ?, updated_at = ? WHERE id = ?') ->execute([$new, $vis, time(), $id]); fb_mod_log('status', $id, '状态改为 ' . $statuses[$new] . ',公开=' . $vis); $msg = '#' . $id . ' 已更新:状态「' . $statuses[$new] . '」,' . ($vis ? '公开' : '不公开') . '。'; } } 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 === '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'] : ''); if (!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'); $msg = '口令已修改,下次登录请使用新口令。'; } } } } /** * 保存站点设置(把表单值写进 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; }; // 站点信息(对外展示,用于说明「本系统为哪个软件提供反馈服务」) // 留空 = 删除该设置项,回落到 config.php 的出厂值,便于恢复默认。 foreach (['site_name', 'site_desc'] as $k) { $v = trim((string)$post($k, '')); if ($v === '') { fb_unset_setting($k); } else { fb_set_setting($k, $v); } } $ints = [ 'upload.image_max_per_post' => [0, 20], 'upload.video_max_per_post' => [0, 10], 'upload.retention_days' => [1, 3650], 'upload.upload_per_hour' => [1, 2000], 'upload.image_max_side' => [480, 4096], ]; foreach ($ints as $k => $range) { $v = (int)$post($k, 0); $v = max($range[0], min($range[1], $v)); fb_set_setting($k, $v); } // MB → 字节 $mbs = [ 'upload.image_max_bytes' => [0.2, 50], 'upload.video_max_bytes' => [1, 500], 'upload.total_cap_bytes' => [10, 100000], ]; foreach ($mbs as $k => $range) { $mb = (float)$post($k, 0); $mb = max($range[0], min($range[1], $mb)); fb_set_setting($k, (int)round($mb * 1048576)); } $storage = (string)$post('attachment.storage', 'off'); if (!in_array($storage, ['off', 'local', 'oss'], true)) { $storage = 'off'; } fb_set_setting('attachment.storage', $storage); // OSS 模式下 oss.enabled 必须跟着开;其它模式关掉,避免误判 fb_set_setting('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) { $v = trim((string)$post($k, '')); // 对象键前缀留空会让对象键变成 "/2026/...",回落到出厂默认 'fb' if ($k === 'oss.prefix' && $v === '') { fb_unset_setting($k); continue; } fb_set_setting($k, $v); } $src = (string)$post('oss.credential_source', 'instance_role'); fb_set_setting('oss.credential_source', $src === 'static' ? 'static' : 'instance_role'); fb_set_setting('oss.access_key', trim((string)$post('oss.access_key', ''))); $sk = trim((string)$post('oss.secret_key', '')); if ($sk !== '') { fb_set_setting('oss.secret_key', $sk); } fb_set_setting('oss.visibility', ((string)$post('oss.visibility', 'public')) === 'private' ? 'private' : 'public'); fb_set_setting('oss.url_scheme', ((string)$post('oss.url_scheme', 'https')) === 'http' ? 'http' : 'https'); fb_set_setting('oss.verify', $post('oss.verify') ? 1 : 0); fb_set_setting('oss.image_process', $post('oss.image_process') ? 1 : 0); fb_set_setting('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', 'settings', 'log'], true)) { $tab = 'pending'; } fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']); ?>

管理后台

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

退出登录
query( "SELECT id, title, body, category, status, author_name, author_email, created_at FROM threads WHERE visible = 0 AND status <> 'closed' ORDER BY created_at DESC" )->fetchAll(); ?>

待审核 ()

当前没有待审核的反馈。

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); ?>
← 返回全部问题
#
· · · ·
官方 ·
true]); } ?>

以官方身份回复

状态与可见性

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

站点信息

附件存储

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

审核

存储体检

当前存储方式: 上传目录: — 鉴权方式: 凭证状态: Bucket: / Endpoint: 已用附件: / 上限

修改登录口令

新口令保存在数据库里,优先级高于 config.php 的 admin_password;改完立即生效。

query('SELECT * FROM mod_log ORDER BY created_at DESC, id DESC LIMIT 100')->fetchAll(); ?>

最近操作 ( 条)

还没有操作记录。

#

#
· · · 进入管理