背景:桶为私有、且只用内网 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 全通过。
111 lines
4.8 KiB
PHP
111 lines
4.8 KiB
PHP
<?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";
|