add initial application structure with configuration, logging, and health check endpoints
This commit is contained in:
25
internal/bootstrap/app.go
Normal file
25
internal/bootstrap/app.go
Normal 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
62
internal/config/config.go
Normal 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
|
||||
}
|
||||
7
internal/config/module.go
Normal file
7
internal/config/module.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
import "go.uber.org/fx"
|
||||
|
||||
var Module = fx.Options(
|
||||
fx.Provide(Load),
|
||||
)
|
||||
3
internal/controller/doc.go
Normal file
3
internal/controller/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package controller
|
||||
|
||||
//go:generate go run ../../cmd/gen/controllers
|
||||
22
internal/controller/health_controller.go
Normal file
22
internal/controller/health_controller.go
Normal 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)
|
||||
}
|
||||
13
internal/controller/module.go
Normal file
13
internal/controller/module.go
Normal 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,
|
||||
),
|
||||
)
|
||||
17
internal/controller/routes.go
Normal file
17
internal/controller/routes.go
Normal 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)
|
||||
}
|
||||
22
internal/controller/user_controller.go
Normal file
22
internal/controller/user_controller.go
Normal 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, "你好")
|
||||
}
|
||||
5
internal/domain/dto/health.go
Normal file
5
internal/domain/dto/health.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package dto
|
||||
|
||||
type HealthRequest struct {
|
||||
Ping string `json:"ping"`
|
||||
}
|
||||
5
internal/domain/vo/health.go
Normal file
5
internal/domain/vo/health.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package vo
|
||||
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
14
internal/exception/handler.go
Normal file
14
internal/exception/handler.go
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
7
internal/exception/module.go
Normal file
7
internal/exception/module.go
Normal 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
17
internal/infra/cache/redis.go
vendored
Normal 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
|
||||
}
|
||||
39
internal/infra/cron/cron.go
Normal file
39
internal/infra/cron/cron.go
Normal 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
|
||||
},
|
||||
})
|
||||
}
|
||||
18
internal/infra/db/mysql.go
Normal file
18
internal/infra/db/mysql.go
Normal 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
|
||||
}
|
||||
49
internal/infra/http/http.go
Normal file
49
internal/infra/http/http.go
Normal 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)
|
||||
},
|
||||
})
|
||||
}
|
||||
23
internal/infra/log/logger.go
Normal file
23
internal/infra/log/logger.go
Normal 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
24
internal/infra/module.go
Normal 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,
|
||||
),
|
||||
)
|
||||
9
internal/middleware/middleware.go
Normal file
9
internal/middleware/middleware.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Register(e *echo.Echo) {
|
||||
e.HideBanner = true
|
||||
}
|
||||
7
internal/middleware/module.go
Normal file
7
internal/middleware/module.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import "go.uber.org/fx"
|
||||
|
||||
var Module = fx.Options(
|
||||
fx.Invoke(Register),
|
||||
)
|
||||
15
internal/repository/health_repo.go
Normal file
15
internal/repository/health_repo.go
Normal 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"}
|
||||
}
|
||||
9
internal/repository/module.go
Normal file
9
internal/repository/module.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package repository
|
||||
|
||||
import "go.uber.org/fx"
|
||||
|
||||
var Module = fx.Options(
|
||||
fx.Provide(
|
||||
NewHealthRepository,
|
||||
),
|
||||
)
|
||||
18
internal/service/health_service.go
Normal file
18
internal/service/health_service.go
Normal 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()
|
||||
}
|
||||
9
internal/service/module.go
Normal file
9
internal/service/module.go
Normal 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
3
internal/util/consts.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package util
|
||||
|
||||
const AppName = "epic-ent"
|
||||
Reference in New Issue
Block a user