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:
2026-11-02 20:24:08 +08:00
parent 4621f8cb22
commit 27e411fd2c
10 changed files with 213 additions and 1652 deletions
+16 -52
View File
@@ -1,16 +1,14 @@
//! 分组树的管理:多层分组、格式、父级移动与排序。
//! 分组树的管理:多层分组、父级移动与排序。
//!
//! 数据层只有一张 `node` 树:顶层节点是「根分组」(`kind='section'`,带
//! `view_mode` 格式),下面的节点都叫分组(`kind='group'`),可以继续嵌套。
//! 条目挂在任意节点上,格式由最近的根分组决定。
//! 数据层只有一张 `node` 树:顶层节点是「根分组」(`kind='section'`),
//! 下面的节点都叫分组(`kind='group'`),可以继续嵌套。
//! 条目的展示格式由 `entry.format` 决定,分组不再保存格式。
use rusqlite::{Connection, OptionalExtension, params};
use serde::Deserialize;
use crate::{db::entries, db::map_error, error::StashError};
const FORMATS: [&str; 3] = ["apps", "posters", "articles"];
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NewNode {
@@ -20,9 +18,6 @@ pub struct NewNode {
/// 为空表示新建一个根分组
#[serde(default)]
pub parent_key: Option<String>,
/// 根分组的格式:apps / posters / articles(子分组继承,不需要传)
#[serde(default)]
pub format: Option<String>,
/// 图标:可以是 iconfont 名或字符;空字符串表示不显示
#[serde(default)]
pub icon: Option<String>,
@@ -41,8 +36,6 @@ pub struct UpdateNode {
/// 传入即移动到新的父节点;不支持移回根(根分组请直接新建)
pub parent_key: Option<String>,
pub sort: Option<i64>,
/// 只对根分组生效
pub format: Option<String>,
/// 图标:空字符串表示清除图标
pub icon: Option<String>,
pub enabled: Option<bool>,
@@ -78,7 +71,7 @@ pub fn create(conn: &mut Connection, new: NewNode) -> Result<(), StashError> {
return Err(StashError::Conflict(format!("分组已存在: {key}")));
}
let (kind, view_mode) = match new.parent_key.as_deref() {
let kind = match new.parent_key.as_deref() {
Some(parent) => {
let parent_exists: i64 = conn
.query_row("SELECT COUNT(*) FROM node WHERE key = ?1", [parent], |row| {
@@ -88,15 +81,9 @@ pub fn create(conn: &mut Connection, new: NewNode) -> Result<(), StashError> {
if parent_exists == 0 {
return Err(StashError::NotFound(format!("父分组不存在: {parent}")));
}
("group", None)
}
None => {
let format = new.format.unwrap_or_else(|| "apps".into());
if !FORMATS.contains(&format.as_str()) {
return Err(StashError::InvalidDirectory("格式不合法".into()));
}
("section", Some(format))
"group"
}
None => "section",
};
let default_sort: i64 = match new.parent_key.as_deref() {
@@ -126,14 +113,13 @@ pub fn create(conn: &mut Connection, new: NewNode) -> Result<(), StashError> {
let tx = conn.transaction().map_err(map_error)?;
tx.execute(
"INSERT INTO node (key, parent_key, title, kind, view_mode, icon, enabled, sort, show_in_sidebar, allow_groups)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 1, 1)",
"INSERT INTO node (key, parent_key, title, kind, icon, enabled, sort)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![
key,
new.parent_key,
title,
kind,
view_mode,
icon,
enabled,
sort
@@ -146,9 +132,9 @@ pub fn create(conn: &mut Connection, new: NewNode) -> Result<(), StashError> {
}
pub fn update(conn: &mut Connection, key: &str, patch: UpdateNode) -> Result<(), StashError> {
let current: Option<(String, Option<String>, Option<String>, i64, Option<String>, bool)> = conn
let current: Option<(String, Option<String>, i64, Option<String>, bool)> = conn
.query_row(
"SELECT title, parent_key, view_mode, sort, icon, enabled FROM node WHERE key = ?1",
"SELECT title, parent_key, sort, icon, enabled FROM node WHERE key = ?1",
[key],
|row| {
Ok((
@@ -157,15 +143,13 @@ pub fn update(conn: &mut Connection, key: &str, patch: UpdateNode) -> Result<(),
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
))
},
)
.optional()
.map_err(map_error)?;
let Some((current_title, current_parent, current_format, current_sort, current_icon, current_enabled)) =
current
let Some((current_title, current_parent, current_sort, current_icon, current_enabled)) = current
else {
return Err(StashError::NotFound(format!("分组不存在: {key}")));
};
@@ -218,26 +202,6 @@ pub fn update(conn: &mut Connection, key: &str, patch: UpdateNode) -> Result<(),
None => current_parent.clone(),
};
let is_root = parent_key.is_none();
let format = if is_root {
match patch.format {
Some(format) => {
if !FORMATS.contains(&format.as_str()) {
return Err(StashError::InvalidDirectory("格式不合法".into()));
}
Some(format)
}
None => current_format,
}
} else {
// 子分组没有自己的格式,跟随根分组
None
};
if is_root && format.is_none() {
return Err(StashError::InvalidDirectory("根分组缺少格式".into()));
}
let icon = match patch.icon {
Some(icon) => {
let icon = icon.trim().to_string();
@@ -268,10 +232,10 @@ pub fn update(conn: &mut Connection, key: &str, patch: UpdateNode) -> Result<(),
};
tx.execute(
"UPDATE node SET title = ?1, parent_key = ?2, view_mode = ?3, sort = ?4,
icon = ?5, enabled = ?6
WHERE key = ?7",
params![title, parent_key, format, final_sort, icon, enabled, key],
"UPDATE node SET title = ?1, parent_key = ?2, sort = ?3,
icon = ?4, enabled = ?5
WHERE key = ?6",
params![title, parent_key, final_sort, icon, enabled, key],
)
.map_err(map_error)?;
entries::audit(&tx, "node.update", key, &title)?;