feat(lyra): 接入 PostgreSQL 与 Goose 迁移
- 使用 pgx/sqlx 建立连接,从环境变量读取数据库密码 - 添加本地 PostgreSQL Compose 配置与初始迁移 - 补充 Goose 操作手册和连接串排错说明
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
-- Lyra 初始迁移:与 docs/demo.sql 的建库内容保持一致(函数、三张表、约束、注释、索引、触发器)。
|
||||
|
||||
-- +goose Up
|
||||
-- updated_at 维护函数
|
||||
-- +goose StatementBegin
|
||||
CREATE FUNCTION public.set_updated_at()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at := now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE public.users (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
account text NOT NULL UNIQUE,
|
||||
password_hash text NOT NULL,
|
||||
display_name text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT users_account_not_empty CHECK (btrim(account) <> ''),
|
||||
CONSTRAINT users_display_name_not_empty CHECK (btrim(display_name) <> '')
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.users IS '后台管理用户表';
|
||||
COMMENT ON COLUMN public.users.id IS '用户 ID';
|
||||
COMMENT ON COLUMN public.users.account IS '登录账号,当前大小写敏感';
|
||||
COMMENT ON COLUMN public.users.password_hash IS '密码哈希';
|
||||
COMMENT ON COLUMN public.users.display_name IS '展示昵称';
|
||||
COMMENT ON COLUMN public.users.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN public.users.updated_at IS '更新时间,触发器自动刷新';
|
||||
|
||||
-- 分类表
|
||||
CREATE TABLE public.categories (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
slug text NOT NULL UNIQUE,
|
||||
title text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
icon text NOT NULL,
|
||||
sort integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT categories_slug_format CHECK (slug ~ '^[a-z0-9]+(-[a-z0-9]+)*$'),
|
||||
CONSTRAINT categories_title_not_empty CHECK (btrim(title) <> '')
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.categories IS '链接分类表';
|
||||
COMMENT ON COLUMN public.categories.id IS '分类 ID,内部关联用';
|
||||
COMMENT ON COLUMN public.categories.slug IS '页面地址(路由)标识';
|
||||
COMMENT ON COLUMN public.categories.title IS '分类标题';
|
||||
COMMENT ON COLUMN public.categories.description IS '分类描述';
|
||||
COMMENT ON COLUMN public.categories.icon IS '分类图标标识';
|
||||
COMMENT ON COLUMN public.categories.sort IS '分类排序,越小越靠前';
|
||||
COMMENT ON COLUMN public.categories.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN public.categories.updated_at IS '更新时间,触发器自动刷新';
|
||||
|
||||
-- 链接表
|
||||
CREATE TABLE public.links (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
category_id bigint NOT NULL,
|
||||
title text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
url text NOT NULL,
|
||||
icon text,
|
||||
keywords text[] NOT NULL DEFAULT ARRAY[]::text[],
|
||||
sort integer NOT NULL DEFAULT 0,
|
||||
pinned boolean NOT NULL DEFAULT false,
|
||||
pinned_sort integer,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT links_category_fk FOREIGN KEY (category_id) REFERENCES public.categories (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT links_title_not_empty CHECK (btrim(title) <> ''),
|
||||
CONSTRAINT links_url_not_empty CHECK (btrim(url) <> ''),
|
||||
CONSTRAINT links_pinned_sort_consistent CHECK (pinned = (pinned_sort IS NOT NULL)),
|
||||
CONSTRAINT links_keywords_one_dimension CHECK (cardinality(keywords) = 0 OR array_ndims(keywords) = 1),
|
||||
CONSTRAINT links_keywords_no_null_elements CHECK (array_position(keywords, NULL) IS NULL)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.links IS '链接表,归属分类';
|
||||
COMMENT ON COLUMN public.links.id IS '链接 ID';
|
||||
COMMENT ON COLUMN public.links.category_id IS '所属分类 ID,删分类前需先处理链接';
|
||||
COMMENT ON COLUMN public.links.title IS '链接标题';
|
||||
COMMENT ON COLUMN public.links.description IS '链接描述';
|
||||
COMMENT ON COLUMN public.links.url IS '链接地址,应用层校验,去尾斜杠后全局唯一';
|
||||
COMMENT ON COLUMN public.links.icon IS '链接图标,可空,前端回退';
|
||||
COMMENT ON COLUMN public.links.keywords IS '搜索关键词数组,供本地搜索匹配';
|
||||
COMMENT ON COLUMN public.links.sort IS '分类内排序,越小越靠前';
|
||||
COMMENT ON COLUMN public.links.pinned IS '首页快捷入口(手动固定),非点击排行';
|
||||
COMMENT ON COLUMN public.links.pinned_sort IS '首页快捷入口顺序,越小越靠前';
|
||||
COMMENT ON COLUMN public.links.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN public.links.updated_at IS '更新时间,触发器自动刷新';
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX categories_order_idx
|
||||
ON public.categories (sort, id);
|
||||
|
||||
CREATE INDEX links_category_order_idx
|
||||
ON public.links (category_id, sort, id);
|
||||
|
||||
CREATE INDEX links_pinned_order_idx
|
||||
ON public.links (pinned_sort, id)
|
||||
WHERE pinned;
|
||||
|
||||
CREATE UNIQUE INDEX links_normalized_url_unique
|
||||
ON public.links (rtrim(url, '/'));
|
||||
|
||||
-- 触发器
|
||||
CREATE TRIGGER users_set_updated_at
|
||||
BEFORE UPDATE ON public.users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
CREATE TRIGGER categories_set_updated_at
|
||||
BEFORE UPDATE ON public.categories
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
CREATE TRIGGER links_set_updated_at
|
||||
BEFORE UPDATE ON public.links
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
-- +goose Down
|
||||
-- 按依赖逆序删除:链接表 → 分类表 → 用户表 → 函数。
|
||||
-- 删表会一并移除表上的触发器与索引,故无需单独 DROP;
|
||||
-- 不用 CASCADE / IF EXISTS,出现意外依赖时让删除直接报错。
|
||||
DROP TABLE public.links;
|
||||
DROP TABLE public.categories;
|
||||
DROP TABLE public.users;
|
||||
DROP FUNCTION public.set_updated_at();
|
||||
Reference in New Issue
Block a user