false, 'error' => 'invalid token'], 403); } $pdo = fb_db(); $action = isset($_GET['action']) ? (string)$_GET['action'] : 'list'; $contacts = !empty($_GET['contacts']); $limit = isset($_GET['limit']) ? max(1, min(500, (int)$_GET['limit'])) : 100; $cats = fb_categories(); $statuses = fb_statuses(); /** 过滤条件 */ function fb_api_where(&$args, $cats, $statuses) { $w = []; if (!empty($_GET['status']) && isset($statuses[(string)$_GET['status']])) { $w[] = 'status = ?'; $args[] = (string)$_GET['status']; } if (!empty($_GET['category']) && isset($cats[(string)$_GET['category']])) { $w[] = 'category = ?'; $args[] = (string)$_GET['category']; } if (!empty($_GET['since'])) { $w[] = 'updated_at >= ?'; $args[] = (int)$_GET['since']; } return $w ? (' WHERE ' . implode(' AND ', $w)) : ''; } /** 主题行 -> 对外结构 */ function fb_api_thread(array $t, $contacts) { $out = [ 'id' => (int)$t['id'], 'title' => $t['title'], 'category' => $t['category'], 'category_cn' => fb_category_label($t['category']), 'status' => $t['status'], 'status_cn' => fb_status_label($t['status']), 'author' => $t['author_name'], 'reply_count' => (int)$t['reply_count'], 'created_at' => (int)$t['created_at'], 'created_time' => date('Y-m-d H:i', (int)$t['created_at']), 'updated_at' => (int)$t['updated_at'], 'updated_time' => date('Y-m-d H:i', (int)$t['updated_at']), 'url' => fb_base_url() . '/thread.php?id=' . (int)$t['id'], ]; if (isset($t['body'])) { $out['body'] = $t['body']; } if ($contacts) { $out['author_email'] = isset($t['author_email']) ? $t['author_email'] : null; $out['author_phone'] = isset($t['author_phone']) ? $t['author_phone'] : null; } return $out; } function fb_api_reply(array $r, $contacts) { $out = [ 'id' => (int)$r['id'], 'author' => !empty($r['is_admin']) ? '官方' : $r['author_name'], 'is_official'=> !empty($r['is_admin']), 'created_at' => (int)$r['created_at'], 'created_time' => date('Y-m-d H:i', (int)$r['created_at']), 'body' => $r['body'], ]; if ($contacts && !empty($r['author_email'])) { $out['author_email'] = $r['author_email']; } return $out; } /** 单条附件的对外结构(不含任何本地路径) */ function fb_api_attach(array $a) { $kind = (string)$a['kind']; $key = (string)$a['oss_key']; $out = [ 'id' => (int)$a['id'], 'kind' => $kind, 'name' => isset($a['orig_name']) ? $a['orig_name'] : null, 'mime' => isset($a['mime']) ? $a['mime'] : null, 'size' => (int)$a['size'], 'url' => fb_attach_url($key), ]; if ($kind === 'image') { $out['thumb'] = fb_attach_thumb_url($key, 640); $out['width'] = isset($a['width']) ? (int)$a['width'] : null; $out['height'] = isset($a['height']) ? (int)$a['height'] : null; } return $out; } /** 取主题的全部附件:[$mainList, [reply_id => [..]]] */ function fb_api_attach_rows(PDO $pdo, $threadId) { $st = $pdo->prepare('SELECT * FROM attachments WHERE thread_id = ? ORDER BY seq ASC, id ASC'); $st->execute([(int)$threadId]); $main = []; $rep = []; foreach ($st->fetchAll() as $r) { $a = fb_api_attach($r); if (empty($r['reply_id'])) { $main[] = $a; } else { $rep[(int)$r['reply_id']][] = $a; } } return [$main, $rep]; } /** 拉取一个主题的全部回复 */ function fb_api_replies(PDO $pdo, $threadId, $contacts, array $attByReply = []) { $st = $pdo->prepare('SELECT * FROM replies WHERE thread_id = ? ORDER BY created_at ASC, id ASC'); $st->execute([(int)$threadId]); $out = []; foreach ($st->fetchAll() as $r) { $item = fb_api_reply($r, $contacts); if (!empty($attByReply[(int)$r['id']])) { $item['attachments'] = $attByReply[(int)$r['id']]; } $out[] = $item; } return $out; } /* -------------------- 路由 -------------------- */ if ($action === 'stats') { $byStatus = []; foreach ($pdo->query('SELECT status, COUNT(*) c FROM threads GROUP BY status')->fetchAll() as $r) { $byStatus[$r['status']] = (int)$r['c']; } $byCat = []; foreach ($pdo->query('SELECT category, COUNT(*) c FROM threads GROUP BY category')->fetchAll() as $r) { $byCat[$r['category']] = (int)$r['c']; } fb_api_json([ 'ok' => true, 'total' => (int)$pdo->query('SELECT COUNT(*) FROM threads')->fetchColumn(), 'replies' => (int)$pdo->query('SELECT COUNT(*) FROM replies')->fetchColumn(), 'attachments'=> (int)$pdo->query('SELECT COUNT(*) FROM attachments')->fetchColumn(), 'attach_bytes' => fb_attach_total_bytes($pdo), 'by_status' => $byStatus, 'by_category'=> $byCat, 'server_time'=> date('c'), ]); } if ($action === 'thread') { $id = isset($_GET['id']) ? (int)$_GET['id'] : 0; if ($id <= 0) { fb_api_json(['ok' => false, 'error' => 'id required'], 400); } $st = $pdo->prepare('SELECT * FROM threads WHERE id = ?'); $st->execute([$id]); $t = $st->fetch(); if (!$t) { fb_api_json(['ok' => false, 'error' => 'not found'], 404); } list($mainAtt, $repAtt) = fb_api_attach_rows($pdo, $id); $data = fb_api_thread($t, $contacts); $data['attachments'] = $mainAtt; $data['replies'] = fb_api_replies($pdo, $id, $contacts, $repAtt); fb_api_json(['ok' => true, 'thread' => $data]); } if ($action === 'digest') { $args = []; $w = fb_api_where($args, $cats, $statuses); $sql = 'SELECT * FROM threads' . $w . ' ORDER BY updated_at DESC LIMIT ' . $limit; $st = $pdo->prepare($sql); $st->execute($args); $rows = $st->fetchAll(); header('Content-Type: text/markdown; charset=utf-8'); header('Cache-Control: no-store'); echo '# ' . fb_cfg('site_name', '轻反馈') . " · 反馈摘要\n\n"; echo '- 生成时间:' . date('Y-m-d H:i') . "\n"; echo '- 筛选:状态=' . (isset($_GET['status']) ? $_GET['status'] : '全部') . ',分类=' . (isset($_GET['category']) ? $_GET['category'] : '全部') . "\n"; echo '- 条数:' . count($rows) . "\n\n---\n\n"; foreach ($rows as $t) { echo '## #' . (int)$t['id'] . ' [' . fb_category_label($t['category']) . '] ' . $t['title'] . "\n\n"; echo '- 状态:' . fb_status_label($t['status']) . "\n"; echo '- 发起人:' . $t['author_name'] . '(' . date('Y-m-d H:i', (int)$t['created_at']) . ")\n"; echo '- 链接:' . fb_base_url() . '/thread.php?id=' . (int)$t['id'] . "\n"; list($mainAtt, ) = fb_api_attach_rows($pdo, $t['id']); if ($mainAtt) { echo '- 附件:' . count($mainAtt) . " 个\n"; foreach ($mainAtt as $a) { echo ' - [' . $a['kind'] . '] ' . $a['url'] . "\n"; } } echo "\n" . $t['body'] . "\n\n"; foreach (fb_api_replies($pdo, $t['id'], false) as $r) { echo '> **' . $r['author'] . '**(' . $r['created_time'] . ")\n>\n"; echo '> ' . str_replace("\n", "\n> ", $r['body']) . "\n\n"; } echo "---\n\n"; } exit; } /* 默认:list / export */ $args = []; $w = fb_api_where($args, $cats, $statuses); $st = $pdo->prepare('SELECT * FROM threads' . $w . ' ORDER BY updated_at DESC LIMIT ' . $limit); $st->execute($args); $rows = $st->fetchAll(); $tids = []; foreach ($rows as $t) { $tids[] = (int)$t['id']; } $counts = $tids ? fb_attach_count_map($pdo, $tids) : []; $threads = []; foreach ($rows as $t) { $item = fb_api_thread($t, $contacts); $item['attachment_count'] = isset($counts[(int)$t['id']]) ? $counts[(int)$t['id']] : 0; if ($action === 'export') { list($mainAtt, $repAtt) = fb_api_attach_rows($pdo, $t['id']); $item['body'] = $t['body']; $item['attachments'] = $mainAtt; $item['replies'] = fb_api_replies($pdo, $t['id'], $contacts, $repAtt); } $threads[] = $item; } fb_api_json([ 'ok' => true, 'action' => $action, 'count' => count($threads), 'threads' => $threads, ]);