知识以前只能"填个标题就完成",正文、标签、归类都要点进去再补。现在新建 就是一张完整的备忘录:标题、类型、分类、标签、正文一次写完,存完直接打开 详情继续编辑;知识页左侧多了工作区三级分类树,可按分类子树 / 未分类 / 标签 筛选。凡是填信息的抽屉都加宽了,还能一键展开整屏。 后端 - 新增 `knowledge_category`(迁移 0007):`workspace_id NOT NULL` + 自引用 `parent_id`、`depth` 1..3 检查约束、同层重名唯一索引(lower(name))、 RLS 策略与 `dadao_app` 授权;`knowledge_doc.category_id` 外键 ON DELETE SET NULL。 - 新增 `/knowledge/categories` 增删改查:超三级 `7007`、同层重名 `7008`、 跨租户或不存在 `7006`;`move=true` 才改父级,成环与移动后超过三级的子树 一律拒绝,删除有知识的分类要 `force=true`(知识变未分类)。 - 知识列表支持 `category_id`(含子树)、`uncategorized`、`tag`;输出带 `category`(含 `path` / `full_path`)。 - PATCH 只改分类或标签也会推进 `version`,避免"只动标签"之后下次保存撞 乐观锁 `4009`。 Web - `Sheet` 增 `size="md/lg/xl"` 与展开整屏按钮;任务、工单、知识抽屉统一 加宽(`TaskDetail` / `Tickets` / `Knowledge`)。 - 知识页:左侧三级分类树(新建、按子树筛选、未分类)、`KnowledgeComposer` 一次写全新的备忘录、列表卡片露出分类路径与标签、详情抽屉里可直接改分类 与标签。 - 抽屉详情只拉一次:重复的 effect 或迟到的响应不再覆盖用户正在敲的内容。 安卓 - `Api.kt`:分类模型与接口、新建/更新带 `tag_names`/`category_id`、 列表带分类与标签筛选。 - 知识页重做:新建改为近全屏大对话框(标题/类型/分类/标签/正文),列表 顶部按分类筛选,详情里能改分类与标签后单独保存。 文档与测试 - `2.Design/API.md`:5.1/5.3/5.4 补分类与筛选参数,新增 5.5 分类接口章节, 错误码补 `7006`/`7007`/`7008`。 - `2.Design/ROADMAP-M0-M1.md`:新增 3.11(三级分类)、3.12(备忘录形态)。 - 新增 `backend/tests/test_knowledge_categories.py`(8 例):备忘形态落库、 三级到底、移动越界与成环、重名、按子树/未分类/标签/关键词筛选、删分类需 `force`、跨租户隔离。 - 新增 `web/e2e/knowledge-notebook.mjs`(10 项,全绿):建两级分类 → 一次 写全 → 卡片与筛选 → 抽屉改标签/分类 → 正文落库 → 重名 7008。 验证:`pytest` 93 passed、`ruff check` / `ruff format --check` 全过、 `tsc --noEmit` 通过、`vite build` 通过、`knowledge-notebook.mjs` 10 项全绿。 安卓端本机只有 JDK 25,Gradle 8.7 / AGP 8.7.3 不支持,本次未做编译验证。
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""知识分类(M3 补充)
|
||||
|
||||
Revision ID: 0007
|
||||
Revises: 0006
|
||||
Create Date: 2026-10-30
|
||||
|
||||
知识库的基本形态是"备忘录",不是"任务":一条知识要有标题、正文、标签,
|
||||
还要能归类。这里补上分类树(一级大类 / 二级 / 三级,深不过三)。
|
||||
|
||||
两个取舍:
|
||||
|
||||
1. **分类独立成表,不塞进 item**。分类是工作区级的字典,一条知识挂一个分类;
|
||||
塞进 item 会让"分类"变成每条知识自己的一份副本,改个名字要改几百行。
|
||||
2. **删分类不删知识**。外键是 `ON DELETE SET NULL`:知识退回"未分类",
|
||||
分类只是入口,不是知识的容器。真要连内容一起删,那是删知识,得单独确认。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0007"
|
||||
down_revision: str = "0006"
|
||||
branch_labels: Sequence[str] | None = None
|
||||
depends_on: Sequence[str] | None = None
|
||||
|
||||
CREATE_SQL = """
|
||||
CREATE TABLE knowledge_category (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
parent_id uuid REFERENCES knowledge_category(id) ON DELETE CASCADE,
|
||||
name varchar(40) NOT NULL,
|
||||
depth integer NOT NULL DEFAULT 1,
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ck_kcat_depth CHECK (depth BETWEEN 1 AND 3),
|
||||
CONSTRAINT ck_kcat_name CHECK (length(btrim(name)) > 0)
|
||||
)
|
||||
"""
|
||||
|
||||
INDEX_STATEMENTS = (
|
||||
"CREATE INDEX ix_kcat_ws_parent ON knowledge_category (workspace_id, parent_id)",
|
||||
# 同一父级下不许重名。根分类的 parent_id 是 NULL,而 PG 里 NULL 互不相等,
|
||||
# 所以根要单独一条 `WHERE parent_id IS NULL` 的部分索引,否则可以建出两个同名大类。
|
||||
"CREATE UNIQUE INDEX uq_kcat_ws_parent_name ON knowledge_category "
|
||||
"(workspace_id, parent_id, lower(name)) WHERE parent_id IS NOT NULL",
|
||||
"CREATE UNIQUE INDEX uq_kcat_ws_root_name ON knowledge_category "
|
||||
"(workspace_id, lower(name)) WHERE parent_id IS NULL",
|
||||
"COMMENT ON TABLE knowledge_category IS '知识分类树,深不过三'",
|
||||
"ALTER TABLE knowledge_doc ADD COLUMN category_id uuid "
|
||||
"REFERENCES knowledge_category(id) ON DELETE SET NULL",
|
||||
"CREATE INDEX ix_knowledge_ws_category ON knowledge_doc (workspace_id, category_id)",
|
||||
)
|
||||
|
||||
POLICY_SQL = (
|
||||
"ALTER TABLE knowledge_category ENABLE ROW LEVEL SECURITY",
|
||||
"CREATE POLICY knowledge_category_isolation ON knowledge_category "
|
||||
"USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())",
|
||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON knowledge_category TO dadao_app",
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(CREATE_SQL)
|
||||
for statement in INDEX_STATEMENTS:
|
||||
op.execute(statement)
|
||||
for statement in POLICY_SQL:
|
||||
op.execute(statement)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_knowledge_ws_category")
|
||||
op.execute("ALTER TABLE knowledge_doc DROP COLUMN IF EXISTS category_id")
|
||||
op.execute("DROP POLICY IF EXISTS knowledge_category_isolation ON knowledge_category")
|
||||
op.execute("DROP TABLE IF EXISTS knowledge_category")
|
||||
Reference in New Issue
Block a user