You've already forked agentic-coding-workflow
132 lines
4.9 KiB
Go
132 lines
4.9 KiB
Go
// Package app 是应用的装配根:把 config、logger、DB 池、各业务模块、HTTP
|
|
// 路由按依赖顺序拼起来,并暴露 Run 给入口程序。本包只做组合,不引入新业务。
|
|
//
|
|
// 启动顺序:
|
|
// 1. 用 config.Env 派生 logger(FromEnv 在生产环境默认开启 source)。
|
|
// 2. 跑 migrations(先 migrate 再开池,避免应用启动时表还没建好;migrate
|
|
// 失败直接返错由调用方记录、退出)。
|
|
// 3. 创建 *db.Pool(实际是 *pgxpool.Pool 别名),失败时不再继续。
|
|
// 4. 模块装配:audit recorder -> user repo+service(注入 audit)-> notify
|
|
// repo+dispatcher(注入 dummy notifier)-> notify handler(复用 userSvc
|
|
// 作为 SessionResolver,user.Service 接口本身满足该契约)。
|
|
// 5. 路由:httpx.NewRouter 返回 chi.Router 接口,直接挂 RequestID/Recover/
|
|
// Logger 三个全局中间件,再让模块 Mount 自己的子路由。
|
|
// 6. 可选 bootstrap:仅当配置里同时给了 admin 邮箱/密码时尝试。失败仅
|
|
// Warn 不阻塞启动。
|
|
// 7. 把所有依赖封装到 *App,由 Run 负责启动 http server 与优雅关停。
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
|
"github.com/yan1h/agent-coding-workflow/internal/config"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/db"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/logger"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
|
|
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
|
|
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
|
|
"github.com/yan1h/agent-coding-workflow/internal/user"
|
|
)
|
|
|
|
// App 持有运行期所有需要被关停的资源。字段保持非导出,避免外部直接操作。
|
|
type App struct {
|
|
cfg *config.Config
|
|
log *slog.Logger
|
|
pool *db.Pool
|
|
server *http.Server
|
|
}
|
|
|
|
// New 把所有模块按启动顺序拼装好。任何阶段失败都会返回错误,不留半成品资源。
|
|
func New(ctx context.Context, cfg *config.Config) (*App, error) {
|
|
log := logger.FromEnv(cfg.Env, nil)
|
|
|
|
// 跑 migrations 必须在开池前;否则首次启动连不存在的表会让 ping 失败。
|
|
if err := db.Migrate(cfg.DB.DSN, "file://migrations"); err != nil {
|
|
return nil, fmt.Errorf("migrate: %w", err)
|
|
}
|
|
|
|
pool, err := db.NewPool(ctx, cfg.DB.DSN)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
auditRec := audit.NewPostgresRecorder(pool)
|
|
|
|
userRepo := user.NewPostgresRepository(pool)
|
|
userSvc := user.NewService(userRepo, auditRec)
|
|
|
|
notifyRepo := notify.NewPostgresRepository(pool)
|
|
notifyDispatcher := notify.NewDispatcher(notifyRepo, log)
|
|
notifyDispatcher.Register(notify.NewDummyNotifier(log))
|
|
// 当前 dispatcher 仅暴露给本包:未来引入定时任务/后台 worker 才需把它
|
|
// 通过 App 字段透出去,目前保留局部变量以避免暴露未稳定 API。
|
|
_ = notifyDispatcher
|
|
|
|
r := httpx.NewRouter(httpx.RouterDeps{
|
|
HealthCheck: func(req *http.Request) error {
|
|
return db.HealthCheck(req.Context(), pool)
|
|
},
|
|
ReadyCheck: func(req *http.Request) error {
|
|
return db.HealthCheck(req.Context(), pool)
|
|
},
|
|
})
|
|
|
|
// chi.Router 接口本身已经声明 Use,无需类型断言到 *chi.Mux。
|
|
r.Use(middleware.RequestID)
|
|
r.Use(middleware.Recover(log))
|
|
r.Use(middleware.Logger(log))
|
|
|
|
// userSvc 直接满足 middleware.SessionResolver(ResolveSession 同名同签名)。
|
|
user.NewHandler(userSvc).Mount(r)
|
|
notify.NewHandler(notifyRepo, userSvc).Mount(r)
|
|
|
|
if cfg.Bootstrap.AdminEmail != "" && cfg.Bootstrap.AdminPassword != "" {
|
|
if u, err := userSvc.Bootstrap(ctx, cfg.Bootstrap.AdminEmail, cfg.Bootstrap.AdminPassword); err != nil {
|
|
log.Warn("bootstrap admin failed", "err", err.Error())
|
|
} else if u != nil {
|
|
log.Info("bootstrap admin created", "email", u.Email)
|
|
}
|
|
}
|
|
|
|
srv := &http.Server{
|
|
Addr: cfg.HTTP.Addr,
|
|
Handler: r,
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
return &App{cfg: cfg, log: log, pool: pool, server: srv}, nil
|
|
}
|
|
|
|
// Run 启动 HTTP server 并阻塞直到 ctx 取消(Shutdown)或服务器报错。
|
|
// 关停路径上保证关闭 db pool;返回 nil 表示由 ctx 取消触发的正常关停。
|
|
func (a *App) Run(ctx context.Context) error {
|
|
a.log.Info("server starting", "addr", a.cfg.HTTP.Addr, "env", a.cfg.Env)
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
if err := a.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
errCh <- err
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
a.log.Info("shutdown signal received, draining...")
|
|
shutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := a.server.Shutdown(shutCtx); err != nil {
|
|
a.log.Error("server shutdown", "err", err.Error())
|
|
}
|
|
a.pool.Close()
|
|
return nil
|
|
case err := <-errCh:
|
|
a.pool.Close()
|
|
return err
|
|
}
|
|
}
|