refactor(db): 删除旧分区展示块与兼容字段
- v23 迁移删除 section_block 表,以及 node 的 view_mode / empty_text / show_in_sidebar / allow_groups
- 重建 node 表并保留 44 条内容、15 个分组、5 张卡片,外键检查通过
- 种子库直接生成 card / card_group,不再依赖 section_block
- 删除旧 /api/catalog、/api/categories、/api/groups/{id} 与 categories 模块
- 分组接口不再接收/返回 format,前端 GroupNode 同步移除
- README/HANDOFF 更新到 v23;管理页提示改为『格式由每条内容决定』
This commit is contained in:
+67
-64
@@ -27,9 +27,8 @@ const SEED_JSON: &str = include_str!("seed.json");
|
||||
/// 种子会自动重新灌一份干净数据。
|
||||
const SCHEMA_SQL: &str = r#"
|
||||
-- 内容节点:分区与分组共用一张自引用树
|
||||
-- kind = 'section' → 顶层分区(首页一张卡片 / 明细页 hero),parent_code 必须为 NULL
|
||||
-- kind = 'group' → 分区下的分组,parent_code 指向所属分区
|
||||
-- view_mode 只对分区有意义,决定首页与明细页怎么渲染
|
||||
-- kind = 'section' → 顶层分区(首页一张卡片 / 明细页 hero),parent_key 必须为 NULL
|
||||
-- kind = 'group' → 分区下的分组,parent_key 指向分区或上层分组
|
||||
CREATE TABLE IF NOT EXISTS node (
|
||||
id INTEGER PRIMARY KEY, -- 主键(SQLite rowid,自动分配,不用 AUTOINCREMENT)
|
||||
key TEXT NOT NULL UNIQUE, -- 业务键,全局唯一(如 recommend / ui)
|
||||
@@ -38,22 +37,15 @@ CREATE TABLE IF NOT EXISTS node (
|
||||
title TEXT NOT NULL, -- 展示名:分区标题 / 分组小标题
|
||||
kind TEXT NOT NULL CHECK (kind IN ('section', 'group')),
|
||||
-- 容器层级
|
||||
view_mode TEXT CHECK (view_mode IN ('apps', 'posters', 'articles')),
|
||||
-- 分区渲染形态;分组为 NULL
|
||||
bar_color TEXT, -- 分区标题左侧色条颜色(分组为 NULL)
|
||||
icon TEXT, -- 分区 hero 字符图标(分组为 NULL)
|
||||
bar_color TEXT, -- 标题左侧色条颜色
|
||||
icon TEXT, -- 字符图标
|
||||
preview_limit INTEGER, -- 首页预览条数;NULL 时前端按类型取默认
|
||||
empty_text TEXT, -- 分区为空时的提示文案
|
||||
show_in_sidebar INTEGER NOT NULL DEFAULT 1 CHECK (show_in_sidebar IN (0, 1)),
|
||||
-- 是否出现在侧栏:0 表示只当首页卡片/内容,不进导航
|
||||
allow_groups INTEGER NOT NULL DEFAULT 0 CHECK (allow_groups IN (0, 1)),
|
||||
-- 是否允许子分组(与类型无关,后台开关)
|
||||
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
||||
-- 是否启用(禁用后前台侧栏/卡片不展示)
|
||||
sort INTEGER NOT NULL DEFAULT 0, -- 同级顺序,越小越靠前
|
||||
CHECK (
|
||||
(kind = 'section' AND parent_key IS NULL AND view_mode IS NOT NULL)
|
||||
OR (kind = 'group' AND parent_key IS NOT NULL AND view_mode IS NULL)
|
||||
(kind = 'section' AND parent_key IS NULL)
|
||||
OR (kind = 'group' AND parent_key IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -95,18 +87,6 @@ CREATE TABLE IF NOT EXISTS entry_tag (
|
||||
PRIMARY KEY (entry_key, sort)
|
||||
);
|
||||
|
||||
-- 首页卡片:一行一个卡片,说明展示哪个节点
|
||||
-- node_key 指向要展示的节点(通常是分组;电影/文章这类无分组的填分区自己)
|
||||
-- 「属于哪个分区」不单独存:分组看 parent_key,分区就是它自己,查询时推导
|
||||
CREATE TABLE IF NOT EXISTS section_block (
|
||||
id INTEGER PRIMARY KEY, -- 主键(SQLite rowid,自动分配)
|
||||
node_key TEXT NOT NULL REFERENCES node(key) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
-- 要展示的节点业务键
|
||||
title TEXT, -- 块标题;NULL 时用节点 title
|
||||
preview_limit INTEGER, -- 卡片里显示几条;NULL 时用前端默认
|
||||
sort INTEGER NOT NULL DEFAULT 0 -- 卡片顺序
|
||||
);
|
||||
|
||||
-- ========== v16:登录与设置 ==========
|
||||
|
||||
-- 管理员:单用户场景只允许一行
|
||||
@@ -226,6 +206,41 @@ const V22_SQL: &str = r#"
|
||||
INSERT OR IGNORE INTO card (key, title, sort) VALUES ('local-service', '本地服务', -1);
|
||||
"#;
|
||||
|
||||
/// v22 → v23:删除旧分区展示块与 node 上的兼容字段,node 只保留当前模型需要的列。
|
||||
const V23_SQL: &str = r#"
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
DROP TABLE IF EXISTS section_block;
|
||||
|
||||
CREATE TABLE node_new (
|
||||
id INTEGER PRIMARY KEY,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
parent_key TEXT REFERENCES node(key) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('section', 'group')),
|
||||
bar_color TEXT,
|
||||
icon TEXT,
|
||||
preview_limit INTEGER,
|
||||
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
||||
sort INTEGER NOT NULL DEFAULT 0,
|
||||
CHECK (
|
||||
(kind = 'section' AND parent_key IS NULL)
|
||||
OR (kind = 'group' AND parent_key IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
INSERT INTO node_new (key, parent_key, title, kind, bar_color, icon, preview_limit, enabled, sort)
|
||||
SELECT key, parent_key, title, kind, bar_color, icon, preview_limit, enabled, sort
|
||||
FROM node;
|
||||
|
||||
DROP TABLE node;
|
||||
ALTER TABLE node_new RENAME TO node;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS node_parent_idx ON node(parent_key, sort);
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
"#;
|
||||
|
||||
/// v20 → v21:格式从分组下沉到条目;老数据按所属根分组的 view_mode 回填。
|
||||
const V21_SQL: &str = r#"
|
||||
ALTER TABLE entry ADD COLUMN format TEXT NOT NULL DEFAULT 'apps';
|
||||
@@ -302,7 +317,7 @@ pub struct SeedReport {
|
||||
///
|
||||
/// 从 v16 起有了第一套迁移:`migrate` 会把老库逐级升上来,
|
||||
/// 只有「比程序更新」或「没有迁移路径」的版本才拒绝启动。
|
||||
pub const SCHEMA_VERSION: i64 = 22;
|
||||
pub const SCHEMA_VERSION: i64 = 23;
|
||||
|
||||
fn current_version(conn: &Connection) -> Result<i64, StashError> {
|
||||
conn.pragma_query_value(None, "user_version", |row| row.get(0))
|
||||
@@ -372,6 +387,11 @@ pub fn migrate(conn: &Connection) -> Result<i64, StashError> {
|
||||
version = 22;
|
||||
}
|
||||
|
||||
if version == 22 {
|
||||
conn.execute_batch(V23_SQL).map_err(map_error)?;
|
||||
version = 23;
|
||||
}
|
||||
|
||||
conn.pragma_update(None, "user_version", version)
|
||||
.map_err(map_error)?;
|
||||
|
||||
@@ -416,18 +436,14 @@ pub fn seed(conn: &mut Connection) -> Result<SeedReport, StashError> {
|
||||
}
|
||||
|
||||
tx.execute(
|
||||
"INSERT INTO node (key, parent_key, title, kind, view_mode, bar_color, icon, preview_limit, empty_text, show_in_sidebar, allow_groups, sort)
|
||||
VALUES (?1, NULL, ?2, 'section', ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
"INSERT INTO node (key, parent_key, title, kind, bar_color, icon, preview_limit, sort)
|
||||
VALUES (?1, NULL, ?2, 'section', ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
section.key,
|
||||
section.title,
|
||||
view,
|
||||
section.bar_color,
|
||||
section.icon,
|
||||
section.preview_limit,
|
||||
section.empty_text,
|
||||
i64::from(section.show_in_sidebar),
|
||||
i64::from(view == "apps"),
|
||||
section.sort,
|
||||
],
|
||||
)
|
||||
@@ -437,15 +453,9 @@ pub fn seed(conn: &mut Connection) -> Result<SeedReport, StashError> {
|
||||
if view == "apps" {
|
||||
for group in §ion.groups {
|
||||
tx.execute(
|
||||
"INSERT INTO node (key, parent_key, title, kind, show_in_sidebar, sort)
|
||||
VALUES (?1, ?2, ?3, 'group', ?4, ?5)",
|
||||
params![
|
||||
group.key,
|
||||
section.key,
|
||||
group.title,
|
||||
i64::from(group.show_in_sidebar),
|
||||
group.sort
|
||||
],
|
||||
"INSERT INTO node (key, parent_key, title, kind, sort)
|
||||
VALUES (?1, ?2, ?3, 'group', ?4)",
|
||||
params![group.key, section.key, group.title, group.sort],
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
report.groups += 1;
|
||||
@@ -461,17 +471,26 @@ pub fn seed(conn: &mut Connection) -> Result<SeedReport, StashError> {
|
||||
}
|
||||
}
|
||||
|
||||
// 首页该分区展示哪个节点:节点必须已经插入(分组或分区本身)
|
||||
// 首页卡片:仓库里直接生成卡片与勾选的分组
|
||||
tx.execute(
|
||||
"INSERT INTO section_block (node_key, title, preview_limit, sort)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
"INSERT INTO card (key, title, sort) VALUES (?1, ?2, ?3)",
|
||||
params![
|
||||
section.block.node_key,
|
||||
section.block.title,
|
||||
section.block.preview_limit,
|
||||
format!("card-{}", section.key),
|
||||
section
|
||||
.block
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| section.title.clone()),
|
||||
section.sort,
|
||||
],
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
let card_id = tx.last_insert_rowid();
|
||||
tx.execute(
|
||||
"INSERT INTO card_group (card_id, group_key, sort, preview_limit)
|
||||
VALUES (?1, ?2, 0, ?3)",
|
||||
params![card_id, section.block.node_key, section.block.preview_limit],
|
||||
)
|
||||
.map_err(|error| {
|
||||
StashError::Database(format!(
|
||||
"分区 {} 的展示节点 {} 无效: {error}",
|
||||
@@ -481,9 +500,6 @@ pub fn seed(conn: &mut Connection) -> Result<SeedReport, StashError> {
|
||||
report.blocks += 1;
|
||||
}
|
||||
|
||||
// 老结构(分区 → 展示块)同步生成卡片,保证新首页立刻有内容
|
||||
tx.execute_batch(BACKFILL_CARDS_SQL).map_err(map_error)?;
|
||||
|
||||
tx.commit().map_err(map_error)?;
|
||||
|
||||
Ok(report)
|
||||
@@ -565,11 +581,6 @@ struct SeedSection {
|
||||
sort: i64,
|
||||
#[serde(default)]
|
||||
preview_limit: Option<i64>,
|
||||
#[serde(default)]
|
||||
empty_text: Option<String>,
|
||||
/// 是否出现在侧栏;默认 true
|
||||
#[serde(default = "default_true")]
|
||||
show_in_sidebar: bool,
|
||||
/// 仅 apps 分区使用
|
||||
#[serde(default)]
|
||||
groups: Vec<SeedGroup>,
|
||||
@@ -601,17 +612,9 @@ struct SeedGroup {
|
||||
key: String,
|
||||
title: String,
|
||||
sort: i64,
|
||||
/// 是否出现在侧栏;默认 true
|
||||
#[serde(default = "default_true")]
|
||||
show_in_sidebar: bool,
|
||||
entries: Vec<SeedEntry>,
|
||||
}
|
||||
|
||||
/// serde 默认值:侧栏默认显示
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// 一条内容:字段与 `entry` 表的列一一对应。
|
||||
///
|
||||
/// `tags` / `meta` 都写进 `entry_tag`(海报用 tags,图文用 meta);
|
||||
|
||||
Reference in New Issue
Block a user