FeedLite 初始提交:通用轻量级反馈系统(PHP + SQLite,零依赖)

- 前台:问题列表 / 详情 / 发布与回复(趣味问答验证码、蜜罐、IP 限流)
- 后台:先审后显、站点设置(站点标题/描述、附件存储、OSS 参数)、操作日志
- 附件:关闭 / 本机硬盘 / 阿里云 OSS 浏览器直传(ECS 实例 RAM 角色)三种模式
- 品牌资源:全套图标(favicon.ico / SVG / PNG 各尺寸 / manifest)
- 安全:CSRF、session 加固、.htaccess 屏蔽敏感文件、口令仅存哈希
- 配置:config.example.php 为模板,config.php 不入库
This commit is contained in:
2026-09-28 20:34:37 +08:00
commit cede838b5e
42 changed files with 5911 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* 维护脚本(建议每天跑一次)
* ------------------------------------------------------------------
* 1) 清理超过保留期的附件(先删 OSS 对象,再删库记录)
* 2) 补发失败的通知邮件
*
* 用法(宝塔 → 计划任务 → Shell 脚本):
* php /www/wwwroot/xxx/feedback/cron.php --token=你的api_token
* 命令行(CLI)运行时可不带令牌。
*/
require __DIR__ . '/lib/bootstrap.php';
$isCli = (PHP_SAPI === 'cli');
if (!$isCli) {
header('Content-Type: text/plain; charset=utf-8');
$token = isset($_GET['token']) ? (string)$_GET['token'] : '';
$expected = (string)fb_cfg('api_token');
if ($expected === '' || !hash_equals($expected, $token)) {
http_response_code(403);
echo "forbidden\n";
exit;
}
}
$pdo = fb_db();
/* ---------------- 1. 附件过期清理 ---------------- */
$days = (int)fb_upload_cfg('retention_days', 365);
$kept = (int)$pdo->query('SELECT COUNT(*) FROM attachments')->fetchColumn();
$gone = 0;
if ($days > 0) {
$gone = fb_attach_cleanup($pdo, $days, 500);
}
echo "附件:共 {$kept} 个,已清理超过 {$days} 天的附件 {$gone} 个(剩余 "
. (int)$pdo->query('SELECT COUNT(*) FROM attachments')->fetchColumn() . " 个,占用 "
. fb_size_h(fb_attach_total_bytes($pdo)) . ")。\n";
/* ---------------- 1b. 附件存储凭证自检 ----------------
* 顺手确认一次「能不能从 ECS 实例 RAM 角色拿到上传凭证」:角色名写错、
* 角色没授予实例、元数据服务被网络策略挡住时,这里会直接说明原因,
* 比等用户上传失败再来排查容易得多。
*/
if (fb_oss_enabled()) {
$cs = fb_oss_cred_status();
$line = '附件存储凭证:' . $cs['label'] . '(AK 尾号 ' . ($cs['ak_tail'] !== '' ? '…' . $cs['ak_tail'] : '无') . ')';
if ($cs['has_token']) { $line .= ',已取得临时安全令牌'; }
if ($cs['expire'] > 0) { $line .= ',有效期至 ' . date('Y-m-d H:i', $cs['expire']); }
if (!$cs['ok']) { $line .= ' ⚠️ 取不到凭证:' . ($cs['error'] !== '' ? $cs['error'] : '未知原因'); }
echo $line . "。\n";
} else {
echo "附件存储未启用(请在 config.php 的 oss 段填写 endpoint / bucket / instance_role 并置 enabled = true)。\n";
}
/* ---------------- 2. 通知邮件补发 ---------------- */
$smtp = (array)fb_cfg('smtp');
if (empty($smtp['enabled'])) {
echo "SMTP 未启用,跳过邮件补发。\n";
exit;
}
$limit = 50;
$rows = $pdo->query(
'SELECT * FROM notify_log WHERE ok = 0 AND tries < 5 ORDER BY id ASC LIMIT ' . $limit
)->fetchAll();
$sent = 0; $fail = 0;
foreach ($rows as $n) {
$st = $pdo->prepare('SELECT * FROM threads WHERE id = ?');
$st->execute([(int)$n['thread_id']]);
$thread = $st->fetch();
if (!$thread) { continue; }
$st = $pdo->prepare('SELECT * FROM replies WHERE id = ?');
$st->execute([(int)$n['reply_id']]);
$reply = $st->fetch();
if (!$reply) { continue; }
$isAdmin = !empty($reply['is_admin']);
$subject = '【' . fb_cfg('site_name') . '】有人回复了你的问题:' . fb_excerpt($thread['title'], 40);
$html = fb_notify_html($thread, $reply, $isAdmin);
list($ok, $err) = fb_smtp_send($smtp, (string)$n['to_email'], $subject, $html);
$up = $pdo->prepare('UPDATE notify_log SET ok = ?, tries = tries + 1, err = ? WHERE id = ?');
$up->execute([$ok ? 1 : 0, $ok ? null : $err, (int)$n['id']]);
if ($ok) { $sent++; } else { $fail++; }
}
echo "邮件:成功 {$sent} 封,失败 {$fail} 封(最多处理 {$limit} 条待发记录)。\n";