Files
FeedLite/config.example.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

177 lines
10 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
/**
* FeedLite · 轻反馈 · 配置文件
* ------------------------------------------------------------------
* 部署后请务必修改 admin_password 与 api_token(及 ip_salt),
* 并按需填写下方的 smtp 段以开启"回复邮件通知"。
*/
return [
/* ---------------- 基本 ---------------- */
// SQLite 数据库文件路径。
// 放在站内 data/ 目录(已被 .htaccess 拒绝直接访问);也可改为站点目录之外的绝对路径。
'db_path' => __DIR__ . '/data/feedback.db',
'site_name' => '轻反馈',
'site_desc' => '欢迎在这里反馈使用问题、提出功能建议。任何人都可以提问与回复,无需注册;只需填写昵称与邮箱。',
// 展示用的官方邮箱(可留空)
'admin_email' => 'support@example.com',
// 站点根地址(用于邮件正文里的链接)。留空则按访问域名自动探测。
// 例如:'https://feedback.example.com'
'base_url' => '',
/* ---------------- 安全 ---------------- */
// 管理员口令:在"回复"表单里填入它,即以「官方」身份回复(高亮显示),并触发邮件通知。
// ★★ 请务必修改! ★★
'admin_password' => 'CHANGE_ME_admin',
// API 令牌:供自动化脚本 / AI 读取数据用。★ 请务必修改 ★
'api_token' => 'CHANGE_ME_api_token',
// 对访客 IP 做加盐哈希(数据库只保存哈希,不保存明文 IP)。★ 请修改为随机值 ★
'ip_salt' => 'CHANGE_ME_random_salt',
/* ---------------- 问题分类 ---------------- */
'categories' => [
'usage' => '使用问题',
'bug' => 'Bug 反馈',
'feature' => '功能建议',
'sync' => '同步问题',
'other' => '其他',
],
/* ---------------- 处理状态 ---------------- */
'statuses' => [
'open' => '待处理',
'doing' => '处理中',
'resolved' => '已解决',
'closed' => '已关闭',
],
/* ---------------- 邮件通知(SMTP) ---------------- */
'smtp' => [
'enabled' => false, // 填好下面参数后改为 true 才会真正发信
'host' => 'smtp.example.com', // 如 smtp.qiye.aliyun.com / smtp.exmail.qq.com
'port' => 465,
'secure' => 'ssl', // ssl(465)| tls(587 STARTTLS)| ''(25 不加密)
'user' => 'noreply@example.com',
'pass' => '', // 邮箱密码或"授权码"
'from' => 'noreply@example.com', // 发件地址(通常与 user 相同)
'from_name' => '轻反馈',
'timeout' => 15,
'verify_ssl' => true, // 若服务器缺少 CA 证书可临时设为 false
],
// 通知策略
'notify' => [
'enabled' => true, // 回复时是否尝试发送通知邮件
'on_every_reply' => true, // true=任何人回复都通知发帖人;false=仅"官方"回复才通知
'cooldown_seconds' => 300, // 同一帖同一收件人的通知节流(秒)
],
/* ---------------- 审核(先审后显) ----------------
* enabled = true:新提交的问题默认**不公开**,由管理员在 admin.php 审核;
* · 通过 → 公开显示(visible=1)
* · 关闭 → 判定"不是应用的问题",保持不公开,并把管理员填写的说明邮件发给提问人
* 审核后台入口:/feedback/admin.php(口令= admin_password)
*/
'moderation' => [
'enabled' => true,
'close_mail_subject' => '关于你反馈的问题《%s》', // %s 会替换成问题标题
],
/* ---------------- 附件上传(阿里云 OSS · 服务器代理) ----------------
* ★ 架构:私有桶 + 内网 Endpoint,读写全部经服务器代理
* 桶设为【私有】、且只用【内网 Endpoint】(*-internal.aliyuncs.com,仅同地域 ECS 可达),
* 浏览器既不直传、也不直连。数据流:
* 上传:浏览器 → upload.php → 服务器 PUT 到内网 OSS
* 读取:浏览器 → media.php → 服务器 GET 内网 OSS 并流式转发(支持 Range)
* 好处:桶可彻底私有、公网完全看不到 Endpoint、AccessKey 永不出服务器。
* 代价:附件流量经过服务器(占带宽),需保证服务器磁盘临时空间够放单个上传文件。
*
* ★ 凭证方式:ECS 实例 RAM 角色(推荐,服务器上不保存长期 AccessKey)或静态 AccessKey。
* 实例角色下,程序从内网元数据服务(http://100.100.100.200,链路本地、不出公网)
* 读取一份「有时效、会自动轮换」的临时凭证来签名。AccessKey 永不落盘、永不进本文件。
*
* ★ 开通步骤(详见 README.md):
* ① 创建【与 ECS 同地域】的 Bucket,读写权限设为「私有」
* ⚠️ 内网 Endpoint 跨地域不通!ECS 在哪个地域,桶就建在哪个地域。
* ② RAM 建权限策略:只授予该 Bucket(某前缀下)的 oss:PutObject / GetObject / DeleteObject
* ③ RAM 建角色(如 FeedLiteOssRole)并赋予上一步的策略
* ④ ECS 控制台把该角色【授予本实例】
* ⑤ 把 endpoint / bucket / instance_role 填到下面,use_internal_endpoint 保持 true,enabled = true
* ⑥ 后台「站点设置 → 存储体检」确认桶连通性为 ✅
* (代理架构下【不需要】配置 CORS,也【不需要】绑定自定义域名)
*
* ★ 前提:站点必须运行在【阿里云 ECS】上并且已绑定 RAM 角色;
* 未绑定则取不到凭证,上传接口会明确报错(其它功能不受影响)。
*/
'oss' => [
'enabled' => false, // 填好参数后改为 true 才启用上传
'endpoint' => 'oss-cn-qingdao.aliyuncs.com', // 不含 http://;填公网或内网均可,程序按下方开关自动转换
'bucket' => 'your-bucket-name',
// ★ ECS 实例 RAM 角色名 —— 必须与【ECS 实例实际绑定的角色名】逐字一致。
// 查法:curl http://100.100.100.200/latest/meta-data/ram/security-credentials/
// 注意:RAM 里"创建的角色名"和"授予实例后实例上生效的角色名"可能不同(如
// 建的是 MyOssRole,授予实例后生效名可能是系统预置策略对应的角色),以查询结果为准。
'instance_role' => '',
// ★ 是否用内网 Endpoint(默认开)。开启后程序自动把 endpoint 改写成 *-internal 形式,
// 走阿里云内网访问 OSS:免公网流量费、延迟更低,且公网完全无法直连该地址。
// ⚠️ 仅当【桶与本机 ECS 同地域】时可用;跨地域时内网不可达,必须关掉本开关
// (回落公网 Endpoint)或改用同地域的桶。后台「存储体检」会探测并提示。
'use_internal_endpoint' => true,
// ---- 以下一般不用改 ----
'metadata_url' => '', // 留空=http://100.100.100.200(ECS 内网元数据服务)
'metadata_v2' => false, // true=加固模式(先取一次性 token 再读元数据,防 SSRF 探测)
'metadata_ttl' => 600, // 加固模式 token 有效期(秒,最长 21600)
'credential_cache' => '', // 凭证缓存文件;留空=与数据库同目录的 oss.sts.php
'credential_refresh_margin' => 300, // 到期前多少秒就提前换新(秒)
'domain' => '', // 代理架构下【不再使用】:附件一律经本站 media.php 输出,无需自定义域名
'url_scheme' => 'https', // 公网 Endpoint 时使用的协议(内网 Endpoint 强制走 http)
'prefix' => 'fb', // 对象键前缀:fb/2026/09/xxxxxxxx.jpg
'visibility' => 'private', // 记录桶的读写权限:代理架构下推荐 private(浏览器永不直连桶)
'sign_ttl' => 600, // 服务器内部签名 URL 的有效期(秒),仅服务器侧使用
'link_ttl' => 604800, // 兼容保留;代理读取时签名有效期由代码收敛到 ≤300 秒
'image_process' => true, // 列表缩略图使用 OSS 图片处理(服务端缩放后再转发,省下行流量)
'verify' => true, // 提交时 HEAD 复核对象(防伪造/防超限)
'host_override' => '', // 仅特殊网关/联调使用;留空自动按 bucket.endpoint 拼接
],
/* ---------------- 附件限额 ----------------
* 单位:字节。原图上限指「压缩前」的原始文件大小(前端会先压缩到最长边 1600px)。
*/
'upload' => [
'image_max_per_post' => 4,
'image_max_bytes' => 5242880, // 5 MB / 张
'image_exts' => ['jpg', 'jpeg', 'png', 'webp', 'gif'],
'image_mimes' => ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
'image_max_side' => 1600, // 前端压缩后的最长边(像素)
'video_max_per_post' => 1,
'video_max_bytes' => 52428800, // 50 MB / 个
'video_exts' => ['mp4', 'mov', 'webm'],
'video_mimes' => ['video/mp4', 'video/quicktime', 'video/webm'],
'total_cap_bytes' => 1073741824, // 全站附件总量上限 1 GB,达到后暂停接收新上传
'retention_days' => 365, // 附件保留天数,超期由 cron.php 清理
'upload_per_hour' => 60, // 每 IP 每小时最多申请多少次上传凭证
],
/* ---------------- 反垃圾 ---------------- */
'rate_limit' => [
'thread_per_hour' => 5, // 每个 IP 每小时最多发起的主题数
'reply_per_hour' => 20, // 每个 IP 每小时最多回复数
],
'post_min_seconds' => 3, // 打开表单到提交不得少于该秒数(过快视为机器人)
/* ---------------- 列表 ---------------- */
'page_size' => 20,
];