Files
FeedLite/new.php
T
laoyang 314a1cf2a5 新增「未公开」状态;待审核改列表式,只留通过并公开
用户要求:
1. 待审核/全部问题都用列表形式
2. 待审核去掉「关闭说明」整块,后面只加一个「通过并公开」按钮
3. 标题本身就是进管理页的链接,去掉「进入管理」
4. 「未公开」应当是一种状态:提交后初始状态不是「待处理」而是「未公开」

状态流转(用户定义):
  提交 → 未公开
    ├─ 通过并公开 → 待处理 → 处理中 → 已解决 → 已关闭
    └─ 直接关闭   → 已关闭(不经过待处理)
  每次状态变更、每次有人回复,都给提问人发信

实现:
- statuses 新增 hidden「未公开」,sort=0 排最前,其余顺延
  幂等迁移:老库检测到没有 hidden 就整体后移一位再插入,
  并把 visible=0 且 status=open 的存量数据归到 hidden
- new.php 初始状态按可见性决定:未公开→hidden,公开→open
  (以前一律写 open,后台「待审核」里显示的全是「待处理」,状态反映不出事实)
- approve 动作:visible=1 且 hidden→open,并用更新后的行渲染邮件
- 详情页「通过并公开」按钮原来传原状态(未公开时通过后仍是未公开),改为固定传 open
- 待审核查询改为 status=hidden(兼容老数据 visible=0 且 open),
  不再用 visible=0 AND status<>closed —— 那个条件会把「已关闭但未公开」也收进来
- 新增 fb_admin_pending_row() 列表行渲染:标题即链接、右侧单个按钮
- 删除 fb_admin_thread_card() 及「关闭说明」表单
- 新增 .plist/.prow 列表样式与 .st-hidden 状态徽标(中性灰)
- mailer 补 hidden 状态的文案,避免落到兜底

验证(线上真实数据):6 个状态齐全、提交落 hidden、
待审核能查到且不含已公开、通过后转 open 并移出待审核、
后续 doing/resolved/closed 推进正常、6 个状态邮件均能渲染、
待审核行结构 10 项断言全过(无关闭说明/无 note/无进入管理/标题可点)。
测试数据已清理,用户原有 4 条数据完好。
2026-11-03 09:09:40 +08:00

234 lines
11 KiB
PHP

