多软件归属:软件→分类两级,配置全部入库 (#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
+113 -8
View File
@@ -58,16 +58,121 @@ function fb_redirect($url) {
exit;
}
/** 分类 / 状态 */
function fb_categories() { return (array)fb_cfg('categories', []); }
function fb_category_label($k) {
$c = fb_categories();
return isset($c[$k]) ? $c[$k] : '其他';
/** ================= 软件 / 分类 / 状态 =================
* 三者全部存数据库、后台可配,不再读配置文件。
* 结构:软件 → 分类(两级)。分类属于某个软件,不同软件可有同名分类。 */
/** 全部软件(按排序),$onlyEnabled=true 时只取启用的 */
function fb_softwares($onlyEnabled = true) {
$sql = 'SELECT * FROM software';
if ($onlyEnabled) { $sql .= ' WHERE enabled = 1'; }
$sql .= ' ORDER BY sort_order, id';
$rows = fb_db()->query($sql)->fetchAll();
$out = [];
foreach ($rows as $r) { $out[] = $r; }
return $out;
}
function fb_statuses() { return (array)fb_cfg('statuses', []); }
/** 按 slug 或 id 取一个软件;取不到返回 null */
function fb_software($key) {
if ($key === null || $key === '') { return null; }
$pdo = fb_db();
if (ctype_digit((string)$key)) {
$st = $pdo->prepare('SELECT * FROM software WHERE id = ? LIMIT 1');
$st->execute([(int)$key]);
} else {
$st = $pdo->prepare('SELECT * FROM software WHERE slug = ? LIMIT 1');
$st->execute([(string)$key]);
}
$r = $st->fetch();
return $r ? $r : null;
}
/** 默认软件:排序最前的启用软件;一个都没有时返回 null */
function fb_default_software() {
$list = fb_softwares(true);
return $list ? $list[0] : null;
}
/** 某软件下的分类;不传软件则返回空数组 */
function fb_categories($softwareId = null, $onlyEnabled = true) {
if ($softwareId === null) { return []; }
$sql = 'SELECT * FROM categories WHERE software_id = ?';
$args = [(int)$softwareId];
if ($onlyEnabled) { $sql .= ' AND enabled = 1'; }
$sql .= ' ORDER BY sort_order, id';
$st = fb_db()->prepare($sql);
$st->execute($args);
return $st->fetchAll();
}
/** 按 id 取分类(含禁用的,后台编辑用) */
function fb_category($id) {
if (!$id) { return null; }
$st = fb_db()->prepare('SELECT * FROM categories WHERE id = ? LIMIT 1');
$st->execute([(int)$id]);
$r = $st->fetch();
return $r ? $r : null;
}
/** 在某软件内按 slug 取分类 */
function fb_category_by_slug($softwareId, $slug) {
if (!$softwareId || $slug === '') { return null; }
$st = fb_db()->prepare('SELECT * FROM categories WHERE software_id = ? AND slug = ? LIMIT 1');
$st->execute([(int)$softwareId, (string)$slug]);
$r = $st->fetch();
return $r ? $r : null;
}
/** 分类显示名:只给 id 就能拿到名字(列表渲染用) */
function fb_category_label($id) {
if (!$id) { return ''; }
static $cache = [];
$id = (int)$id;
if (!isset($cache[$id])) {
$c = fb_category($id);
$cache[$id] = $c ? (string)$c['name'] : '';
}
return $cache[$id];
}
/** 全部状态(按排序) */
function fb_statuses($onlyEnabled = true) {
$sql = 'SELECT * FROM statuses';
if ($onlyEnabled) { $sql .= ' WHERE enabled = 1'; }
$sql .= ' ORDER BY sort_order, id';
return fb_db()->query($sql)->fetchAll();
}
/** 状态 key 列表(校验用) */
function fb_status_keys() {
$out = [];
foreach (fb_statuses(true) as $s) { $out[(string)$s['key_name']] = (string)$s['name']; }
return $out;
}
/** 状态显示名 */
function fb_status_label($k) {
$s = fb_statuses();
return isset($s[$k]) ? $s[$k] : (string)$k;
static $cache = null;
if ($cache === null) {
$cache = [];
foreach (fb_statuses(false) as $s) { $cache[(string)$s['key_name']] = (string)$s['name']; }
}
$k = (string)$k;
return isset($cache[$k]) ? $cache[$k] : $k;
}
/** 状态的「给用户看的话」——进邮件正文,后台可改 */
function fb_status_user_note($k) {
static $cache = null;
if ($cache === null) {
$cache = [];
foreach (fb_statuses(false) as $s) {
$cache[(string)$s['key_name']] = (string)($s['user_note'] === null ? '' : $s['user_note']);
}
}
$k = (string)$k;
return isset($cache[$k]) ? $cache[$k] : '';
}
/** CSRF */