本文件回答:数据库有哪些表、每张表有哪些字段、约束、索引与业务规则。 依赖:无
每张表统一按四部分书写:用途、字段表、索引、规则。
全局约定:
uuid,默认 gen_random_uuid()。需要 PostgreSQL 13+,或启用 pgcrypto 扩展。timestamptz。snake_case。text + CHECK,不用 PostgreSQL 原生 enum(便于后续加值不改类型)。pg_trgm 扩展,数据库初始化时必须执行 CREATE EXTENSION IF NOT EXISTS pg_trgm;。zhparser / pg_jieba / pg_bigm 等)。搜索走 PostgreSQL 自带的模糊匹配,理由见第八节。user_id 归属字段,即使本期单人自用。这是技术预留:注册功能本期不实现,users 表只放唯一账号,但数据模型从第一天就按“未来可能有第二个账号”设计,避免以后改表。users → notes / tags / portfolio_items,migration 必须按此顺序创建。各表小节引用此处,不重复定义。
| 枚举名 | 取值 | 说明 |
|---|---|---|
note_visibility |
private / public |
笔记可见性 |
repo_platform |
gogs / github |
作品集仓库托管平台 |
token_scope |
read / read_write |
API Token 权限档位(用于 tokens 表,待补) |
用途:存储所有笔记内容,私有与公开共用一张表,靠 visibility 区分。
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|---|---|---|---|---|
id |
uuid | PRIMARY KEY | gen_random_uuid() |
主键 |
user_id |
uuid | NOT NULL, FK → users(id) ON DELETE CASCADE | — | 所属用户 |
title |
text | NOT NULL | — | 标题 |
content |
text | NOT NULL | '' |
正文,允许空串 |
visibility |
text | NOT NULL, CHECK (visibility IN ('private','public')) | 'private' |
可见性,默认私有 |
published_at |
timestamptz | NULL | NULL | 首次公开发布时间,仅 public 时有意义 |
search_text |
text | GENERATED ALWAYS AS (`title | ' ' | |
created_at |
timestamptz | NOT NULL | now() |
创建时间 |
updated_at |
timestamptz | NOT NULL | now() |
最后更新时间,由应用层在每次写入时更新 |
索引:
-- 公开列表:按用户 + 发布时间倒序
CREATE INDEX idx_notes_public ON notes (user_id, published_at DESC)
WHERE visibility = 'public' AND published_at IS NOT NULL;
-- 私有工作台:按用户 + 最近更新排序
CREATE INDEX idx_notes_updated_at ON notes (user_id, updated_at DESC);
-- 模糊搜索:search_text trigram 索引
CREATE INDEX idx_notes_search_trgm ON notes USING gin (search_text gin_trgm_ops);
规则:
DEFAULT 'private' + NOT NULL 兜底;创建接口不接受 visibility 参数(见 conventions.md 第四节)。private → public 通过独立的 publish 操作完成,该操作在 published_at IS NULL 时写入当前时间。published_at:public → private 时 published_at 保留,语义为“首次公开发布时间”;再次公开不覆盖。visibility = 'public' AND published_at IS NOT NULL。任何公开读路径必须使用此条件,不得只看 visibility。published_at 对 private 笔记无意义:private 笔记不因 published_at 是否存在而改变可见性,该字段只服务于公开列表排序与未来 RSS。search_text 由数据库自动维护:应用层不得在 INSERT / UPDATE 时写入该字段,也不得在 Go 代码中手动拼接。搜索查询统一使用 search_text,不直接查 title 或 content。docs/backend/auth.md,本表不重复定义。WHERE user_id = current_user_id。授权中间件在 visibility 过滤之外,必须叠加用户归属过滤。跨用户访问视为越权,返回 404 而非 403(不泄露笔记存在性)。用途:存储所有标签名,标签本身独立于笔记存在(允许创建后暂不关联任何笔记)。
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|---|---|---|---|---|
id |
uuid | PRIMARY KEY | gen_random_uuid() |
主键 |
user_id |
uuid | NOT NULL, FK → users(id) ON DELETE CASCADE | — | 所属用户 |
name |
text | NOT NULL | — | 标签名 |
created_at |
timestamptz | NOT NULL | now() |
创建时间 |
索引:
-- 同一用户下标签名不区分大小写唯一
CREATE UNIQUE INDEX idx_tags_user_name_lower ON tags (user_id, lower(name));
-- 标签名模糊搜索
CREATE INDEX idx_tags_name_trgm ON tags USING gin (name gin_trgm_ops);
规则:
(user_id, lower(name))。Go 和 go 在同一用户下视为同一个标签。写入时应用层可保留原样,但唯一性判断以 lower(name) 为准。note_tags 中的关联由外键 ON DELETE CASCADE 自动清理。如需重命名,先改本文件。WHERE user_id = current_user_id。用途:多对多关联表,一篇笔记可挂多个标签,一个标签可用于多篇笔记。
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|---|---|---|---|---|
note_id |
uuid | NOT NULL, FK → notes(id) ON DELETE CASCADE | — | 笔记 |
tag_id |
uuid | NOT NULL, FK → tags(id) ON DELETE CASCADE | — | 标签 |
主键:(note_id, tag_id) 复合主键,天然防止重复关联。
索引:
-- 复合主键已覆盖 note_id 开头的查询;单独为 tag_id 建索引,支持"按标签查笔记"
CREATE INDEX idx_note_tags_tag_id ON note_tags (tag_id);
规则:
note_tags 本身没有 user_id,用户归属由 notes.user_id 和 tags.user_id 分别保证。应用层在创建关联时必须校验 notes.user_id = tags.user_id = current_user_id,防止把 A 用户的标签挂到 B 用户的笔记上。这一校验在应用层完成,数据库层不强制(可以用触发器强制,但增加复杂度,本期不做)。用途:主页作品集的条目本体,一个作品可以关联多个仓库链接。
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|---|---|---|---|---|
id |
uuid | PRIMARY KEY | gen_random_uuid() |
主键 |
user_id |
uuid | NOT NULL, FK → users(id) ON DELETE CASCADE | — | 所属用户 |
title |
text | NOT NULL | — | 作品标题 |
description |
text | NOT NULL | '' |
简介 |
sort_order |
int | NOT NULL | 0 |
主页排序,小的在前 |
created_at |
timestamptz | NOT NULL | now() |
创建时间 |
索引:
CREATE INDEX idx_portfolio_items_sort ON portfolio_items (user_id, sort_order);
规则:
portfolio_repos 表中。WHERE user_id = ? 作为未来扩展的保险。用途:一个作品可关联多个仓库链接(如同时托管在 Gogs 和 GitHub)。
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|---|---|---|---|---|
id |
uuid | PRIMARY KEY | gen_random_uuid() |
主键 |
portfolio_item_id |
uuid | NOT NULL, FK → portfolio_items(id) ON DELETE CASCADE | — | 所属作品 |
platform |
text | NOT NULL, CHECK (platform IN ('gogs','github')) | — | 托管平台 |
repo_path |
text | NOT NULL | — | 如 username/repo-name |
is_primary |
boolean | NOT NULL | false |
是否主链接,主页默认跳转这一个 |
索引:
-- 同一作品下平台 + 路径唯一
CREATE UNIQUE INDEX idx_portfolio_repos_unique ON portfolio_repos (portfolio_item_id, platform, repo_path);
-- 每个作品最多一个主链接
CREATE UNIQUE INDEX idx_portfolio_repos_primary ON portfolio_repos (portfolio_item_id)
WHERE is_primary = true;
规则:
repo_path,不存完整 URL:完整 URL 由 platform + 配置中的 base URL 拼接。Gogs 的 base URL 放在应用配置里(见 architecture.md),GitHub 固定为 https://github.com。is_primary = true 的仓库,主页展示逻辑需处理这种情况。portfolio_repos 不单独带 user_id,归属由所属作品决定。任何仓库操作必须先校验所属作品的 user_id = current_user_id。范围:笔记标题、笔记正文、标签名。三处都支持模糊包含查询。
技术:
notes.search_text 生成列合并,对合并字段建 pg_trgm GIN 索引,查询 WHERE user_id = ? AND search_text ILIKE '%关键词%'。tags.name 建 pg_trgm GIN 索引,查询 WHERE user_id = ? AND name ILIKE '%关键词%'。返回:
note_tags + tags)。api.md 定义,本文件不规定。以下四张表的字段和规则依赖 auth.md 与 audit.md 定稿,本文件暂不展开,等那两个文件定稿后补入本节:
users — 用户账号。本期只有一条记录(当前使用者),注册功能不实现。trusted_devices — 可授信浏览器设备。tokens — 长期 API Token。audit_events — 审计事件。