<?php
/**
* 发布新问题
*/
require __DIR__ . '/lib/bootstrap.php';
$pdo = fb_db();
$err = '';
$atts = []; // 校验失败时保留已上传的附件
/* ---- 软件:URL 带 sw 就用它,否则落到默认软件 ---- */
$swList = fb_softwares(true);
$sw = null;
$swKey = isset($_GET['sw']) ? (string)$_GET['sw'] : '';
if ($swKey !== '') {
$sw = fb_software($swKey);
if (!$sw || (int)$sw['enabled'] !== 1) { $sw = null; }
}
if ($sw === null && $swList) { $sw = $swList[0]; }
$swId = $sw ? (int)$sw['id'] : 0;
$cats = fb_categories($swId);
$catKeys = [];
foreach ($cats as $c) { $catKeys[] = (int)$c['id']; }
$form = [
'category_id' => $catKeys ? (string)$catKeys[0] : '',
'title' => '',
'body' => '',
'name' => '',
'email' => '',
'phone' => '',
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach (['category_id', 'title', 'body', 'name', 'email', 'phone'] as $k) {
$form[$k] = trim(isset($_POST[$k]) ? (string)$_POST[$k] : '');
}
// POST 里带了 software_id 时以它为准(切换软件后表单会跟着变)
if (isset($_POST['software_id']) && ctype_digit((string)$_POST['software_id'])) {
$swPost = fb_software((int)$_POST['software_id']);
if ($swPost && (int)$swPost['enabled'] === 1) {
$sw = $swPost;
$swId = (int)$swPost['id'];
}
}
$capInput = isset($_POST['captcha']) ? (string)$_POST['captcha'] : '';
$hp = trim(isset($_POST['website']) ? (string)$_POST['website'] : '');
$t0 = isset($_POST['t0']) ? (int)$_POST['t0'] : 0;
list($attOk, $attList, $attErr) = fb_attach_parse(isset($_POST['attachments']) ? $_POST['attachments'] : '');
// 分类必须属于这个软件,否则视为未选
$curCatId = ctype_digit($form['category_id']) ? (int)$form['category_id'] : 0;
$curCat = $curCatId ? fb_category($curCatId) : null;
if (!$curCat || (int)$curCat['software_id'] !== $swId) { $curCat = null; $form['category_id'] = ''; }
if (!fb_csrf_ok(isset($_POST['csrf']) ? $_POST['csrf'] : '')) {
$err = '会话已过期,请重新提交。';
} elseif ($hp !== '') {
$err = '提交未通过校验,请重试。';
} elseif ($t0 > 0 && (time() - $t0) < max(0, (int)fb_cfg('post_min_seconds', 3))) {
$err = '提交过于频繁,请稍候再试。';
} elseif (fb_len($form['title']) < 4 || fb_len($form['title']) > 80) {
$err = '标题请填写 4~80 个字符。';
} elseif (!fb_valid_body($form['body'])) {
$err = '问题描述请填写 2~5000 个字符。';
} elseif (!fb_valid_name($form['name'])) {
$err = '请填写昵称(1~24 个字符)。';
} elseif (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) {
$err = '请填写有效的邮箱地址(用于接收回复通知)。';
} elseif ($form['phone'] !== '' && !preg_match('/^[0-9+\-\s()]{5,30}$/', $form['phone'])) {
$err = '手机号格式不正确(可留空)。';
} elseif (!$attOk) {
$err = $attErr;
} elseif (!fb_rate_check('thread', (int)fb_cfg('rate_limit.thread_per_hour', 5))) {
$err = '发帖过于频繁,请稍后再试。';
} else {
// 附件回源复核 + 全站总量检查(放在人机验证之前,避免验证码被无效消耗)
list($vOk, $vList, $vErr) = fb_attach_verify($attList);
if (!$vOk) {
$err = $vErr;
$atts = $attList;
} elseif (!fb_attach_cap_ok($pdo, $vList)) {
$err = '附件存储空间已满,请减少附件后重试。';
$atts = $vList;
} elseif (!fb_captcha_check($capInput)) {
$err = '人机验证未通过,请重新作答。';
$atts = $vList;
} else {
$now = time();
/* 审核:开启后新提交默认不公开(visible=0),管理员通过后才出现在列表。
*
* 🔴 状态与可见性必须同时设,两者是同一件事的两个侧面:
* 未公开 → status='hidden'(未公开),公开 → status='open'(待处理)。
* 以前一律写 'open',导致后台「待审核」里全是「待处理」,
* 状态根本反映不出"还没公开"这个事实。 */
$visible = fb_moderation_on() ? 0 : 1;
$initStatus = $visible === 1 ? 'open' : 'hidden';
$ins = $pdo->prepare('INSERT INTO threads(title, body, category, category_id, software_id, author_name, author_email, author_phone, status, visible, reply_count, created_at, updated_at, ip_hash) VALUES(?,?,?,?,?,?,?,?,?,?,0,?,?,?)');
$ins->execute([
$form['title'], $form['body'],
$curCat ? (string)$curCat['slug'] : 'other', // 老列留个值,便于回退与检索
$curCat ? (int)$curCat['id'] : null,
$swId > 0 ? $swId : null,
$form['name'], $form['email'],
$form['phone'] !== '' ? $form['phone'] : null,
$initStatus, $visible, $now, $now, fb_ip_hash(),
]);
$newId = (int)$pdo->lastInsertId();
fb_attach_save($pdo, $newId, null, $vList);
fb_rate_hit('thread');
// 通知管理员「有新反馈」(工单 #2)
// 此前 notify.admin_new_thread 这个设置项是**假的** ——
// 白名单里有、后台有勾选框,但没有任何代码实现它。
// 用户反馈「提交了反馈却收不到邮件」正是这个原因。
try {
$stNt = $pdo->prepare('SELECT * FROM threads WHERE id = ?');
$stNt->execute([$newId]);
$newThread = $stNt->fetch(PDO::FETCH_ASSOC);
if ($newThread) {
$swName = '';
if (!empty($swId)) {
$swRow = fb_software((int)$swId);
if ($swRow) { $swName = (string)$swRow['name']; }
}
/* ① 先给提交人发「已收到」回执 —— 让他知道东西送达了、正在等审核。
* 这一步是用户最在意的:提交完页面跳走了,没有回执就等于石沉大海。
* 放在管理员通知之前,是因为回执对人的价值更高,且失败互不影响。 */
fb_notify_event($pdo, 'receipt', $newThread, ['software_name' => $swName]);
/* ② 再通知管理员「有新反馈」 */
fb_notify_event($pdo, 'new', $newThread, ['software_name' => $swName]);
}
} catch (Throwable $e) {
// 发信失败绝不能让用户提交失败 —— 反馈已经存库了
}
fb_redirect('thread.php?id=' . $newId . '&submitted=1');
}
}
}
fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]);
?>
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
<section class="narrow">
<h1>我要反馈</h1>
<?php if ($sw): ?>
<div class="sw-context"><?php echo fb_software_badge($sw, 'md'); ?>
<span class="muted small">您正在为「<?php echo e(fb_software_site_title($sw)); ?>」提交反馈</span></div>
<?php endif; ?>
<p class="muted">请描述您遇到的问题或想要的功能。提交后会先进入审核,确认为有效问题后才会公开显示;<b>若经确认不是应用的问题,我们会把结论与说明通过邮件发送给您</b>(请填写常用邮箱)。</p>
<?php fb_flash_box($err, 'error'); ?>
<form method="post" action="new.php" class="card-form">
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
<input type="hidden" name="t0" value="<?php echo time(); ?>">
<div class="hp-field" aria-hidden="true">
<label>请勿填写此栏<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
</div>
<?php if ($sw): ?>
<input type="hidden" name="software_id" value="<?php echo (int)$sw['id']; ?>">
<?php /* 软件切换统一收到顶栏的汉堡菜单里,表单内不再平铺一排 chip ——
免得同一个选择在两个地方出现,用户不知道该点哪个。 */ ?>
<?php endif; ?>
<?php if ($cats): ?>
<label class="field">
<span>问题分类 <b class="req">*</b></span>
<select name="category_id" required>
<?php foreach ($cats as $c): ?>
<option value="<?php echo (int)$c['id']; ?>" <?php echo $form['category_id'] === (string)$c['id'] ? 'selected' : ''; ?>><?php echo e((string)$c['name']); ?></option>
<?php endforeach; ?>
</select>
</label>
<?php endif; ?>
<label class="field">
<span>标题 <b class="req">*</b></span>
<input type="text" name="title" maxlength="80" value="<?php echo e($form['title']); ?>" placeholder="一句话概括您的问题" required>
</label>
<label class="field">
<span>详细描述 <b class="req">*</b></span>
<textarea name="body" rows="9" maxlength="5000" placeholder="请尽量说明:您做了什么、期望的结果、实际的结果;如是同步问题,可说明使用的邮箱 / 服务器类型。" required><?php echo e($form['body']); ?></textarea>
</label>
<div class="row2">
<label class="field">
<span>昵称 <b class="req">*</b></span>
<input type="text" name="name" maxlength="24" value="<?php echo e($form['name']); ?>" placeholder="怎么称呼您" required>
</label>
<label class="field">
<span>邮箱 <b class="req">*</b><i class="hint">仅用于接收回复通知,不会公开</i></span>
<input type="email" name="email" maxlength="120" value="<?php echo e($form['email']); ?>" placeholder="you@example.com" required>
</label>
</div>
<label class="field">
<span>手机号 <i class="hint">选填;仅用于必要时联系,不会公开</i></span>
<input type="tel" name="phone" maxlength="30" value="<?php echo e($form['phone']); ?>" placeholder="选填">
</label>
<?php fb_uploaders($atts); ?>
<div class="captcha">
<label class="field">
<span>人机验证 <b class="req">*</b> <i class="hint">防机器人,随手答一下</i></span>
<div class="captcha-line">
<span class="c-q"><?php echo e(fb_captcha_question()); ?></span>
<input type="text" name="captcha" value="" placeholder="答案" autocomplete="off" required>
<a class="reload" href="captcha.php" data-captcha-refresh>换一题</a>
</div>
</label>
</div>
<div class="form-actions">
<button class="btn primary" type="submit">提交反馈</button>
</div>
</form>
<p class="muted small">提示:本页面不会公开展示您的邮箱与手机号。请勿在内容中填写密码等敏感信息。可上传截图或录屏帮助说明问题;文件会直接传到对象存储,不占用本站服务器空间。</p>
</section>
<?php
fb_layout_foot();