## 1. ECS RAM 角色改为自动探测 原先要用户去阿里云控制台查「实例 RAM 角色名」再手填,纯属多余 —— 机器自己知道答案。 新增 fb_oss_detect_instance_role():读元数据 GET /latest/meta-data/ram/security-credentials/(末尾带斜杠)拿到角色名列表。 - 兼容单行 / 多行 / JSON 数组三种返回 - 未配置角色时 fb_oss_instance_creds() 自动探测,不再报「未配置」 - 后台改成只读展示「已绑定:xxx」,手填入口收进「高级」折叠里 - 保存时留空 = 自动探测后写入,不存空值 - 探测失败给出可操作原因(没绑角色 / 不在 ECS 上 / 元数据被禁用) ## 2. 邮件配置界面(原先完全缺失) lib/mailer.php 有完整 SMTP 实现,但后台一个输入框都没有, 只能改 config.php —— 用户装完根本找不到地方配。 根因有两层: - fb_setting_keys() 白名单里没有任何 smtp.* / notify.* 键, 就算加了表单,fb_set_setting() 也会把值全部丢弃 - 设置页只有「站点信息」「附件存储」两个区块 补上:白名单 15 个键 + 后台「邮件通知」区块 - SMTP 服务器/端口/加密方式(SSL/STARTTLS/不加密) - 登录账号/密码(留空不修改,避免每次重填) - 发件地址/显示名、超时、SSL 校验 - 通知规则:回复通知、每次回复都通知、新反馈通知管理员、发信冷却 - 「发送测试邮件」按钮,AJAX 发完就地显示成功或失败原因, 不用保存再靠猜 ## 3. 每个软件一个主题色 需求:从短链接进不同软件,颜色不同,一眼区分。 - software 表加 color 字段(幂等迁移) - 全站配色靠 --brand / --brand-600 / --brand-soft 三个变量, 按软件覆写这三个即可,不用为每个软件写一套 CSS - fb_color_variants() 由主色推出深一档(×0.85)与浅一档(往白混 88%) - 未配色的软件按 id 从 8 色调色板取默认色,保证同站不撞色 - 后台:颜色选择器(原生取色 + 8 个预设 + 用默认) - 前台:index / new / thread 都按当前软件注入主题色, meta theme-color 也跟着变 验证:三功能专项 30/30;新旧回归全过 (全新安装 33/33、老基线 25/25、附件 22/22、安装向导 60/60、约束检查 20/20)
This commit is contained in:
@@ -108,7 +108,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
||||
$t = fb_admin_thread($pdo, $id);
|
||||
|
||||
// 这些动作不针对某一条反馈(不依赖 $t),单独放行
|
||||
$noThread = in_array($act, ['settings', 'password'], true)
|
||||
$noThread = in_array($act, ['settings', 'password', 'mail_test'], true)
|
||||
|| fb_admin_is_taxonomy_act($act);
|
||||
|
||||
if (!$t && !$noThread) {
|
||||
@@ -200,6 +200,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
||||
fb_mod_log('delete', $id, '删除问题:' . fb_excerpt((string)$t['title'], 40));
|
||||
$msg = '#' . $id . ' 已删除。';
|
||||
|
||||
} elseif ($act === 'mail_test') {
|
||||
// 发送测试邮件:只验证 SMTP 通不通,不改任何配置
|
||||
$to = trim((string)(isset($_POST['to']) ? $_POST['to'] : ''));
|
||||
if ($to === '') { $to = trim((string)fb_cfg('admin_email', '')); }
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
$msg = '请填写有效的收件邮箱。';
|
||||
$msgType = 'error';
|
||||
} else {
|
||||
$sc = (array)fb_cfg('smtp', []);
|
||||
$site = (string)fb_cfg('site_name', '轻反馈');
|
||||
$body = '<p>这是一封来自 <b>' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8')
|
||||
. '</b> 的测试邮件。</p>'
|
||||
. '<p>如果你收到了它,说明 SMTP 配置正确,回复通知可以正常发出。</p>'
|
||||
. '<p style="color:#888;font-size:12px">发送时间:'
|
||||
. date('Y-m-d H:i:s') . '</p>';
|
||||
list($ok, $err) = fb_smtp_send($sc, $to, '【' . $site . '】邮件测试', $body);
|
||||
if ($ok) {
|
||||
$msg = '测试邮件已发出,请查收 ' . $to . '(也看一下垃圾箱)。';
|
||||
$msgType = 'ok';
|
||||
} else {
|
||||
$msg = '发送失败:' . $err;
|
||||
$msgType = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
} elseif (fb_admin_is_taxonomy_act($act)) {
|
||||
list($msg, $msgType) = fb_admin_taxonomy_action($pdo, $act, $_POST);
|
||||
|
||||
@@ -297,6 +322,12 @@ function fb_admin_save_settings() {
|
||||
$v = trim((string)$post($k, ''));
|
||||
// 对象键前缀留空会让对象键变成 "/2026/...",回落到出厂默认 'fb'
|
||||
if ($k === 'oss.prefix' && $v === '') { fb_unset_setting($k); continue; }
|
||||
// RAM 角色留空 = 让程序自己去元数据服务探测,不写死一个空值
|
||||
if ($k === 'oss.instance_role' && $v === '') {
|
||||
list($rOk, $rVal) = fb_oss_detect_instance_role();
|
||||
if ($rOk) { fb_set_setting($k, (string)$rVal); }
|
||||
continue; // 探测不到就保持原值不动
|
||||
}
|
||||
fb_set_setting($k, $v);
|
||||
}
|
||||
|
||||
@@ -311,6 +342,48 @@ function fb_admin_save_settings() {
|
||||
fb_set_setting('oss.verify', $post('oss.verify') ? 1 : 0);
|
||||
fb_set_setting('oss.image_process', $post('oss.image_process') ? 1 : 0);
|
||||
fb_set_setting('oss.use_internal_endpoint', $post('oss.use_internal_endpoint') ? 1 : 0);
|
||||
/* ---------------- 邮件通知(SMTP) ---------------- */
|
||||
$smtpText = ['smtp.host', 'smtp.user', 'smtp.from', 'smtp.from_name'];
|
||||
foreach ($smtpText as $k) {
|
||||
fb_set_setting($k, trim((string)$post($k, '')));
|
||||
}
|
||||
// 口令留空 = 不修改(避免每次保存都得重填)
|
||||
$sp = (string)(isset($_POST['smtp_pass']) ? $_POST['smtp_pass'] : '');
|
||||
if (trim($sp) !== '') { fb_set_setting('smtp.pass', $sp); }
|
||||
|
||||
$smtpPort = (int)$post('smtp.port', 465);
|
||||
if ($smtpPort <= 0 || $smtpPort > 65535) { $smtpPort = 465; }
|
||||
fb_set_setting('smtp.port', $smtpPort);
|
||||
|
||||
$sec = strtolower(trim((string)$post('smtp.secure', 'ssl')));
|
||||
if (!in_array($sec, ['ssl', 'tls', ''], true)) { $sec = 'ssl'; }
|
||||
fb_set_setting('smtp.secure', $sec);
|
||||
|
||||
$smtpTo = (int)$post('smtp.timeout', 15);
|
||||
fb_set_setting('smtp.timeout', max(5, min(60, $smtpTo)));
|
||||
fb_set_setting('smtp.verify_ssl', (int)(bool)$post('smtp.verify_ssl'));
|
||||
|
||||
// 没填 from 时用 user 兜底(绝大多数 SMTP 要求两者一致)
|
||||
if (trim((string)fb_cfg('smtp.from', '')) === '') {
|
||||
fb_set_setting('smtp.from', trim((string)fb_cfg('smtp.user', '')));
|
||||
}
|
||||
|
||||
// 只有 host 填了才允许开启,否则开着一个空配置只会每次发信都失败
|
||||
$wantOn = (bool)$post('smtp.enabled');
|
||||
$hostNow = trim((string)fb_cfg('smtp.host', ''));
|
||||
fb_set_setting('smtp.enabled', ($wantOn && $hostNow !== '') ? 1 : 0);
|
||||
if ($wantOn && $hostNow === '') {
|
||||
$msg = '邮件通知未开启:请先填写 SMTP 服务器地址。';
|
||||
$msgType = 'error';
|
||||
}
|
||||
|
||||
/* ---------------- 通知开关 ---------------- */
|
||||
fb_set_setting('notify.enabled', (int)(bool)$post('notify.enabled'));
|
||||
fb_set_setting('notify.on_every_reply', (int)(bool)$post('notify.on_every_reply'));
|
||||
fb_set_setting('notify.admin_new_thread', (int)(bool)$post('notify.admin_new_thread'));
|
||||
$cd = (int)$post('notify.cooldown_seconds', 60);
|
||||
fb_set_setting('notify.cooldown_seconds', max(0, min(86400, $cd)));
|
||||
|
||||
fb_set_setting('moderation.enabled', $post('moderation.enabled') ? 1 : 0);
|
||||
|
||||
fb_mod_log('settings', null, '保存站点设置:存储=' . $storage);
|
||||
@@ -654,6 +727,88 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
value="<?php echo e((string)fb_cfg('footer.credit_url', '') !== '' ? (string)fb_cfg('footer.credit_url') : fb_credit_url_default()); ?>">
|
||||
<i class="hint">页脚「FeedLite」二字指向的地址,默认是本项目开源仓库。只接受 http:// 或 https:// 开头的网址;填回默认值即恢复。</i></label>
|
||||
|
||||
<h2 style="margin:24px 0 10px">邮件通知</h2>
|
||||
<?php
|
||||
$smOn = (bool)fb_cfg('smtp.enabled', false);
|
||||
$smHost = (string)fb_cfg('smtp.host', '');
|
||||
$smUser = (string)fb_cfg('smtp.user', '');
|
||||
$smFrom = (string)fb_cfg('smtp.from', '');
|
||||
$smName = (string)fb_cfg('smtp.from_name', '');
|
||||
$smPort = (int)fb_cfg('smtp.port', 465);
|
||||
$smSec = (string)fb_cfg('smtp.secure', 'ssl');
|
||||
$smHasPw = (string)fb_cfg('smtp.pass', '') !== '';
|
||||
$smTmo = (int)fb_cfg('smtp.timeout', 15);
|
||||
$smVerify= (bool)fb_cfg('smtp.verify_ssl', true);
|
||||
?>
|
||||
<label class="check"><input type="checkbox" name="smtp_enabled" value="1"<?php echo $smOn ? ' checked' : ''; ?>> 开启邮件通知
|
||||
<span class="hint" style="display:block;margin:2px 0 0 24px">开启后,有人回复你反馈的问题会发信通知提交人。填好下方参数并用「发送测试邮件」验证通了再勾。</span></label>
|
||||
|
||||
<div class="grid2">
|
||||
<label class="field"><span>SMTP 服务器</span>
|
||||
<input type="text" name="smtp_host" maxlength="120" placeholder="smtp.qiye.aliyun.com"
|
||||
value="<?php echo e($smHost); ?>">
|
||||
<i class="hint">阿里云企业邮 smtp.qiye.aliyun.com;腾讯企业邮 smtp.exmail.qq.com;QQ 邮箱 smtp.qq.com。</i></label>
|
||||
<label class="field"><span>端口与加密</span>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input type="number" name="smtp_port" min="1" max="65535" style="flex:0 0 110px"
|
||||
value="<?php echo (int)$smPort; ?>">
|
||||
<select name="smtp_secure" style="flex:1">
|
||||
<option value="ssl"<?php echo $smSec === 'ssl' ? ' selected' : ''; ?>>SSL(通常 465)</option>
|
||||
<option value="tls"<?php echo $smSec === 'tls' ? ' selected' : ''; ?>>STARTTLS(通常 587)</option>
|
||||
<option value=""<?php echo $smSec === '' ? ' selected' : ''; ?>>不加密(通常 25,不推荐)</option>
|
||||
</select>
|
||||
</div></label>
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
<label class="field"><span>登录账号</span>
|
||||
<input type="text" name="smtp_user" maxlength="120" placeholder="noreply@example.com"
|
||||
value="<?php echo e($smUser); ?>"></label>
|
||||
<label class="field"><span>密码 / 授权码 <i class="hint" style="display:inline"><?php echo $smHasPw ? '已设置,留空不修改' : '未设置'; ?></i></span>
|
||||
<input type="password" name="smtp_pass" autocomplete="new-password"
|
||||
placeholder="<?php echo $smHasPw ? '留空保持不变' : '邮箱密码或授权码'; ?>"></label>
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
<label class="field"><span>发件地址</span>
|
||||
<input type="email" name="smtp_from" maxlength="120" placeholder="留空则同登录账号"
|
||||
value="<?php echo e($smFrom); ?>"></label>
|
||||
<label class="field"><span>发件人显示名称</span>
|
||||
<input type="text" name="smtp_from_name" maxlength="60"
|
||||
placeholder="<?php echo e((string)fb_cfg('site_name', '轻反馈')); ?>"
|
||||
value="<?php echo e($smName); ?>"></label>
|
||||
</div>
|
||||
|
||||
<details style="margin:6px 0 12px">
|
||||
<summary style="cursor:pointer;font-size:13px;color:var(--muted)">高级选项</summary>
|
||||
<div class="grid2" style="margin-top:8px">
|
||||
<label class="field"><span>超时(秒)</span>
|
||||
<input type="number" name="smtp_timeout" min="5" max="60" value="<?php echo (int)$smTmo; ?>"></label>
|
||||
<label class="check" style="align-self:end"><input type="checkbox" name="smtp_verify_ssl" value="1"<?php echo $smVerify ? ' checked' : ''; ?>> 校验服务器 SSL 证书
|
||||
<span class="hint" style="display:block;margin-left:24px">服务器缺 CA 证书时可临时取消,但不建议。</span></label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<?php if ($smHost !== ''): ?>
|
||||
<div class="mail-test">
|
||||
<span class="muted" style="font-size:13.5px">配好后先发一封测试邮件确认能通:</span>
|
||||
<input type="email" id="mailTestTo" placeholder="收件邮箱,默认发给自己"
|
||||
value="<?php echo e((string)fb_cfg('admin_email', '')); ?>">
|
||||
<button type="button" class="btn" id="mailTestBtn" data-csrf="<?php echo e(fb_csrf_token()); ?>">发送测试邮件</button>
|
||||
<span id="mailTestOut" class="mail-test-out"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3 style="margin:20px 0 8px;font-size:15px">通知规则</h3>
|
||||
<label class="check"><input type="checkbox" name="notify_enabled" value="1"<?php echo fb_cfg('notify.enabled', true) ? ' checked' : ''; ?>> 有人回复时通知提交人</label>
|
||||
<label class="check"><input type="checkbox" name="notify_on_every_reply" value="1"<?php echo fb_cfg('notify.on_every_reply', false) ? ' checked' : ''; ?>> 每次回复都通知(默认只在官方回复时通知,避免刷屏)</label>
|
||||
<label class="check"><input type="checkbox" name="notify_admin_new_thread" value="1"<?php echo fb_cfg('notify.admin_new_thread', false) ? ' checked' : ''; ?>> 有新反馈时通知管理员
|
||||
<span class="hint" style="display:block;margin-left:24px">发到「站点信息」里填的官方邮箱。</span></label>
|
||||
<label class="field" style="max-width:280px"><span>同一帖同一收件人的最短发信间隔(秒)</span>
|
||||
<input type="number" name="notify_cooldown_seconds" min="0" max="86400"
|
||||
value="<?php echo (int)fb_cfg('notify.cooldown_seconds', 60); ?>">
|
||||
<i class="hint">0 = 不限制。</i></label>
|
||||
|
||||
<h2 style="margin:24px 0 10px">附件存储</h2>
|
||||
<label class="field">
|
||||
<span>附件保存在哪</span>
|
||||
@@ -696,8 +851,38 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
<option value="instance_role"<?php echo fb_oss_credential_source() === 'instance_role' ? ' selected' : ''; ?>>ECS 实例 RAM 角色(推荐)</option>
|
||||
<option value="static"<?php echo fb_oss_credential_source() === 'static' ? ' selected' : ''; ?>>静态 AccessKey</option>
|
||||
</select></label>
|
||||
<label class="field"><span>ECS 实例 RAM 角色名</span>
|
||||
<input type="text" name="oss_instance_role" placeholder="FeedLiteOssRole" value="<?php echo e((string)fb_cfg('oss.instance_role', '')); ?>"></label>
|
||||
<?php
|
||||
// RAM 角色名不该让用户去控制台查 —— 直接问 ECS 元数据服务。
|
||||
// 已配置的显示配置值;没配置就现场探测一次作为占位提示。
|
||||
$curRole = trim((string)fb_cfg('oss.instance_role', ''));
|
||||
$detect = null;
|
||||
if ($curRole === '') {
|
||||
list($dOk, $dVal) = fb_oss_detect_instance_role();
|
||||
$detect = [$dOk, $dVal];
|
||||
}
|
||||
?>
|
||||
<div class="field">
|
||||
<span>ECS 实例 RAM 角色</span>
|
||||
<?php if ($curRole !== ''): ?>
|
||||
<div class="role-ok">已绑定:<b><?php echo e($curRole); ?></b>
|
||||
<i class="hint" style="display:inline;margin-left:6px">(自动从实例元数据读取,无需手填)</i>
|
||||
</div>
|
||||
<?php elseif ($detect && $detect[0]): ?>
|
||||
<div class="role-ok">自动探测到:<b><?php echo e((string)$detect[1]); ?></b>
|
||||
<i class="hint" style="display:inline;margin-left:6px">保存时若留空就自动用这个</i>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="role-warn">
|
||||
<?php echo e($detect ? (string)$detect[1] : '未能自动探测'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<details style="margin-top:6px">
|
||||
<summary style="cursor:pointer;font-size:13px;color:var(--muted)">手动指定角色名(一般不需要)</summary>
|
||||
<input type="text" name="oss_instance_role" style="margin-top:6px"
|
||||
placeholder="留空即自动探测"
|
||||
value="<?php echo e($curRole); ?>">
|
||||
</details>
|
||||
</div>
|
||||
<label class="field"><span>AccessKeyId(静态鉴权用)</span>
|
||||
<input type="text" name="oss_access_key" value="<?php echo e((string)fb_cfg('oss.access_key', '')); ?>"></label>
|
||||
<label class="field"><span>AccessKeySecret(留空=不修改)</span>
|
||||
|
||||
Reference in New Issue
Block a user