add initial application structure with configuration, logging, and health check endpoints

This commit is contained in:
kever
2026-01-14 23:58:00 +08:00
commit fed727e593
31 changed files with 770 additions and 0 deletions

25
internal/bootstrap/app.go Normal file
View File

@@ -0,0 +1,25 @@
package bootstrap
import (
"go.uber.org/fx"
"epic-ent/internal/config"
"epic-ent/internal/controller"
"epic-ent/internal/exception"
"epic-ent/internal/infra"
"epic-ent/internal/middleware"
"epic-ent/internal/repository"
"epic-ent/internal/service"
)
func NewApp() *fx.App {
return fx.New(
config.Module,
infra.Module,
repository.Module,
service.Module,
controller.Module,
middleware.Module,
exception.Module,
)
}

62
internal/config/config.go Normal file
View File

@@ -0,0 +1,62 @@
package config
import (
"fmt"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
MySQL MySQLConfig `mapstructure:"mysql"`
Redis RedisConfig `mapstructure:"redis"`
Log LogConfig `mapstructure:"log"`
Cron CronConfig `mapstructure:"cron"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
ReadTimeout string `mapstructure:"readTimeout"`
WriteTimeout string `mapstructure:"writeTimeout"`
}
type MySQLConfig struct {
DSN string `mapstructure:"dsn"`
}
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
}
type CronConfig struct {
Enabled bool `mapstructure:"enabled"`
Timezone string `mapstructure:"timezone"`
}
func Load() (*Config, error) {
v := viper.New()
v.SetConfigName("application")
v.SetConfigType("yaml")
v.AddConfigPath(".")
v.SetEnvPrefix("APP")
v.AutomaticEnv()
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
}
return &cfg, nil
}

View File

@@ -0,0 +1,7 @@
package config
import "go.uber.org/fx"
var Module = fx.Options(
fx.Provide(Load),
)

View File

@@ -0,0 +1,3 @@
package controller
//go:generate go run ../../cmd/gen/controllers

View File

@@ -0,0 +1,22 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
"epic-ent/internal/service"
)
type HealthController struct {
svc *service.HealthService
}
func NewHealthController(svc *service.HealthService) *HealthController {
return &HealthController{svc: svc}
}
func (h *HealthController) Health(c echo.Context) error {
status := h.svc.Check()
return c.JSON(http.StatusOK, status)
}

View File

@@ -0,0 +1,13 @@
// Code generated by cmd/gen/controllers; DO NOT EDIT.
package controller
import "go.uber.org/fx"
var Module = fx.Options(
fx.Provide(
NewHealthController,
),
fx.Invoke(
RegisterRoutes,
),
)

View File

@@ -0,0 +1,17 @@
package controller
import (
"github.com/labstack/echo/v4"
"go.uber.org/fx"
)
type RouteParams struct {
fx.In
Echo *echo.Echo
Health *HealthController
}
func RegisterRoutes(p RouteParams) {
p.Echo.GET("/health", p.Health.Health)
}

View File

@@ -0,0 +1,22 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
"epic-ent/internal/service"
)
type UserController struct {
svc *service.HealthService
}
func NewUserController(svc *service.HealthService) *HealthController {
return &HealthController{svc: svc}
}
func (h *UserController) Health(c echo.Context) error {
//status := h.svc.Check()
return c.JSON(http.StatusOK, "你好")
}

View File

@@ -0,0 +1,5 @@
package dto
type HealthRequest struct {
Ping string `json:"ping"`
}

View File

@@ -0,0 +1,5 @@
package vo
type HealthStatus struct {
Status string `json:"status"`
}

View File

@@ -0,0 +1,14 @@
package exception
import (
"github.com/labstack/echo/v4"
)
func RegisterErrorHandler(e *echo.Echo) {
e.HTTPErrorHandler = func(err error, c echo.Context) {
_ = c.JSON(500, map[string]any{
"code": "INTERNAL_ERROR",
"message": err.Error(),
})
}
}

View File

@@ -0,0 +1,7 @@
package exception
import "go.uber.org/fx"
var Module = fx.Options(
fx.Invoke(RegisterErrorHandler),
)

17
internal/infra/cache/redis.go vendored Normal file
View File

@@ -0,0 +1,17 @@
package cache
import (
"github.com/redis/go-redis/v9"
"epic-ent/internal/config"
)
func NewRedis(cfg *config.Config) (*redis.Client, error) {
rdb := redis.NewClient(&redis.Options{
Addr: cfg.Redis.Addr,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
})
return rdb, nil
}

View File

@@ -0,0 +1,39 @@
package cron
import (
"context"
"time"
"github.com/robfig/cron/v3"
"go.uber.org/fx"
"go.uber.org/zap"
"epic-ent/internal/config"
)
func RegisterJobs(lc fx.Lifecycle, cfg *config.Config, logger *zap.Logger) {
if !cfg.Cron.Enabled {
return
}
loc, err := time.LoadLocation(cfg.Cron.Timezone)
if err != nil {
logger.Warn("invalid timezone, fallback to Local", zap.Error(err))
loc = time.Local
}
c := cron.New(cron.WithLocation(loc))
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
c.Start()
logger.Info("cron started")
return nil
},
OnStop: func(ctx context.Context) error {
c.Stop()
logger.Info("cron stopped")
return nil
},
})
}

View File

@@ -0,0 +1,18 @@
package db
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"epic-ent/internal/config"
)
func NewMySQL(cfg *config.Config) (*sql.DB, error) {
db, err := sql.Open("mysql", cfg.MySQL.DSN)
if err != nil {
return nil, err
}
return db, nil
}

View File

@@ -0,0 +1,49 @@
package http
import (
"context"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"go.uber.org/fx"
"go.uber.org/zap"
"epic-ent/internal/config"
)
func NewEcho() *echo.Echo {
return echo.New()
}
func StartServer(lc fx.Lifecycle, cfg *config.Config, e *echo.Echo, logger *zap.Logger) {
addr := fmt.Sprintf(":%d", cfg.Server.Port)
if cfg.Server.ReadTimeout != "" {
if d, err := time.ParseDuration(cfg.Server.ReadTimeout); err == nil {
e.Server.ReadTimeout = d
}
}
if cfg.Server.WriteTimeout != "" {
if d, err := time.ParseDuration(cfg.Server.WriteTimeout); err == nil {
e.Server.WriteTimeout = d
}
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
logger.Info("http server starting", zap.String("addr", addr))
go func() {
if err := e.Start(addr); err != nil && err != http.ErrServerClosed {
logger.Error("http server stopped", zap.Error(err))
}
}()
return nil
},
OnStop: func(ctx context.Context) error {
logger.Info("http server stopping")
return e.Shutdown(ctx)
},
})
}

View File

@@ -0,0 +1,23 @@
package log
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"epic-ent/internal/config"
)
func NewLogger(cfg *config.Config) (*zap.Logger, error) {
level := zapcore.InfoLevel
if err := level.Set(cfg.Log.Level); err != nil {
level = zapcore.InfoLevel
}
zapCfg := zap.NewProductionConfig()
zapCfg.Level = zap.NewAtomicLevelAt(level)
if cfg.Log.Format == "console" {
zapCfg.Encoding = "console"
}
return zapCfg.Build()
}

24
internal/infra/module.go Normal file
View File

@@ -0,0 +1,24 @@
package infra
import (
"go.uber.org/fx"
"epic-ent/internal/infra/cache"
"epic-ent/internal/infra/cron"
"epic-ent/internal/infra/db"
"epic-ent/internal/infra/http"
infraLog "epic-ent/internal/infra/log"
)
var Module = fx.Options(
fx.Provide(
infraLog.NewLogger,
db.NewMySQL,
cache.NewRedis,
http.NewEcho,
),
fx.Invoke(
cron.RegisterJobs,
http.StartServer,
),
)

View File

@@ -0,0 +1,9 @@
package middleware
import (
"github.com/labstack/echo/v4"
)
func Register(e *echo.Echo) {
e.HideBanner = true
}

View File

@@ -0,0 +1,7 @@
package middleware
import "go.uber.org/fx"
var Module = fx.Options(
fx.Invoke(Register),
)

View File

@@ -0,0 +1,15 @@
package repository
import (
"epic-ent/internal/domain/vo"
)
type HealthRepository struct{}
func NewHealthRepository() *HealthRepository {
return &HealthRepository{}
}
func (r *HealthRepository) Check() vo.HealthStatus {
return vo.HealthStatus{Status: "ok"}
}

View File

@@ -0,0 +1,9 @@
package repository
import "go.uber.org/fx"
var Module = fx.Options(
fx.Provide(
NewHealthRepository,
),
)

View File

@@ -0,0 +1,18 @@
package service
import (
"epic-ent/internal/domain/vo"
"epic-ent/internal/repository"
)
type HealthService struct {
repo *repository.HealthRepository
}
func NewHealthService(repo *repository.HealthRepository) *HealthService {
return &HealthService{repo: repo}
}
func (s *HealthService) Check() vo.HealthStatus {
return s.repo.Check()
}

View File

@@ -0,0 +1,9 @@
package service
import "go.uber.org/fx"
var Module = fx.Options(
fx.Provide(
NewHealthService,
),
)

3
internal/util/consts.go Normal file
View File

@@ -0,0 +1,3 @@
package util
const AppName = "epic-ent"