Files
FeedLite/cron.php
T
laoyang 1401ad899b OSS 附件改为私有桶+内网Endpoint+服务器代理架构
背景:桶为私有、且只用内网 Endpoint(外网不可达),浏览器既无法直传、
也无法直连。原「浏览器直传 + 签名URL直连」两条路都失效,改为全站代理。

lib/oss.php:
- endpoint 自动转内网(*-internal),use_internal_endpoint 开关控制,内网走 http
- 移除浏览器直传凭证(PostObject),改为服务器端 PUT(流式 fb_oss_put_file)
- 新增 fb_oss_stream_get 流式读取(回调式,支持 Range,不整文件进内存)
- 签名支持 x-oss-process 子资源 + Authorization 头签名(Content-Type 入签)
- 新增 fb_oss_probe 连通性自检、fb_oss_error_text 错误归因(含跨地域提示)

media.php(新增): 附件代理输出,鉴权边界=对象键须在本站附件表(或本人待入库
  登记)且帖子对访客可见,防止本站沦为能读整个共用桶的开放代理;透传 Range。

upload.php: local/oss 统一走两步(领键→action=put),OSS 模式由服务器代理 PUT。
lib/db.php: 新增 pending_attachments 表 + idx_att_key 索引(迁移幂等自动建)。
lib/attach.php: 待入库登记(mark/clear/mine/gc),解决上传后入库前的预览鉴权。
cron.php: 清理过期待入库登记 + 桶连通性自检输出。
admin.php: 设置页加「使用内网 Endpoint」开关、存储体检显示实际连接地址与探测按钮。
README/config: 全面更新为代理架构(无需CORS、无需自定义域名、同地域要求)。

验证:本地 e2e 25/25 + 附件专项 13/13(含防开放代理/Range/内网改写);
线上真机上传→内网PUT→media.php代理读回→Range 全通过。
2026-09-28 22:12:28 +08:00

111 lines
4.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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";
/* ---------------- 1a. 清理「待入库」孤儿登记 ----------------
* 上传成功但一直没提交表单的附件,会在 pending_attachments 里留下登记。
* 超过 2 小时仍没转正的,清掉登记(media.php 之后就不再认这些键)。
* ⚠️ 只删登记表记录;OSS 上对应的孤儿对象由保留期清理策略另行处理。
*/
$pgone = fb_attach_pending_gc(7200);
if ($pgone > 0) { echo "待入库登记:清理 {$pgone} 条过期未提交记录。\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";
// 连通性自检:能否真正访问所配置的桶(内网 endpoint 跨地域时会在这里暴露)
$probe = fb_oss_probe();
echo '桶连通性:' . ($probe['ok'] ? '✅ ' : '❌ ') . $probe['host']
. '(' . ($probe['internal'] ? '内网' : '公网') . ' Endpoint,HTTP ' . $probe['status'] . ')— '
. $probe['detail'] . "。\n";
if (!$probe['ok'] && $probe['internal'] && $probe['status'] === 0) {
echo " ↳ 提示:内网 Endpoint 只有与桶【同地域】的阿里云 ECS 能访问。\n"
. " 若本机与桶不同地域,请到后台「站点设置」关闭『使用内网 Endpoint』,或改用同地域的桶。\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";