From 8c0c9978e507f97589781196162897bad7e26694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=82=B2=E6=96=B0?= <2635131263@qq.com> Date: Sat, 17 Oct 2026 15:09:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(lyra):=20=E6=8E=A5=E5=85=A5=20PostgreSQL?= =?UTF-8?q?=20=E4=B8=8E=20Goose=20=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 pgx/sqlx 建立连接,从环境变量读取数据库密码 - 添加本地 PostgreSQL Compose 配置与初始迁移 - 补充 Goose 操作手册和连接串排错说明 --- .gitignore | 3 +- go/projects/lyra/README.md | 13 +- go/projects/lyra/cmd/lyra/main.go | 36 +++- go/projects/lyra/compose.yaml | 36 ++++ go/projects/lyra/docs/architecture.md | 11 +- go/projects/lyra/docs/demo.sql | 10 +- go/projects/lyra/docs/goose.md | 179 ++++++++++++++++++ .../2026-10-17-goose-initial-migration.md | 71 +++++++ .../2026-10-17-postgres-connection-check.md | 32 ++++ go/projects/lyra/etc/lyra.toml | 11 ++ go/projects/lyra/go.mod | 7 + go/projects/lyra/go.sum | 23 +++ go/projects/lyra/internal/config/config.go | 25 ++- go/projects/lyra/migrations/00001_init.sql | 134 +++++++++++++ go/projects/lyra/migrations/README.md | 22 ++- 15 files changed, 580 insertions(+), 33 deletions(-) create mode 100644 go/projects/lyra/compose.yaml create mode 100644 go/projects/lyra/docs/goose.md create mode 100644 go/projects/lyra/docs/plans/2026-10-17-goose-initial-migration.md create mode 100644 go/projects/lyra/docs/plans/2026-10-17-postgres-connection-check.md create mode 100644 go/projects/lyra/migrations/00001_init.sql diff --git a/.gitignore b/.gitignore index d658fd5..c0553ee 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ Thumbs.db # ---- Note 目录 ---- note/项目/ -docs/plans \ No newline at end of file +docs/plans +go/projects/lyra/data \ No newline at end of file diff --git a/go/projects/lyra/README.md b/go/projects/lyra/README.md index 0ea0e8e..94c0963 100644 --- a/go/projects/lyra/README.md +++ b/go/projects/lyra/README.md @@ -8,8 +8,12 @@ 已搭建目录骨架、程序入口 `main.go` 与 HTTP 通路:启动时加载 `etc/lyra.toml`, 注册 `GET /hello`(经 `httpx.OK` 输出响应)并启动 HTTP 服务;路由注册配有 httptest 回归测试,不监听实际端口、不连接数据库。各内部目录的职责与依赖 -约束记录在 `AGENTS.md` 中。数据库访问、迁移文件与真实业务逻辑均未实现; -架构文档中标注“待新增”的文件(links 等)尚未创建。 +约束记录在 `AGENTS.md` 中。数据库迁移 SQL 已就位:`migrations/00001_init.sql` +(初始函数与三张表),通过手工 goose CLI 执行,操作手册见 +[docs/goose.md](docs/goose.md),迁移目录说明见 +[migrations/README.md](migrations/README.md);Go 应用侧的数据库连接与 +Goose Go 依赖仍未实现,真实业务逻辑同样未实现;架构文档中标注“待新增”的 +文件(links 等)尚未创建。 ## 目录职责 @@ -24,7 +28,7 @@ httptest 回归测试,不监听实际端口、不连接数据库。各内部 | `internal/service/` | 业务规则、事务协调;可按业务分包,服务级简单接口平铺在根包 | | `internal/model/` | 数据结构定义与数据访问(SQL,平铺) | | `internal/httpx/` | 公共响应辅助 | -| `migrations/` | Goose SQL 迁移文件(暂无) | +| `migrations/` | Goose SQL 迁移文件(00001 初始迁移已就位) | | `docs/` | 架构与计划文档 | ## 运行 @@ -42,4 +46,5 @@ go test ./... # 含路由回归测试 ## 技术栈 已引入 Gin 与 Viper(读取 TOML 配置),go.sum 已生成; -PostgreSQL、Squirrel + sqlx(pgx stdlib 驱动)与 Goose 为目标选型,随后续功能引入。 +PostgreSQL、Squirrel + sqlx(pgx stdlib 驱动)与 Goose 为目标选型,随后续功能引入; +数据库迁移已用独立 goose CLI(v3.29.0)管理,Goose Go 依赖尚未引入。 diff --git a/go/projects/lyra/cmd/lyra/main.go b/go/projects/lyra/cmd/lyra/main.go index 53ce603..4267a4a 100644 --- a/go/projects/lyra/cmd/lyra/main.go +++ b/go/projects/lyra/cmd/lyra/main.go @@ -2,30 +2,48 @@ package main import ( + "context" "fmt" + "log" "lyra/internal/config" "lyra/internal/router" "github.com/gin-gonic/gin" + "github.com/jmoiron/sqlx" + "github.com/joho/godotenv" + + // 注册 pgx 驱动,供 sqlx 以 "pgx" 驱动名使用。 + _ "github.com/jackc/pgx/v5/stdlib" ) func main() { + // 加载 .env.local 文件,覆盖环境变量 + if err := godotenv.Load(".env.local"); err != nil { + log.Fatal("加载环境变量失败,请检查文件是否存在") + } // 加载配置(internal/config) cfg, err := config.Load("etc/lyra.toml") if err != nil { - panic(err) + log.Fatal("加载配置失败,请检查配置文件和环境变量") } - // TODO: 组装依赖——建立 PostgreSQL 连接(internal/postgres)。 gin.SetMode(cfg.App.Env) - gEngine := gin.Default() + ginEngine := gin.Default() //没有反向代理,在启动前关闭代理信任: - if err := gEngine.SetTrustedProxies(nil); err != nil { - panic(err) + if err := ginEngine.SetTrustedProxies(nil); err != nil { + log.Fatal("设置代理信任失败,请检查配置") } - router.RegisterRoutes(gEngine) - // TODO: 退出时关闭连接、释放资源。 + dataSourceName := fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=disable", cfg.Database.Host, cfg.Database.Port, cfg.Database.User, cfg.Database.Name, cfg.Database.Password) + + ctx := context.Background() + db, err := sqlx.ConnectContext(ctx, cfg.Database.Driver, dataSourceName) + if err != nil { + log.Fatal("连接 PostgreSQL 失败,请检查连接配置、认证和网络") + } + defer db.Close() + + router.RegisterRoutes(ginEngine) addr := fmt.Sprintf("%s:%d", cfg.HTTP.Host, cfg.HTTP.Port) - if err := gEngine.Run(addr); err != nil { - panic(err) + if err := ginEngine.Run(addr); err != nil { + log.Fatal("启动 HTTP 服务失败,请检查端口是否被占用") } } diff --git a/go/projects/lyra/compose.yaml b/go/projects/lyra/compose.yaml new file mode 100644 index 0000000..2f56393 --- /dev/null +++ b/go/projects/lyra/compose.yaml @@ -0,0 +1,36 @@ +version: "3.8" + +services: + postgres: + image: abcfy2/zhparser:16-alpine + container_name: lab-postgres + restart: unless-stopped + environment: + POSTGRES_DB: lab + POSTGRES_USER: admin + POSTGRES_PASSWORD: ${PG_PASSWORD} + volumes: + - ./data/postgres:/var/lib/postgresql/data + # 测试环境不用刻意限制内存,让 Docker 自然调度即可 + networks: + - net-db # 只加入数据库专用网络 + # 如果外部需要直连调试,可临时 expose 端口 + ports: + - "127.0.0.1:5432:5432" + + redis: + image: redis:7-alpine + container_name: lab-redis + restart: unless-stopped + # 测试环境保留持久化更省心,不用关闭 + command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru + volumes: + - ./data/redis:/data + networks: + - net-db + ports: + - "127.0.0.1:6379:6379" + +networks: + net-db: + external: true # 使用预先创建的外部网络 diff --git a/go/projects/lyra/docs/architecture.md b/go/projects/lyra/docs/architecture.md index 3a1f47b..f7237a1 100644 --- a/go/projects/lyra/docs/architecture.md +++ b/go/projects/lyra/docs/architecture.md @@ -7,7 +7,7 @@ ## 已确认技术栈 - 已引入:Gin v1.12.0(HTTP 路由)、Viper v1.21.0(配置读取,TOML)。`go.sum` 已由 Go 工具生成。 -- 目标选型(选型已确认,尚未作为代码依赖引入):PostgreSQL、Squirrel + sqlx(pgx stdlib 驱动)、Goose(数据库迁移)。 +- 目标选型(选型已确认,尚未作为代码依赖引入):PostgreSQL、Squirrel + sqlx(pgx stdlib 驱动)、Goose(数据库迁移;迁移 SQL 已就位,以独立 goose CLI v3.29.0 执行,Go 依赖未引入)。 ## 当前进度 @@ -16,7 +16,8 @@ - 已注册 `GET /hello`:`internal/router/router.go` 的 `RegisterRoutes` 将其指向 `handler.Hello`,经 `httpx.OK` 输出 `api.Response`;路由注册配有 httptest 回归测试,不监听实际端口、不连接数据库。 - 已定义公共响应契约:`api.Response`(success/data/error)、`api.ErrorInfo`(code/message/debug)与错误码类型 `api.ErrorCode` 及其默认文案 `Message()`。 - 已实现响应输出辅助 `httpx.OK`、`httpx.Fail`。 -- 尚未实现:真实业务逻辑(`/hello` 只是 HTTP 通路示例)、数据库访问与迁移 SQL;配置校验、环境变量覆盖与默认值;`ErrorInfo.Debug` 的填充与按环境开关。日志方案未定。 +- 数据库迁移已就位:`migrations/00001_init.sql`(`set_updated_at()` 函数、users/categories/links 三张表、约束、注释、索引与触发器,建库内容与 [demo.sql](demo.sql) 一致),由手工 goose CLI(v3.29.0)执行,见 [../migrations/README.md](../migrations/README.md)。 +- 尚未实现:真实业务逻辑(`/hello` 只是 HTTP 通路示例)、Go 应用侧数据库访问与 Goose Go 依赖;配置校验、环境变量覆盖与默认值;`ErrorInfo.Debug` 的填充与按环境开关。日志方案未定。 ## 目录结构 @@ -52,7 +53,7 @@ lyra/ │ └── response.go # 公共响应输出辅助(OK/Fail) ├── etc/ │ └── lyra.toml # 运行配置(App/HTTP) -├── migrations/ # Goose SQL 迁移文件(暂无迁移,仅 README) +├── migrations/ # Goose SQL 迁移文件(00001_init.sql 初始迁移 + README) ├── docs/ │ ├── plans/ │ └── architecture.md # 本文 @@ -62,7 +63,7 @@ lyra/ ``` - 标注“待新增”的文件只是职责分布示例,随实现按需新增,不预建空文件或空目录;`internal/` 各目录均以 `AGENTS.md` 记录职责与依赖约束。 -- model 下的 `link*.go` 展示一个资源在各层的文件分布;分类、设置等资源按同样方式在 api/handler/service/model 新增对应文件。具体迁移文件名随首个迁移确定。 +- model 下的 `link*.go` 展示一个资源在各层的文件分布;分类、设置等资源按同样方式在 api/handler/service/model 新增对应文件。首个迁移已落地为 `migrations/00001_init.sql`。 - `handler/polaris/`、`service/polaris/` 仅示意业务分包的形态:实际分包与否随业务边界明确时决定,路由、api、model 不照搬同样的分包。 ## 组织原则 @@ -92,6 +93,6 @@ lyra/ - 配置校验、环境变量覆盖与默认值:尚未实现(当前无任何校验、覆盖或默认值)。 - `ErrorInfo.Debug` 的填充与按环境(dev)开关:尚未实现,目前仅有字段。 - 分页响应约定(如 data/meta 结构)与统一错误处理中间件:尚未引入,仅为历史讨论。 -- Goose 的部署执行入口与并发控制(串行执行或数据库锁)方案。 +- Goose 迁移的部署执行入口已确定为手工 CLI(见 [../migrations/README.md](../migrations/README.md)),并发控制以“单操作者串行执行”为约定,数据库锁未引入,服务启动不自动迁移。 - PostgreSQL 按项目划分 schema 的做法(组织边界,不等于权限隔离)。 - 日志方案(候选标准库 slog)与中间件集成方式。 diff --git a/go/projects/lyra/docs/demo.sql b/go/projects/lyra/docs/demo.sql index 0f2aa58..91cf9b7 100644 --- a/go/projects/lyra/docs/demo.sql +++ b/go/projects/lyra/docs/demo.sql @@ -71,16 +71,12 @@ CREATE TABLE public.links ( 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_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) + 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 '链接表,归属分类'; diff --git a/go/projects/lyra/docs/goose.md b/go/projects/lyra/docs/goose.md new file mode 100644 index 0000000..16f66c5 --- /dev/null +++ b/go/projects/lyra/docs/goose.md @@ -0,0 +1,179 @@ +# Goose 迁移操作手册 + +Lyra 数据库迁移的**唯一完整操作手册**。`migrations/` 目录是数据库结构的 +唯一正式执行来源;[demo.sql](demo.sql) 仅保留作参考,不要再手工执行它。 +迁移目录本身的说明见 [migrations/README.md](../migrations/README.md)。 + +## 目录与前提 + +- 所有 goose 命令都在 Lyra module 根目录(`migrations/` 的上一级)执行: + + ```bash + cd go/projects/lyra # 从仓库根出发 + ``` + +- 目标数据库**已创建**:迁移不负责建库,`lab` 库需先存在。 +- 首次迁移要求目标对象**尚不存在**:如果之前手工执行过 demo.sql 建过 + 同名对象,`up` 会报 `already exists`,先按故障表评估处理。 + +## 安装 goose CLI(固定 v3.29.0) + +goose 只作为独立命令行工具使用(Go 应用尚未引入 goose 依赖): + +```bash +go install -tags='no_clickhouse no_libsql no_mssql no_mysql no_sqlite3 no_vertica no_ydb' github.com/pressly/goose/v3/cmd/goose@v3.29.0 +goose -version # 应输出 v3.29.0 +``` + +- 固定 `@v3.29.0`,保证各机器行为一致。 +- `-tags=...` 是可选的 PostgreSQL 专用精简构建(`no_*` 标签已在 goose + v3.29.0 源码中核实);不追求精简可省略。 +- 二进制装到 `$(go env GOPATH)/bin`(设了 `GOBIN` 则装到 `GOBIN`)。 + `goose -version` 提示找不到命令时,把该目录加入 `PATH`: + + ```bash + export PATH="$PATH:$(go env GOPATH)/bin" # 可写入 ~/.bashrc + ``` + +## 配置连接:.env.local + +goose **不读取**应用的 `etc/lyra.toml`,也不会只凭 `PG_PASSWORD` 之类的 +应用环境变量自动拼出连接串——它只认自己环境变量里的 `GOOSE_DBSTRING`。 +在 module 根创建 `.env.local` 并追加以下内容(**全部是占位值**,换成实际 +信息;`lab` 库需已创建): + +```bash +GOOSE_DRIVER=postgres +GOOSE_MIGRATION_DIR=./migrations +GOOSE_DBSTRING="postgres://example_user:URL_ENCODED_PASSWORD@127.0.0.1:5432/lab?sslmode=disable" +``` + +要点: + +- `GOOSE_DRIVER=postgres` 是 **goose 的驱动名**;Go 应用内用的是 pgx + (驱动名 `pgx`),两边名字不同但都连 PostgreSQL,不要混填。 +- 本仓库 `.gitignore` 已忽略 `.env*`,`.env.local` 不会提交;建议再执行 + `chmod 600 .env.local` 收紧文件权限。 +- `127.0.0.1` 与端口必须换成**实际可达**的地址和端口(远程/容器按实际 + 映射填写)。 +- `sslmode=disable` 仅适用于本地确认无 TLS 要求的场景;远程环境按部署的 + TLS 要求调整该参数。 +- 本手册所有 goose 命令统一用 `-env=.env.local` 显式加载该文件:goose + 默认读的是 `.env`,`-env=.env.local` 既明确来源、又避免误用其他本地 + 配置。连接信息配置一次即可,无需每次交互输入密码。 +- 系统环境里已有的同名变量(如曾 `export` 过 `GOOSE_DBSTRING`)可能优先 + 于 `.env.local` 生效:改了 `.env.local` 仍行为异常时,先查是否存在同名 + 环境变量——只列变量名、不打印值,避免泄露连接串: + + ```bash + env | cut -d= -f1 | grep -i '^GOOSE' + ``` + +## 连接串特殊字符:URL 编码 + +`GOOSE_DBSTRING` 是 URL 格式:**用户名、密码**中的特殊字符必须逐个做 +百分号编码,否则主机名/路径解析会错位。 + +- 对用户名和密码分别编码,**不要把整串 URL 一起编码**,也不要为迁就 DSN + 去修改数据库里的实际密码。避免对已经编码的内容重复编码。 +- 不要把真实密码交给在线网站转换;按下表手工替换即可。 + +| 字符 | 编码 | 字符 | 编码 | +|---|---|---|---| +| `@` | `%40` | `:` | `%3A` | +| `#` | `%23` | `/` | `%2F` | +| `%` | `%25` | `?` | `%3F` | + +示例:密码 `demo#pass`(演示值)在 DSN 中写成 `demo%23pass`。 + +## 常用命令 + +```bash +# 离线检查迁移文件能否被 goose 解析(注解与语句拆分); +# 不连接数据库,也不验证 SQL 在 PostgreSQL 上的语义 +goose -env=.env.local validate + +# goose 工具自身版本(与数据库里的迁移版本是两回事) +goose -version + +# 连接数据库:查看迁移状态 / 当前版本 +goose -env=.env.local status +goose -env=.env.local version + +# 执行迁移(连接数据库) +goose -env=.env.local up +``` + +注意:v3.29.0 的 `validate` **不接受位置路径参数**,迁移目录只由 +`GOOSE_MIGRATION_DIR`(`.env.local` 已设 `./migrations`)或 `-dir` 标志 +决定。`validate` 通过只证明文件能解析,不证明连接正常或 SQL 语义正确。 + +## 首次初始化 + +前提:`lab` 库已创建,且 00001 中的对象(函数、三张表、索引、触发器) +都不存在——**不要**先手工执行 demo.sql 再 `up`。 + +```bash +goose -env=.env.local validate # 离线确认文件可解析 +goose -env=.env.local status # 显示 00001 Pending:连接成功,该迁移尚未应用 +goose -env=.env.local up # 执行 00001,并在 goose_db_version 中记录版本: + # set_updated_at() 函数、users/categories/links 三张表、 + # 约束、注释、索引与触发器 +goose -env=.env.local status # 00001 显示应用时间 +goose -env=.env.local version # 显示 version 1 +``` + +- `up` 可重复执行:已应用版本按 `goose_db_version` 自动跳过。 +- **已执行过的迁移文件不再修改**;要变更结构就新增迁移文件(见下节)。 + +## 日常新增迁移 + +```bash +goose -env=.env.local -s create add_example_column sql +``` + +`-s` 表示顺序编号:在现有 00001 基础上生成 +`migrations/00002_add_example_column.sql`(实际编号按目录中现有最大版本 +延续)。例如给 users 表加一列,编辑生成的文件: + +```sql +-- +goose Up +ALTER TABLE public.users + ADD COLUMN avatar_url text NOT NULL DEFAULT ''; + +-- +goose Down +ALTER TABLE public.users + DROP COLUMN avatar_url; +``` + +然后按顺序执行: + +```bash +goose -env=.env.local validate +goose -env=.env.local status +goose -env=.env.local up +goose -env=.env.local status +``` + +Up/Down 成对编写,保证迁移可回滚。 + +## 回滚警告 + +```bash +goose -env=.env.local down # ⚠ 危险操作,见下 +``` + +- `down` 只回滚**最近一个**已应用迁移的 Down 部分,**不是**清库或重建的 + 通用命令。 +- **回滚 00001 会删除 `links`/`categories`/`users` 三张表及表中全部数据。** +- 执行任何 `down` 之前:阅读对应迁移 SQL、评估数据损失、备份数据库。 + 不建议随手“试一下” `down`,也不要把它列入日常维护流程。 + +## 故障速查 + +| 报错特征 | 常见原因与处理 | +|---|---| +| `hostname resolving error`(主机名里混入非法字符) | DBSTRING 格式或 URL 编码不对:检查用户名/密码特殊字符是否已按上表编码、`://` 与 `@` `:` 分隔符是否完整 | +| `password authentication failed` | 用户名或密码不对:核对 `.env.local`(编码后)的值;再排查系统环境同名变量是否优先生效(只查变量名,不打印值) | +| `connection refused` / `i/o timeout` | 主机端口不可达:确认 PostgreSQL 实际监听地址、端口、容器端口映射与防火墙 | +| `already exists`(对象已存在) | 目标对象曾被手工创建(如执行过 demo.sql):先评估现有对象与迁移的差异再决定处理方式;**不要直接删数据**,也不要手工往 `goose_db_version` 插版本记录伪造状态 | diff --git a/go/projects/lyra/docs/plans/2026-10-17-goose-initial-migration.md b/go/projects/lyra/docs/plans/2026-10-17-goose-initial-migration.md new file mode 100644 index 0000000..11816c0 --- /dev/null +++ b/go/projects/lyra/docs/plans/2026-10-17-goose-initial-migration.md @@ -0,0 +1,71 @@ +# Goose 初始迁移 + +日期:2026-10-17 + +## 目标与范围 + +用户已同意创建首个 Goose 迁移并提供后续自行管理的说明。以当前工作区的 +`docs/demo.sql` 为来源,保留已有未提交改动,不修改其表结构设计。 +本次不连接或修改用户数据库,不把迁移嵌入服务启动,也不修改 Go 应用依赖。 + +## 实施步骤 + +1. 核对 Goose 官方 SQL 注解、事务行为及 CLI 命令。 +2. 新建 `migrations/00001_init.sql`,Up 保留示例中的函数、三张表、约束、 + 注释、索引与触发器;去掉外层事务,以 StatementBegin/StatementEnd + 包裹 PL/pgSQL 函数。Down 按依赖逆序删除表,最后删除函数,不使用 CASCADE。 +3. 更新 `migrations/README.md`,说明 CLI 使用、连接信息的安全提供、首次 + 初始化、状态检查、后续迁移及回滚风险。采用固定 CLI 版本 v3.29.0,命令在 Lyra + module 根执行;数据库必须已创建,首个迁移要求目标对象尚不存在。 +4. 同步项目 README 与架构文档的迁移进度。`docs/demo.sql` 保留为参考, + 正式迁移以 migrations 为准,避免双重执行。 + +## 工作分工与验证 + +- librarian:只读核对官方规则与 CLI 能力。 +- fixer:仅负责首个 SQL 迁移及上述三个说明文档;不修改其他代码或配置, + 不运行任何连接数据库的命令。 +- 父会话:维护计划、检查最终 diff 与文案,确认无意外改动及敏感信息。 +- fixer 验证:静态对比 Up 与当前 demo 的 SQL 内容,检查 Down 依赖顺序; + 使用 v3.29.0 的 `goose -env=none -dir ./migrations validate` 检查文件解析, + 此命令不连接数据库。校验用工具仅构建到 `/tmp/opencode`,不修改应用依赖。 +- 不为仅 SQL/文档变更重复运行无关 Go HTTP 测试。不宣称数据库集成验证通过; + 未在 PostgreSQL 实际执行的限制在交付时明确说明。 + +## 已核对的规则 + +- 官方 v3.29.0:Up/Down 默认使用事务;函数体需 StatementBegin/StatementEnd。 +- 后续新建迁移使用 `goose -s create <名称> sql`,延续顺序编号。 +- CLI 默认读取 `.env`;说明中的命令统一加 `-env=none`,显式使用环境变量, + 避免误用本地配置。`-version` 为工具版本,`version` 为数据库版本。 +- 官方来源:https://pressly.github.io/goose/documentation/annotations/ + 与 https://github.com/pressly/goose/releases/tag/v3.29.0。 + +## 实施与验收记录 + +- 已完成初始迁移与使用文档;父会话已检查 SQL 与相关文档差异。 +- 实测修正:v3.29.0 的 validate 不使用位置参数,必须通过 `-dir` 或 + `GOOSE_MIGRATION_DIR` 选择目录;已按实际行为编写说明。 +- fixer 已构建 v3.29.0 PostgreSQL 专用 CLI 至 `/tmp/opencode/lyra-goose-bin`, + 离线解析得到 Up=40、Down=4;与 demo 的 Up 内容逐行对比完全一致。 +- 后续编号 00002 已在临时目录验证;没有修改应用依赖或连接数据库。 +- 最终文案校正:目标库为空是执行前提,不当作已验证事实;普通 down 是否 + 丢数据取决于迁移内容,00001 的 Down 则确定会删除三张表及其数据。 +- `git diff --check` 通过。PostgreSQL 上的实际执行尚未验证。 + +## 后续:整理日常操作手册 + +用户已要求将实际操作步骤写入 Lyra docs。以下为新增文档任务,不改已有迁移。 + +1. 新建 `docs/goose.md`,作为唯一完整操作手册:固定版本安装、项目根目录、 + `.env.local` 配置、密码 URL 编码、validate/status/up/version、后续新增迁移、 + 回滚警告与常见故障。所有数据库操作命令使用 `-env=.env.local`。 +2. 将 `migrations/README.md` 收敛为迁移目录说明和手册链接,移除旧的交互输入 + 密码流程,避免两份操作步骤不一致;项目 README 增加手册入口。 +3. 仅使用演示占位值,不读取 .env.local、真实连接配置或既有错误中的疑似凭据。 + 不运行服务、不连接数据库、不更改 SQL、依赖和配置,也不暂存用户改动。 +4. fixer 负责上述三个文档和静态链接/命令一致性检查;父会话负责最终差异与 + 文案审查。已有 CLI 验证证据复用,不为文档改动重复构建或执行数据库命令。 + +说明:用户已确认 status 成功并显示 00001 Pending;这是连接及迁移发现成功的 +证据,不足以宣称 Up 或 Down 已完成数据库集成验证。 diff --git a/go/projects/lyra/docs/plans/2026-10-17-postgres-connection-check.md b/go/projects/lyra/docs/plans/2026-10-17-postgres-connection-check.md new file mode 100644 index 0000000..51ea3b0 --- /dev/null +++ b/go/projects/lyra/docs/plans/2026-10-17-postgres-connection-check.md @@ -0,0 +1,32 @@ +# PostgreSQL 启动连接检查 + +日期:2026-10-17 + +## 已批准目标 + +用户要求在 main.go 中通过 sqlx 查询数据库信息,检查尚无业务表的数据库连接。 +查询只读取 current_database()、current_user、version(),不依赖业务表,不执行 DDL。 + +## 实施范围 + +1. 保留当前工作区所有用户改动,仅在 cmd/lyra/main.go 中注册项目已选定的 + pgx stdlib 驱动,使用带超时的连接及 GetContext 查询,输出数据库信息。 +2. 通过 go.mod/go.sum 加入 pgx/v5/stdlib 所需依赖;etc/lyra.toml 仅将 + driver 的 postgres 改为 pgx,与驱动注册名保持一致,不读出或改动敏感配置。 +3. 补充数据库连接关闭;不重构配置加载、不实现 dotenv、不涉及 Goose 或业务表。 + +## 分工与证据路径 + +- fixer:写入范围限定 main.go、go.mod、go.sum、TOML 的 driver 一行; + 首次修改检查缩进和换行,保留已有修改。负责 gofmt、go test ./... 与构建验证。 +- 父会话:检查新增查询、超时、驱动名与范围,检查无明文凭据及意外文件改动。 +- 不打印配置文件完整内容或完整 DSN;不读取 .env.local 或 compose 中的敏感值。 +- 不运行服务、不连接真实数据库;编译/测试只证明查询代码可编译和现有测试通过, + 实际连接是否成功由用户本地启动后观察查询输出确认。 + +## 验收 + +- 已加入 pgx/v5 v5.11.0 驱动、五秒超时连接与系统信息查询,保留 HTTP 通路。 +- fixer 执行 `go build ./...`、`go test ./...` 均通过,未连接真实数据库。 +- 最终安全校正:连接与查询失败时只记录固定的阶段提示,不直接输出驱动原始 + 错误,避免连接串解析错误意外携带敏感配置。 diff --git a/go/projects/lyra/etc/lyra.toml b/go/projects/lyra/etc/lyra.toml index 03132a7..0e71a8d 100644 --- a/go/projects/lyra/etc/lyra.toml +++ b/go/projects/lyra/etc/lyra.toml @@ -5,3 +5,14 @@ env = "debug" #gin支持 release | test | debug 3种mode,如果需要自定义 [http] host = "0.0.0.0" port = 7777 + +[database] +driver = "pgx" +host = "localhost" +port = 5432 +user = "admin" +name = "lab" + +[redis] +host = "localhost" +port = 6379 \ No newline at end of file diff --git a/go/projects/lyra/go.mod b/go/projects/lyra/go.mod index d978a55..8374625 100644 --- a/go/projects/lyra/go.mod +++ b/go/projects/lyra/go.mod @@ -4,6 +4,8 @@ go 1.27.1 require ( github.com/gin-gonic/gin v1.12.0 + github.com/jackc/pgx/v5 v5.11.0 + github.com/jmoiron/sqlx v1.4.0 github.com/spf13/viper v1.21.0 ) @@ -21,6 +23,10 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -43,6 +49,7 @@ require ( golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.10 // indirect diff --git a/go/projects/lyra/go.sum b/go/projects/lyra/go.sum index bb5c33f..edc3b30 100644 --- a/go/projects/lyra/go.sum +++ b/go/projects/lyra/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= @@ -27,6 +29,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -36,6 +40,18 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -46,8 +62,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -80,6 +100,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -104,6 +125,8 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/go/projects/lyra/internal/config/config.go b/go/projects/lyra/internal/config/config.go index c074a1a..5f199e5 100644 --- a/go/projects/lyra/internal/config/config.go +++ b/go/projects/lyra/internal/config/config.go @@ -7,8 +7,10 @@ import ( ) type Config struct { - App AppConfig `mapstructure:"app"` - HTTP HTTPConfig `mapstructure:"http"` + App AppConfig `mapstructure:"app"` + HTTP HTTPConfig `mapstructure:"http"` + Database DatabaseConfig `mapstructure:"database"` + Redis RedisConfig `mapstructure:"redis"` } type AppConfig struct { @@ -21,6 +23,20 @@ type HTTPConfig struct { Port int `mapstructure:"port"` } +type DatabaseConfig struct { + Driver string `mapstructure:"driver"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Name string `mapstructure:"name"` + Password string `mapstructure:"password"` +} + +type RedisConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` +} + func Load(path string) (*Config, error) { v := viper.New() // 指定文件,按 .toml 扩展名识别格式 @@ -29,7 +45,10 @@ func Load(path string) (*Config, error) { if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("读取配置失败: %w", err) } - + v.AllowEmptyEnv(true) + if err := v.BindEnv("database.password", "PG_PASSWORD"); err != nil { + return nil, fmt.Errorf("绑定数据库密码环境变量失败: %w", err) + } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("解析配置失败: %w", err) diff --git a/go/projects/lyra/migrations/00001_init.sql b/go/projects/lyra/migrations/00001_init.sql new file mode 100644 index 0000000..924b1f0 --- /dev/null +++ b/go/projects/lyra/migrations/00001_init.sql @@ -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(); diff --git a/go/projects/lyra/migrations/README.md b/go/projects/lyra/migrations/README.md index 0f37267..80108c5 100644 --- a/go/projects/lyra/migrations/README.md +++ b/go/projects/lyra/migrations/README.md @@ -1,7 +1,21 @@ # migrations -未来存放 [Goose](https://pressly.github.io/goose/) SQL 迁移文件的目录。 +Lyra 的 [Goose](https://pressly.github.io/goose/) SQL 迁移目录,是数据库结构的**唯一正式执行来源**。 +[docs/demo.sql](../docs/demo.sql) 仅保留作参考,不要再手工执行它,避免与迁移重复建表。 -当前没有任何迁移文件;首个迁移的文件名与写法随实现确定。部署执行入口、 -并发控制(串行执行或数据库锁)与 schema 划分方案尚未确定,见 -[../docs/architecture.md](../docs/architecture.md) 的"尚未确定"部分。 +## 操作手册 + +安装 goose CLI、配置 `.env.local` 连接、首次初始化、日常新增迁移、回滚警告与 +故障排查等全部操作步骤,见唯一完整手册:[docs/goose.md](../docs/goose.md)。 + +## 现有迁移 + +| 文件 | 内容 | +|---|---| +| `00001_init.sql` | 初始结构:`set_updated_at()` 函数、`users`/`categories`/`links` 三张表、约束、注释、索引与触发器,建库内容与 `docs/demo.sql` 一致 | + +## 执行模型约定 + +- 迁移只作用于 `public` schema,沿用现有设计,暂不做 schema 划分。 +- 单个操作者串行执行,不引入数据库锁等并发控制。 +- 部署执行入口是手工 CLI,服务启动时**不会**自动执行迁移。