go-casstm --- # go-caatsm Refactor Plan ## Objective Refactor the project to adopt a modern, maintainable, and scalable architecture using: - Clean Architecture (app / domain / adapter / infra) - nats.go JetStream (replace Watermill) - PostgreSQL pgx (replace Hasura GraphQL) - Koanf configuration system (replace Viper) - Google Wire for dependency injection - Structured logging (+ optional metrics/tracing) Goal: improve reliability, performance, extensibility, and professional engineering quality. --- ## High-Level Architecture Refactor into the following structure: ```text /cmd/receiver/main.go # entrypoint using wire-generated injector /config/config.toml /internal /app # Orchestrates flows processor.go listener.go /domain telegram.go /adapter parser/ mapper/ /infra config/ # koanf loader nats/ # jetstream consumer/publisher postgres/ # pgx repository log/ # zap logger /pkg/di/wire.go # wire DI root ``` Principles: - Domain is pure Go types (no external imports). - App orchestrates: NATS msg → parser → domain → repository. - Infra handles external concerns (NATS, PostgreSQL, config, logging). - Adapter performs mapping between infra/domain. - `cmd` 只负责启动,不包含业务逻辑。 --- ## Phase 1 — Project Structure Migration **Goal:** Introduce new directories without breaking existing code. ### Tasks - Create new `/internal/app`, `/internal/domain`, `/internal/adapter`, `/internal/infra` directories. - Move domain-level structs (telegram, metadata) into `/internal/domain`. - Move parsing logic into `/internal/adapter/parser`. - Add `/pkg/di` for Wire. - Update `go.mod` and imports accordingly. ### Acceptance Criteria - Project builds successfully. - Existing behavior unchanged(只是结构调整,不改逻辑). --- ## Phase 2 — Replace Viper → Koanf **Goal:** Introduce reliable & explicit config loading. ### Tasks - Add Koanf loader at `/internal/infra/config/koanf.go`. - Load from file (`config/config.toml`) then environment (`CAATSM_` prefix). - Define a strongly typed `Config` struct (NATS, Postgres, logging, etc.). - Remove global singleton config; pass `*Config` explicitly via DI. - Add config validation logic (e.g. non-empty URLs, timeouts > 0). ### Example (参考实现思路) ```go func LoadConfig() (*Config, error) { k := koanf.New(".") if err := k.Load(file.Provider("config/config.toml"), toml.Parser()); err != nil { return nil, err } if err := k.Load(env.Provider("CAATSM_", ".", func(s string) string { return strings.ToLower(strings.TrimPrefix(s, "CAATSM_")) }), nil); err != nil { return nil, err } var cfg Config if err := k.Unmarshal("", &cfg); err != nil { return nil, err } return &cfg, cfg.Validate() } ``` ### Acceptance Criteria - Running `go run cmd/receiver/main.go` loads config via Koanf correctly。 - No global config singletons remain。 - Unit tests can construct `Config` directly,方便单测。 --- ## Phase 3 — Wire Dependency Injection **Goal:** Remove manual wiring logic, centralize dependency creation. ### Tasks - Create `/pkg/di/wire.go` with injectors. - Provide constructors: - `ProvideConfig` (Koanf) - `ProvideLogger` (Zap) - `ProvideJetStream` (NATS) - `ProvideDB` (pgxpool) - `ProvideRepository` (Postgres repo) - `NewMessageProcessor` (app layer) - Generate `wire_gen.go`. - Modify `cmd/receiver/main.go` to use Wire-generated `Initialize()` (或类似函数名)。 ### Example Wire skeleton ```go //go:build wireinject package di import ( "github.com/google/wire" "go-caatsm/internal/app" "go-caatsm/internal/infra/config" "go-caatsm/internal/infra/log" "go-caatsm/internal/infra/nats" "go-caatsm/internal/infra/postgres" ) func InitializeProcessor() (*app.MessageProcessor, error) { wire.Build( config.ProvideConfig, log.ProvideLogger, nats.ProvideJetStream, postgres.ProvideDB, postgres.ProvideRepository, app.NewMessageProcessor, ) return &app.MessageProcessor{}, nil } ``` ### Acceptance Criteria - Project builds with Wire DI。 - main.go 只负责调用 `InitializeProcessor()` 和启动 processor。 - 新增依赖时只需修改 Wire graph,不用手动改 main.go。 --- ## Phase 4 — Replace Watermill → nats.go JetStream **Goal:** Gain full control over message flow, retries, DLQ. ### Tasks - 引入 `/internal/infra/nats/jetstream.go`,实现: - 连接创建(`nats.Connect`,`js, _ := nc.JetStream()`) - Stream + Consumer 自动创建(如不存在则创建) - 使用 Pull Subscribe 模式(`PullSubscribe`) - 手动 ACK / NAK - 简单 Retry 策略(MaxDeliveries + NAK) - 死信队列(DLQ stream/subject) - 实现批量抓取(例如 `Fetch(50, MaxWait(...))`)。 - 实现 `Consumer.Start(ctx)`,内部循环读取消息并调用 `app.MessageProcessor.Handle()`。 ### Example 消费逻辑骨架 ```go func (c *Consumer) Start(ctx context.Context) error { sub, err := c.js.PullSubscribe(c.subject, c.consumerName) if err != nil { return err } for { select { case <-ctx.Done(): return ctx.Err() default: } msgs, err := sub.Fetch(50, nats.MaxWait(2*time.Second)) if err != nil { if errors.Is(err, nats.ErrTimeout) { continue } c.logger.Error("fetch failed", zap.Error(err)) continue } for _, msg := range msgs { if err := c.handler.Handle(ctx, msg.Data); err != nil { _ = msg.Nak() continue } _ = msg.Ack() } } } ``` ### Acceptance Criteria - 消费逻辑完全基于 nats.go,不再依赖 Watermill。 - ACK / NAK 正常工作,可通过 JetStream 管理界面/CLI 查看重试与 DLQ。 - 可通过配置控制批量大小、等待时间、MaxDeliveries 等。 --- ## Phase 5 — Replace Hasura GraphQL → PostgreSQL (pgx) **Goal:** High-performance & reliable write pipeline. ### Tasks - 添加 `/internal/infra/postgres/db.go`,使用 `pgxpool.Pool` 管理连接。 - 添加 `/internal/infra/postgres/repository.go`: - `InsertOne(ctx, telegram domain.Telegram) error` - `InsertBatch(ctx, []domain.Telegram) error`(使用 `CopyFrom`) - 定义 telegram 表结构(如已存在则对齐 struct 和列)。 - 增加必要索引(如 `uuid`、时间戳、业务 key 等)。 - 删除 Hasura GraphQL client、genqlient 相关代码。 ### Example CopyFrom 骨架 ```go func (r *Repository) InsertBatch(ctx context.Context, msgs []domain.Telegram) error { rows := make([][]any, len(msgs)) for i, m := range msgs { rows[i] = []any{ m.UUID, m.Raw, m.ParsedJSON, m.CreatedAt, } } _, err := r.pool.CopyFrom( ctx, pgx.Identifier{"aviation_telegrams"}, []string{"uuid", "raw", "parsed", "created_at"}, pgx.CopyFromRows(rows), ) return err } ``` ### Acceptance Criteria - 消息数据成功写入 PostgreSQL。 - 批量写入时使用 CopyFrom,性能明显优于单条 INSERT。 - Hasura / GraphQL 相关依赖从代码和 go.mod 中移除。 --- ## Phase 6 — Application Layer (Processor) **Goal:** Create clean orchestrator for the message lifecycle. ### Tasks - 在 `/internal/app/processor.go` 实现 `MessageProcessor`: - 接口定义: - `type Parser interface { Parse(raw []byte) (domain.Telegram, error) }` - `type Repository interface { InsertOne / InsertBatch }` - 核心流程: 1. 收到 NATS 消息(由 consumer 调用 `HandleMessage` 或类似接口) 2. 调用 `Parser.Parse` 得到 `domain.Telegram` 3. 调用 `Repository.Insert...` 写入数据库 4. 返回成功/失败,由 caller 决定 ACK/NAK - 在 `/internal/adapter/parser` 中处理具体报文解析逻辑,保持 domain 纯净。 ### Acceptance Criteria - Processor 不依赖具体的 NATS / pgx 类型,只依赖接口。 - Parser / Repository 可以在测试中替换为 mock。 - 业务流程清晰、单一职责。 --- ## Phase 7 — Logging & Observability **Goal:** Unify logging and enable production-ready debugging. ### Tasks - 在 `/internal/infra/log/logger.go` 实现 Zap 初始化(支持 dev/prod 模式)。 - 将 main、consumer、processor、repository 中的 `fmt.Println` 替换为结构化日志。 - 每条关键日志附加必要 context 字段: - `message_id` - `subject` - `stream` - `attempt` - (可选)添加 Prometheus metrics(处理量、错误数、重试次数)。 ### Acceptance Criteria - 日志输出统一,方便在 Loki / ELK 中检索。 - 出错时能通过日志快速定位是哪个环节(NATS 消费 / 解析 / DB 写入)出了问题。 --- ## Phase 8 — Remove Dead Code & Cleanup **Goal:** Remove legacy patterns and unused modules. ### Tasks - 移除 Watermill 相关代码与依赖。 - 移除 Hasura / genqlient 相关代码与依赖。 - 移除 Viper 配置加载器与全局单例。 - 删除不再使用的 handler / repository 实现。 - 运行 `go mod tidy` 清理依赖。 - 检查 Taskfile / Makefile,更新为新的启动、测试命令。 ### Acceptance Criteria - `go test ./...` 与 `go build ./...` 均成功。 - go.mod 中不再包含 Watermill / Hasura / genqlient / Viper。 - 代码中不再有全局 Config/Logger 单例。 --- ## Final Acceptance Criteria Refactor 完成的标志: 1. **启动链路:** - 使用 Koanf 加载配置。 - 使用 Wire 完成依赖注入。 - 使用 nats.go JetStream 消费消息。 - 使用 pgx 将数据写入 PostgreSQL。 2. **架构层次清晰:** - `internal/domain` 无外部依赖。 - `internal/app` 只依赖 domain + 抽象接口。 - `internal/infra` 只负责技术细节。 - `cmd` 只启动,不包含业务逻辑。 3. **旧技术栈完全移除:** - Watermill、Hasura、GraphQL、Viper、全局单例全部删除。 4. **数据流全链路可工作:** - NATS → Parser → Domain Model → Repository → PostgreSQL 全流程可验证。 ---