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

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

12 KiB
Raw Blame History

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;handler/service 业务分包的调整记录见 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 一致),由手工 goose CLI(v3.29.0)执行,见 ../migrations/README.md。
  • 尚未实现:业务 service 层与完整业务 API;连接收拢到 internal/postgres(当前连接代码在入口 main);Goose Go 依赖;配置校验与默认值;ErrorInfo.Debug 的填充与按环境开关;连接/查询超时。日志方案未定。

目录结构

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),并发控制以“单操作者串行执行”为约定,数据库锁未引入,服务启动不自动迁移。
  • PostgreSQL 按项目划分 schema 的做法(组织边界,不等于权限隔离)。
  • 日志方案(候选标准库 slog)与中间件集成方式。