Files
labmem ab83ad96b4 feat(lyra)!: 接入分类查询并集中组装依赖
- 使用 Squirrel 与 sqlx 实现分类查询
- 集中组装依赖并向 handler 注入 model 接口
- 显式转换分类响应 DTO,统一空列表输出
- 补充路由与 handler 测试,同步架构文档

BREAKING CHANGE: 将 GET /hello 替换为 GET /test,响应由问候字符串改为分类列表
2026-10-17 17:06:20 +08:00

106 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lyra 目标结构
日期:2026-10-17
本文记录已确认的目标目录结构与职责边界,并与当前真实实现同步。目录树是**目标结构**,并非全部已存在:标注“待新增”的文件尚未创建;标注“占位”的目录(`internal/postgres`、`internal/service`)仅含 `AGENTS.md`(记录职责与依赖约束),暂不构成 Go 包。当前已有 Go 代码的是 `cmd/lyra` 与 `internal/{api,config,handler,httpx,model,router}`。仓库内另有本地 `.env.local`(已被 gitignore、不入库)等文件,不属于目标结构,不在此展开。选型与讨论过程见 [plans/2026-10-16-lyra-architecture.md](plans/2026-10-16-lyra-architecture.md);handler/service 业务分包的调整记录见 [plans/2026-10-17-business-package-layout.md](plans/2026-10-17-business-package-layout.md)。
## 已确认技术栈
- 已引入并使用:Gin v1.12.0(HTTP 路由)、Viper v1.21.0(配置读取,TOML)、godotenv v1.5.1(启动时加载本地 `.env.local`)、sqlx v1.4.0 + Squirrel v1.5.4(查询构造与执行)、pgx v5.11.0 stdlib(PostgreSQL 驱动,由 sqlx 以 "pgx" 驱动名使用)。`go.sum` 已由 Go 工具生成。
- Goose:迁移 SQL 已就位,以独立 goose CLI(v3.29.0)执行,Go 依赖尚未引入。
- PostgreSQL 已在启动时连接;连接代码当前在入口 `main`,收拢到 `internal/postgres` 是目标结构。
## 当前进度
- 程序入口在 module 根目录以 `go run ./cmd/lyra` 启动,流程为:`godotenv.Load` 读取本地 `.env.local`(不覆盖已存在的进程环境变量)→ `config.Load` 读取相对当前工作目录的 `etc/lyra.toml`(Viper,`mapstructure` 标签)→ `gin.SetMode` → 以 `gin.Default()` 创建引擎并 `SetTrustedProxies(nil)` 关闭代理信任 → `sqlx.ConnectContext` 建立 PostgreSQL 连接(失败即退出,`defer db.Close()`)→ `buildDependencies`/`buildHandlers` 组装依赖 → `RegisterRoutes` → 监听配置的地址与端口。`.env.local` 与 `etc/lyra.toml` 均按当前工作目录解析,换目录执行会找不到。连接与查询当前未设置超时。
- 已注册 `GET /test`:`internal/router/router.go` 的 `RegisterRoutes` 将其指向入口注入的 `router.Handlers.TestHandler`(`gin.HandlerFunc`,由 `handler.NewTestHandler(...).Hello()` 构造)。处理器调用 `model.CategoryModel.FindAll` 查询 `public.categories`——当前固定 `sort >= 1`、按 `sort`/`id` 排序、`limit 10`——经显式转换为本轮新增的 `api.CategoryResponse` 后由 `httpx.OK` 输出;查询出错经 `httpx.Fail` 返回 500 与 `api.CodeInternalError`。该接口用于验证 handler→model→数据库链路,不是完整业务 API。
- `internal/model` 已落地:`Category` 结构(id/slug/title/sort)、`CategoryModel` 接口与 `defaultCategoryModel` 实现——Squirrel 构造 SQL(Dollar 占位符),sqlx `SelectContext` 执行与映射。
- 已定义公共响应契约:`api.Response`(success/data/error)、`api.ErrorInfo`(code/message/debug)与错误码类型 `api.ErrorCode` 及其默认文案 `Message()`;响应输出辅助 `httpx.OK`、`httpx.Fail` 已实现并被 handler 使用。
- 配置:`database.password` 绑定了环境变量 `PG_PASSWORD`(可经 `.env.local` 或进程环境提供),其余配置项暂不支持环境变量覆盖;没有校验与默认值。`Redis` 配置项已定义但暂无消费方。
- 测试:路由注册有 httptest 回归测试,覆盖 `GET /test` 注入替身处理器的调用,以及未知路径/不支持方法返回 404;handler 有基于假 `CategoryModel` 的替身测试,覆盖 DTO 转换、空结果输出 `data:[]`、模型出错返回 500 且不泄漏内部错误信息、请求 context 透传。两类测试均不监听实际端口、不连接数据库,因此不证明真实 SQL 可用——连接真实数据库的端到端验证本轮未做。
- 数据库迁移已就位:`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)。
- 尚未实现:业务 service 层与完整业务 API;连接收拢到 `internal/postgres`(当前连接代码在入口 `main`);Goose Go 依赖;配置校验与默认值;`ErrorInfo.Debug` 的填充与按环境开关;连接/查询超时。日志方案未定。
## 目录结构
```text
lyra/
├── cmd/lyra/
│ ├── main.go # 启动流程:加载 .env.local 与配置、连接数据库、组装依赖
│ └── dependency.go # Dependencies 与 handler 组装
├── internal/
│ ├── api/ # HTTP 请求/响应 DTO、错误码与响应契约(平铺,按资源分文件)
│ │ ├── response.go # Response/ErrorInfo 结构
│ │ ├── code.go # ErrorCode 与错误码常量、默认文案
│ │ ├── category.go # CategoryResponse 分类响应 DTO
│ │ ├── links.go # links 资源 DTO 示例(待新增)
│ │ └── settings.go # 待新增
│ ├── config/
│ │ └── config.go # 配置定义与加载
│ ├── postgres/ # 数据库连接的目标位置(占位:仅 AGENTS.md,连接暂在 cmd/lyra/main.go)
│ ├── router/
│ │ ├── router.go # 路由注册、项目路径前缀
│ │ └── router_test.go # 路由 httptest 回归测试
│ ├── handler/
│ │ ├── test.go # GET /test 处理:调用 model、显式 DTO 转换、响应输出
│ │ ├── test_test.go # handler 替身测试
│ │ ├── links.go # 参数绑定、DTO 转换、响应输出(待新增)
│ │ └── polaris/ # 按明确业务分包的示例(待新增,不预建空包)
│ ├── service/ # 业务规则、事务协调(占位:仅 AGENTS.md)
│ │ ├── links.go # 业务规则、事务协调(待新增)
│ │ ├── links_test.go # 测试与源文件同目录(待新增)
│ │ └── polaris/ # 按明确业务分包的示例(待新增,不预建空包)
│ ├── model/
│ │ ├── category.go # Category 结构、CategoryModel 接口与 Squirrel+sqlx 实现
│ │ ├── link.go # Link 数据结构定义(待新增)
│ │ ├── link_model.go # 数据访问对象:CRUD 与 SQL(待新增)
│ │ └── link_model_test.go # 查询测试(待新增)
│ └── httpx/
│ └── response.go # 公共响应输出辅助(OK/Fail)
├── etc/
│ └── lyra.toml # 运行配置(App/HTTP/Database/Redis)
├── migrations/ # Goose SQL 迁移文件(00001_init.sql 初始迁移 + README)
├── docs/
│ ├── plans/
│ └── architecture.md # 本文
├── README.md
├── go.mod
└── go.sum # 已生成
```
- 标注“待新增”的文件只是职责分布示例,随实现按需新增,不预建空文件或空目录;`internal/` 各目录均以 `AGENTS.md` 记录职责与依赖约束(树中未逐一画出)。`internal/postgres`、`internal/service` 目前仅有 `AGENTS.md`,为占位目录,不构成 Go 包。
- model 下的 `link*.go` 展示一个资源在各层的文件分布;分类资源已按此方式在 api/model 落地(`api/category.go`、`model/category.go`,handler 为链路验证用的 `test.go`),设置等资源按同样方式新增。首个迁移已落地为 `migrations/00001_init.sql`。
- `handler/polaris/`、`service/polaris/` 仅示意业务分包的形态:实际分包与否随业务边界明确时决定,路由、api、model 不照搬同样的分包。
## 组织原则
- handler、service 可以按明确业务分包(如 `polaris/`),仅在业务边界清晰时新增;服务级简单接口(如 `hello`)保留在各自根包内平铺,不强制分包。
- router、api、model 等其他层暂时平铺,不按业务项目建代码子目录;项目通过 `/api/polaris` 等路由前缀区分,包内文件按资源划分。
- 公共基础能力全局唯一:配置在 internal/config,连接在 internal/postgres,HTTP 辅助在 internal/httpx,不随业务项目复制或按项目再拆。
- 不保留 platform/modules 分类层;简单接口不强制走完整三层,也不按每张表机械复制层级。
- 不预建空业务包,分包随实际业务代码一起落地。
## 层间边界
- 真实业务的主调用链为 handler→service→model→PostgreSQL;简单接口可以不经过 service——当前 `GET /test` 即由 handler 直接调用 model(service 层尚未实现)。service 负责业务规则及需要的事务协调,model 支持同一事务内的数据库操作,不将跨多次写入各自独立提交当成原子业务。
- 请求/响应 DTO 集中在 internal/api,与内部模型分开;即使初期字段相近,也不默认把数据库模型直接暴露为响应,接受必要的显式转换代码(分类列表已经 handler 的 `toCategoryResponses` 转为 `api.CategoryResponse`)。
- handler 引用 API DTO 完成绑定与转换;service/model 不依赖 Gin,也不依赖 internal/api DTO;api 包不反向引用 handler/service/model,避免传输契约与实现耦合或循环引用。
- internal/api 拥有响应契约与业务错误码(`Response`/`ErrorInfo`/`ErrorCode`);不依赖 Gin。
- internal/httpx 负责响应输出:`OK` 固定 HTTP 200;`Fail` 接收 HTTP 状态码与业务错误码,`message` 为空时回退错误码默认文案。HTTP 状态码表示传输/结果类别,`ErrorCode` 表示自定义业务错误,两者分开。
- internal/router 负责把 handler 挂到路由上:当前注册 `GET /test`,指向入口组装注入的 `Handlers.TestHandler`,后续按业务项目路径前缀(如 `/api/polaris`)扩展。
- 配置由入口 `main` 加载后组装并传递:`internal/config` 只负责配置定义与加载,运行配置来自 `etc/lyra.toml`;当前 `app.env` 直接传给 `gin.SetMode`(仅 `debug`/`release`/`test`),HTTP 地址端口供启动使用,数据库相关配置由入口 `main` 组装 DSN 并用于连接(`database.password` 可来自 `PG_PASSWORD`),`Redis` 配置暂无消费方。
- model 承载数据结构定义与数据访问:Squirrel/SQL 构造、sqlx 执行与结果映射,驱动为 pgx stdlib(已按此落地 `internal/model/category.go`);不是 ORM 自动持久化,也不要求每种类型机械建立一套结构。
- service/model 使用标准 context.Context,不接收 gin.Context。
## 尚未确定 / 待实现
以下均为计划中的推荐或待确认项,尚未实现,也未定为已决:
- 配置校验与默认值:尚未实现。环境变量覆盖当前仅 `database.password` 绑定 `PG_PASSWORD`,其余配置项不支持。
- 连接与查询超时:尚未设置,也未确定超时策略。
- `ErrorInfo.Debug` 的填充与按环境(dev)开关:尚未实现,目前仅有字段。
- 分页响应约定(如 data/meta 结构)与统一错误处理中间件:尚未引入,仅为历史讨论。
- Goose 迁移的部署执行入口已确定为手工 CLI(见 [../migrations/README.md](../migrations/README.md)),并发控制以“单操作者串行执行”为约定,数据库锁未引入,服务启动不自动迁移。
- PostgreSQL 按项目划分 schema 的做法(组织边界,不等于权限隔离)。
- 日志方案(候选标准库 slog)与中间件集成方式。