多软件归属:软件→分类两级,配置全部入库 (#1)

原先分类写死在 config.example.php,后台改不了;也没有「软件」概念。
现在改为数据库驱动的两级结构,后台可配(管理界面待做)。

## 数据层(lib/db.php)
- 新增 software / categories / statuses 三张表
- categories.software_id 外键:每个软件有自己的分类集合,可同名互不干扰
- threads 加 software_id / category_id;老 category 文本列保留不删(可回退)
- fb_db_migrate_taxonomy() 幂等迁移:建表 → 空表灌初始数据 → 回填老帖子

## 迁移实测
- 老 5 个分类(使用问题/Bug 反馈/功能建议/同步问题/其他)原样落进默认软件
- 老帖子按 category slug 回填 category_id,匹配不到的留 NULL

## 状态(5 个)
待处理 / 处理中 / 已解决 / 已关闭 / 不予考虑(wontfix)
每个状态带 user_note「给用户看的话」,后台可改,将进邮件正文。
待处理 = 刚提交:您提交的反馈已收到,我们正在处理中。

## 前台
- index.php:软件切换条 + 分类 chip(一级软件、二级分类)
- new.php:选软件、分类下拉随软件变、服务端校验分类归属
- URL:?sw=<slug>&cat=<slug>;老链接 ?cat=xxx 解析不到时退回全部,200 不 404

## 界面一致性
按用户要求去掉「单软件时隐藏软件选择器」的逻辑:
所有界面一致,配一个就列一个,配多个就列多个。

## 兼容
fb_categories() 现在必须传软件 id;fb_statuses() 返回行数组,
另有 fb_status_keys() 取 key=>name 映射。

验证:WSL 内临时实例实测——单软件显示 1 个软件条;两软件各显示
自己的分类集合与帖子,互不串;无 PHP 警告/致命错误。
This commit is contained in:
2026-11-01 11:35:55 +08:00
parent 690228e7c1
commit 59f7466ae4
6 changed files with 378 additions and 39 deletions
+59 -14
View File
@@ -4,13 +4,27 @@
*/
require __DIR__ . '/lib/bootstrap.php';
$pdo = fb_db();
$cats = fb_categories();
$err = '';
$atts = []; // 校验失败时保留已上传的附件
$catKeys = array_keys($cats);
$form = [
'category' => $catKeys ? (string)$catKeys[0] : 'other',
$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' => '',
@@ -19,16 +33,27 @@ $form = [
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach (['category', 'title', 'body', 'name', 'email', 'phone'] as $k) {
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'] : '');
if (!isset($cats[$form['category']])) { $form['category'] = 'other'; }
// 分类必须属于这个软件,否则视为未选
$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 = '会话已过期,请重新提交。';
@@ -66,9 +91,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$now = time();
// 审核:开启后新提交默认不公开(visible=0),管理员通过后才会出现在列表
$visible = fb_moderation_on() ? 0 : 1;
$ins = $pdo->prepare('INSERT INTO threads(title, body, category, author_name, author_email, author_phone, status, visible, reply_count, created_at, updated_at, ip_hash) VALUES(?,?,?,?,?,?,?,?,0,?,?,?)');
$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'], $form['category'],
$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,
'open', $visible, $now, $now, fb_ip_hash(),
@@ -99,14 +127,31 @@ fb_layout_head('我要反馈', ['robots' => 'noindex,follow']);
<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']; ?>">
<div class="field">
<span>所属软件 <b class="req">*</b><i class="hint">换软件会刷新下面的分类</i></span>
<div class="chips">
<?php foreach ($swList as $one): ?>
<a class="chip <?php echo ((int)$one['id'] === $swId) ? 'on' : ''; ?>"
href="new.php?sw=<?php echo e(urlencode((string)$one['slug'])); ?>"><?php echo e((string)$one['name']); ?></a>
<?php endforeach; ?>
</div>
</div>
<?php elseif ($sw): ?>
<input type="hidden" name="software_id" value="<?php echo (int)$sw['id']; ?>">
<?php endif; ?>
<?php if ($cats): ?>
<label class="field">
<span>问题分类 <b class="req">*</b></span>
<select name="category">
<?php foreach ($cats as $k => $v): ?>
<option value="<?php echo e($k); ?>" <?php echo $form['category'] === $k ? 'selected' : ''; ?>><?php echo e($v); ?></option>
<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>