commit fed727e59378caa775879ddf21c7ebc941040c8b Author: kever Date: Wed Jan 14 23:58:00 2026 +0800 add initial application structure with configuration, logging, and health check endpoints diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e537b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +*.log + +# Build outputs +/bin/ +/dist/ +/tmp/ +/build/ + +# Go workspace +/vendor/ +/coverage/ +/cover.out +*.prof + +# Local env/config overrides +.env +.env.* +application.local.yaml +application.*.local.yaml + +# IDE/editor +.idea/ +.vscode/ + +# OS +.DS_Store +Thumbs.db diff --git a/application.yaml b/application.yaml new file mode 100644 index 0000000..234797a --- /dev/null +++ b/application.yaml @@ -0,0 +1,20 @@ +server: + port: 8080 + readTimeout: 5s + writeTimeout: 5s + +mysql: + dsn: "root:mysql_78GywN@tcp(111.228.49.52:3306)/db?parseTime=true&loc=Local" + +redis: + addr: "111.228.49.52" + password: "redis_7aFAxY" + db: 0 + +log: + level: info + format: json + +cron: + enabled: true + timezone: "Asia/Shanghai" diff --git a/cmd/gen/controllers/main.go b/cmd/gen/controllers/main.go new file mode 100644 index 0000000..357ad29 --- /dev/null +++ b/cmd/gen/controllers/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "bytes" + "fmt" + "go/format" + "os" + "path/filepath" + "regexp" + "sort" +) + +func main() { + controllerDir, err := findControllerDir() + if err != nil { + fatal(err) + } + + ctors, err := findControllerCtors(controllerDir) + if err != nil { + fatal(err) + } + + content, err := renderModule(ctors) + if err != nil { + fatal(err) + } + + outPath := filepath.Join(controllerDir, "module.go") + if err := os.WriteFile(outPath, content, 0o644); err != nil { + fatal(err) + } +} + +func findControllerDir() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + + if exists(filepath.Join(wd, "module.go")) && exists(filepath.Join(wd, "routes.go")) { + return wd, nil + } + + candidate := filepath.Join(wd, "internal", "controller") + if exists(filepath.Join(candidate, "module.go")) { + return candidate, nil + } + + return "", fmt.Errorf("could not locate internal/controller") +} + +func findControllerCtors(dir string) ([]string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + ctorRe := regexp.MustCompile(`(?m)^func\s+(New[A-Za-z0-9_]*Controller)\s*\(`) + + seen := make(map[string]struct{}) + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !hasSuffix(name, "_controller.go") { + continue + } + if name == "module.go" { + continue + } + + path := filepath.Join(dir, name) + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + matches := ctorRe.FindAllSubmatch(data, -1) + for _, m := range matches { + if len(m) < 2 { + continue + } + seen[string(m[1])] = struct{}{} + } + } + + var ctors []string + for k := range seen { + ctors = append(ctors, k) + } + sort.Strings(ctors) + + return ctors, nil +} + +func renderModule(ctors []string) ([]byte, error) { + var buf bytes.Buffer + + buf.WriteString("// Code generated by cmd/gen/controllers; DO NOT EDIT.\n") + buf.WriteString("package controller\n\n") + buf.WriteString("import \"go.uber.org/fx\"\n\n") + buf.WriteString("var Module = fx.Options(\n") + + if len(ctors) > 0 { + buf.WriteString("\tfx.Provide(\n") + for _, ctor := range ctors { + buf.WriteString("\t\t") + buf.WriteString(ctor) + buf.WriteString(",\n") + } + buf.WriteString("\t),\n") + } + + buf.WriteString("\tfx.Invoke(\n\t\tRegisterRoutes,\n\t),\n") + buf.WriteString(")\n") + + formatted, err := format.Source(buf.Bytes()) + if err != nil { + return nil, err + } + + return formatted, nil +} + +func exists(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return !info.IsDir() +} + +func hasSuffix(s, suffix string) bool { + return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..79de99d --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "context" + "log" + + "epic-ent/internal/bootstrap" +) + +func main() { + app := bootstrap.NewApp() + + if err := app.Start(context.Background()); err != nil { + log.Fatalf("failed to start app: %v", err) + } + + <-app.Done() + + if err := app.Stop(context.Background()); err != nil { + log.Printf("failed to stop app: %v", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..41b2d78 --- /dev/null +++ b/go.mod @@ -0,0 +1,40 @@ +module epic-ent + +go 1.24.0 + +require ( + github.com/go-sql-driver/mysql v1.9.3 + github.com/labstack/echo/v4 v4.15.0 + github.com/redis/go-redis/v9 v9.17.2 + github.com/robfig/cron/v3 v3.0.1 + github.com/spf13/viper v1.21.0 + go.uber.org/fx v1.24.0 + go.uber.org/zap v1.27.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..95ca2bb --- /dev/null +++ b/go.sum @@ -0,0 +1,70 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24= +github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.22.2/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/bootstrap/app.go b/internal/bootstrap/app.go new file mode 100644 index 0000000..b934515 --- /dev/null +++ b/internal/bootstrap/app.go @@ -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, + ) +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..8cbb49e --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/config/module.go b/internal/config/module.go new file mode 100644 index 0000000..a4b3425 --- /dev/null +++ b/internal/config/module.go @@ -0,0 +1,7 @@ +package config + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide(Load), +) diff --git a/internal/controller/doc.go b/internal/controller/doc.go new file mode 100644 index 0000000..f0b62ac --- /dev/null +++ b/internal/controller/doc.go @@ -0,0 +1,3 @@ +package controller + +//go:generate go run ../../cmd/gen/controllers diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go new file mode 100644 index 0000000..509f7b9 --- /dev/null +++ b/internal/controller/health_controller.go @@ -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) +} diff --git a/internal/controller/module.go b/internal/controller/module.go new file mode 100644 index 0000000..96b022d --- /dev/null +++ b/internal/controller/module.go @@ -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, + ), +) diff --git a/internal/controller/routes.go b/internal/controller/routes.go new file mode 100644 index 0000000..9f278ec --- /dev/null +++ b/internal/controller/routes.go @@ -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) +} diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go new file mode 100644 index 0000000..84824bb --- /dev/null +++ b/internal/controller/user_controller.go @@ -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, "你好") +} diff --git a/internal/domain/dto/health.go b/internal/domain/dto/health.go new file mode 100644 index 0000000..fdfda22 --- /dev/null +++ b/internal/domain/dto/health.go @@ -0,0 +1,5 @@ +package dto + +type HealthRequest struct { + Ping string `json:"ping"` +} diff --git a/internal/domain/vo/health.go b/internal/domain/vo/health.go new file mode 100644 index 0000000..7469364 --- /dev/null +++ b/internal/domain/vo/health.go @@ -0,0 +1,5 @@ +package vo + +type HealthStatus struct { + Status string `json:"status"` +} diff --git a/internal/exception/handler.go b/internal/exception/handler.go new file mode 100644 index 0000000..9635f14 --- /dev/null +++ b/internal/exception/handler.go @@ -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(), + }) + } +} diff --git a/internal/exception/module.go b/internal/exception/module.go new file mode 100644 index 0000000..b38bc52 --- /dev/null +++ b/internal/exception/module.go @@ -0,0 +1,7 @@ +package exception + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Invoke(RegisterErrorHandler), +) diff --git a/internal/infra/cache/redis.go b/internal/infra/cache/redis.go new file mode 100644 index 0000000..39abad3 --- /dev/null +++ b/internal/infra/cache/redis.go @@ -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 +} diff --git a/internal/infra/cron/cron.go b/internal/infra/cron/cron.go new file mode 100644 index 0000000..115b634 --- /dev/null +++ b/internal/infra/cron/cron.go @@ -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 + }, + }) +} diff --git a/internal/infra/db/mysql.go b/internal/infra/db/mysql.go new file mode 100644 index 0000000..0212402 --- /dev/null +++ b/internal/infra/db/mysql.go @@ -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 +} diff --git a/internal/infra/http/http.go b/internal/infra/http/http.go new file mode 100644 index 0000000..713372a --- /dev/null +++ b/internal/infra/http/http.go @@ -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) + }, + }) +} diff --git a/internal/infra/log/logger.go b/internal/infra/log/logger.go new file mode 100644 index 0000000..5a6383d --- /dev/null +++ b/internal/infra/log/logger.go @@ -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() +} diff --git a/internal/infra/module.go b/internal/infra/module.go new file mode 100644 index 0000000..a4a8d78 --- /dev/null +++ b/internal/infra/module.go @@ -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, + ), +) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go new file mode 100644 index 0000000..47b0e43 --- /dev/null +++ b/internal/middleware/middleware.go @@ -0,0 +1,9 @@ +package middleware + +import ( + "github.com/labstack/echo/v4" +) + +func Register(e *echo.Echo) { + e.HideBanner = true +} diff --git a/internal/middleware/module.go b/internal/middleware/module.go new file mode 100644 index 0000000..b35fa29 --- /dev/null +++ b/internal/middleware/module.go @@ -0,0 +1,7 @@ +package middleware + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Invoke(Register), +) diff --git a/internal/repository/health_repo.go b/internal/repository/health_repo.go new file mode 100644 index 0000000..6d103e3 --- /dev/null +++ b/internal/repository/health_repo.go @@ -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"} +} diff --git a/internal/repository/module.go b/internal/repository/module.go new file mode 100644 index 0000000..e6ff95f --- /dev/null +++ b/internal/repository/module.go @@ -0,0 +1,9 @@ +package repository + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + NewHealthRepository, + ), +) diff --git a/internal/service/health_service.go b/internal/service/health_service.go new file mode 100644 index 0000000..185b7b8 --- /dev/null +++ b/internal/service/health_service.go @@ -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() +} diff --git a/internal/service/module.go b/internal/service/module.go new file mode 100644 index 0000000..7c1a38c --- /dev/null +++ b/internal/service/module.go @@ -0,0 +1,9 @@ +package service + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + NewHealthService, + ), +) diff --git a/internal/util/consts.go b/internal/util/consts.go new file mode 100644 index 0000000..20e5d42 --- /dev/null +++ b/internal/util/consts.go @@ -0,0 +1,3 @@ +package util + +const AppName = "epic-ent"