调整项目架构,让框架更优化
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"git.toowon.com/jimmy/go-common/config"
|
||||
"git.toowon.com/jimmy/go-common/email"
|
||||
"git.toowon.com/jimmy/go-common/excel"
|
||||
commonhttp "git.toowon.com/jimmy/go-common/http"
|
||||
"git.toowon.com/jimmy/go-common/i18n"
|
||||
"git.toowon.com/jimmy/go-common/logger"
|
||||
"git.toowon.com/jimmy/go-common/middleware"
|
||||
@@ -38,7 +39,6 @@ type Factory struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
i18n *i18n.I18n
|
||||
excel *excel.Excel
|
||||
chain *middleware.Chain
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -47,13 +47,6 @@ type Factory struct {
|
||||
// Option Factory 可选项(支持重载模块实现)
|
||||
type Option func(*Factory)
|
||||
|
||||
// WithStorage 注入自定义存储实现
|
||||
func WithStorage(s storage.Storage) Option {
|
||||
return func(f *Factory) {
|
||||
f.storage = s
|
||||
}
|
||||
}
|
||||
|
||||
// Init 从配置文件初始化全局 Factory(启动时调用一次)
|
||||
func Init(filePath string, opts ...Option) error {
|
||||
cfg, err := config.LoadFromFile(filePath)
|
||||
@@ -86,6 +79,15 @@ func (f *Factory) Config() *config.Config {
|
||||
return f.cfg
|
||||
}
|
||||
|
||||
// NewHandler 创建 HTTP 出参处理器,并自动注入已配置的 i18n(若可用)
|
||||
// 能力仍在 *http.Handler 上(Success / Error),Factory 不透传出参方法。
|
||||
func (f *Factory) NewHandler(w http.ResponseWriter, r *http.Request, opts ...commonhttp.HandlerOption) *commonhttp.Handler {
|
||||
if i18nInst, err := f.getI18n(); err == nil && i18nInst != nil {
|
||||
opts = append([]commonhttp.HandlerOption{commonhttp.WithI18n(i18nInst)}, opts...)
|
||||
}
|
||||
return commonhttp.NewHandler(w, r, opts...)
|
||||
}
|
||||
|
||||
func (f *Factory) getLogger() (*logger.Logger, error) {
|
||||
if f.logger != nil {
|
||||
return f.logger, nil
|
||||
@@ -107,7 +109,7 @@ func (f *Factory) getLogger() (*logger.Logger, error) {
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Logger 获取日志对象
|
||||
// Logger 获取日志对象(无 logger 配置时使用默认配置)
|
||||
func (f *Factory) Logger() (*logger.Logger, error) {
|
||||
return f.getLogger()
|
||||
}
|
||||
@@ -312,21 +314,9 @@ func (f *Factory) I18n() (*i18n.I18n, error) {
|
||||
return f.getI18n()
|
||||
}
|
||||
|
||||
func (f *Factory) getExcel() *excel.Excel {
|
||||
if f.excel != nil {
|
||||
return f.excel
|
||||
}
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.excel == nil {
|
||||
f.excel = excel.NewExcel()
|
||||
}
|
||||
return f.excel
|
||||
}
|
||||
|
||||
// Excel 获取 Excel 导出器
|
||||
// Excel 创建 Excel 导出器(每次新建,避免共享可变文件状态)
|
||||
func (f *Factory) Excel() *excel.Excel {
|
||||
return f.getExcel()
|
||||
return excel.NewExcel()
|
||||
}
|
||||
|
||||
// MiddlewareChain 获取默认中间件链
|
||||
@@ -341,8 +331,11 @@ func (f *Factory) MiddlewareChain() *middleware.Chain {
|
||||
}
|
||||
|
||||
var mws []func(http.Handler) http.Handler
|
||||
l, _ := f.getLogger()
|
||||
i18nInst, _ := f.getI18n()
|
||||
|
||||
// logger 无配置时有默认实现;失败则降级为 nil(Recovery/Logging 可处理)
|
||||
l, _ := f.getLoggerUnlocked()
|
||||
// i18n 为可选:未配置时 Recovery 使用默认文案
|
||||
i18nInst, _ := f.getI18nUnlocked()
|
||||
|
||||
mws = append(mws, middleware.Recovery(&middleware.RecoveryConfig{
|
||||
Logger: l,
|
||||
@@ -384,6 +377,41 @@ func (f *Factory) MiddlewareChain() *middleware.Chain {
|
||||
return f.chain
|
||||
}
|
||||
|
||||
// getLoggerUnlocked 在已持有 f.mu 时初始化 logger(供 MiddlewareChain 使用)
|
||||
func (f *Factory) getLoggerUnlocked() (*logger.Logger, error) {
|
||||
if f.logger != nil {
|
||||
return f.logger, nil
|
||||
}
|
||||
var cfg *config.LoggerConfig
|
||||
if f.cfg != nil {
|
||||
cfg = f.cfg.Logger
|
||||
}
|
||||
l, err := logger.NewLogger(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.logger = l
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// getI18nUnlocked 在已持有 f.mu 时初始化 i18n(供 MiddlewareChain 使用)
|
||||
func (f *Factory) getI18nUnlocked() (*i18n.I18n, error) {
|
||||
if f.i18n != nil {
|
||||
return f.i18n, nil
|
||||
}
|
||||
if f.cfg == nil || f.cfg.I18n == nil {
|
||||
return nil, fmt.Errorf("i18n config is nil")
|
||||
}
|
||||
i := i18n.NewI18n(f.cfg.I18n.DefaultLang)
|
||||
if f.cfg.I18n.LocalesDir != "" {
|
||||
if err := i.LoadFromDir(f.cfg.I18n.LocalesDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
f.i18n = i
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// Migrator 创建迁移器并加载指定目录下的 SQL 文件
|
||||
func (f *Factory) Migrator(migrationsDir string) (*migration.Migrator, error) {
|
||||
db, err := f.getDatabase()
|
||||
|
||||
121
factory/factory_test.go
Normal file
121
factory/factory_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.toowon.com/jimmy/go-common/config"
|
||||
"git.toowon.com/jimmy/go-common/i18n"
|
||||
"git.toowon.com/jimmy/go-common/logger"
|
||||
)
|
||||
|
||||
func TestExcelReturnsFreshInstance(t *testing.T) {
|
||||
app := New(nil)
|
||||
a := app.Excel()
|
||||
b := app.Excel()
|
||||
if a == nil || b == nil {
|
||||
t.Fatal("Excel() returned nil")
|
||||
}
|
||||
if a == b {
|
||||
t.Fatal("Excel() should return a new instance each call")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithLoggerOption(t *testing.T) {
|
||||
custom, err := logger.NewLogger(&config.LoggerConfig{
|
||||
Level: "error",
|
||||
Output: "stdout",
|
||||
Async: config.BoolPtr(false),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := New(nil, WithLogger(custom))
|
||||
got, err := app.Logger()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != custom {
|
||||
t.Fatal("WithLogger should inject the provided logger")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithI18nAndNewHandler(t *testing.T) {
|
||||
i := i18n.NewI18n("zh-CN")
|
||||
i.LoadFromMap("zh-CN", map[string]i18n.MessageInfo{
|
||||
"common.success": {Code: 0, Message: "成功"},
|
||||
})
|
||||
app := New(nil, WithI18n(i))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||
h := app.NewHandler(rec, req)
|
||||
h.Success(map[string]string{"ok": "1"})
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if body == "" || !strings.Contains(body, "成功") {
|
||||
t.Fatalf("response body missing i18n message: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHandlerWithoutI18n(t *testing.T) {
|
||||
app := New(nil)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
h := app.NewHandler(rec, req)
|
||||
h.Success(nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupLogger(t *testing.T) {
|
||||
app := New(nil)
|
||||
if err := app.Warmup(ModuleLogger); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupMissingDatabase(t *testing.T) {
|
||||
app := New(nil)
|
||||
err := app.Warmup(ModuleDatabase)
|
||||
if err == nil {
|
||||
t.Fatal("expected database warmup to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupUnknownModule(t *testing.T) {
|
||||
app := New(nil)
|
||||
err := app.Warmup(Module("unknown"))
|
||||
if err == nil {
|
||||
t.Fatal("expected unknown module error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseLogger(t *testing.T) {
|
||||
app := New(nil)
|
||||
if _, err := app.Logger(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := app.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Close 后再取 logger 应可重新创建
|
||||
if _, err := app.Logger(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = app.Close()
|
||||
}
|
||||
|
||||
func TestMustLogger(t *testing.T) {
|
||||
app := New(nil)
|
||||
l := app.MustLogger()
|
||||
if l == nil {
|
||||
t.Fatal("MustLogger returned nil")
|
||||
}
|
||||
}
|
||||
181
factory/lifecycle.go
Normal file
181
factory/lifecycle.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.toowon.com/jimmy/go-common/email"
|
||||
"git.toowon.com/jimmy/go-common/i18n"
|
||||
"git.toowon.com/jimmy/go-common/logger"
|
||||
"git.toowon.com/jimmy/go-common/sms"
|
||||
"git.toowon.com/jimmy/go-common/storage"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Module 可预热的模块标识(用于 Warmup)
|
||||
type Module string
|
||||
|
||||
const (
|
||||
ModuleLogger Module = "logger"
|
||||
ModuleDatabase Module = "database"
|
||||
ModuleRedis Module = "redis"
|
||||
ModuleStorage Module = "storage"
|
||||
ModuleEmail Module = "email"
|
||||
ModuleSMS Module = "sms"
|
||||
ModuleI18n Module = "i18n"
|
||||
)
|
||||
|
||||
// MustInit 从配置文件初始化全局 Factory;失败则 panic(适合 main 启动)
|
||||
func MustInit(filePath string, opts ...Option) *Factory {
|
||||
if err := Init(filePath, opts...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return Default()
|
||||
}
|
||||
|
||||
// Warmup 按需预热模块:配置缺失或连接失败时立即返回错误(适合启动期 fail-fast)
|
||||
func (f *Factory) Warmup(modules ...Module) error {
|
||||
if f == nil {
|
||||
return fmt.Errorf("factory is nil")
|
||||
}
|
||||
for _, m := range modules {
|
||||
var err error
|
||||
switch m {
|
||||
case ModuleLogger:
|
||||
_, err = f.Logger()
|
||||
case ModuleDatabase:
|
||||
_, err = f.Database()
|
||||
case ModuleRedis:
|
||||
_, err = f.Redis()
|
||||
case ModuleStorage:
|
||||
_, err = f.Storage()
|
||||
case ModuleEmail:
|
||||
_, err = f.Email()
|
||||
case ModuleSMS:
|
||||
_, err = f.SMS()
|
||||
case ModuleI18n:
|
||||
_, err = f.I18n()
|
||||
default:
|
||||
return fmt.Errorf("unknown module: %s", m)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("warmup %s: %w", m, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭已创建的有状态模块(email / sms / redis / database / logger)
|
||||
// 可重复调用;未初始化的模块会被跳过。
|
||||
func (f *Factory) Close() error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
var errs []error
|
||||
|
||||
if f.email != nil {
|
||||
if err := f.email.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("email: %w", err))
|
||||
}
|
||||
f.email = nil
|
||||
}
|
||||
if f.sms != nil {
|
||||
if err := f.sms.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("sms: %w", err))
|
||||
}
|
||||
f.sms = nil
|
||||
}
|
||||
if f.redis != nil {
|
||||
if err := f.redis.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("redis: %w", err))
|
||||
}
|
||||
f.redis = nil
|
||||
}
|
||||
if f.db != nil {
|
||||
if sqlDB, err := f.db.DB(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("database: %w", err))
|
||||
} else if err := sqlDB.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("database: %w", err))
|
||||
}
|
||||
f.db = nil
|
||||
}
|
||||
if f.logger != nil {
|
||||
if err := f.logger.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("logger: %w", err))
|
||||
}
|
||||
f.logger = nil
|
||||
}
|
||||
|
||||
// 中间件链依赖 logger/i18n,关闭后允许下次重新组装
|
||||
f.chain = nil
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// MustLogger 获取日志对象;失败则 panic(适合启动期注入)
|
||||
func (f *Factory) MustLogger() *logger.Logger {
|
||||
l, err := f.Logger()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// MustDatabase 获取数据库连接;失败则 panic(适合启动期注入)
|
||||
func (f *Factory) MustDatabase() *gorm.DB {
|
||||
db, err := f.Database()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// MustRedis 获取 Redis 客户端;失败则 panic(适合启动期注入)
|
||||
func (f *Factory) MustRedis() *redis.Client {
|
||||
c, err := f.Redis()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// MustStorage 获取存储对象;失败则 panic(适合启动期注入)
|
||||
func (f *Factory) MustStorage() storage.Storage {
|
||||
s, err := f.Storage()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// MustEmail 获取邮件客户端;失败则 panic(适合启动期注入)
|
||||
func (f *Factory) MustEmail() *email.Email {
|
||||
e, err := f.Email()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// MustSMS 获取短信客户端;失败则 panic(适合启动期注入)
|
||||
func (f *Factory) MustSMS() *sms.SMS {
|
||||
s, err := f.SMS()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// MustI18n 获取国际化对象;失败则 panic(适合启动期注入)
|
||||
func (f *Factory) MustI18n() *i18n.I18n {
|
||||
i, err := f.I18n()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
68
factory/options.go
Normal file
68
factory/options.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"git.toowon.com/jimmy/go-common/email"
|
||||
"git.toowon.com/jimmy/go-common/i18n"
|
||||
"git.toowon.com/jimmy/go-common/logger"
|
||||
"git.toowon.com/jimmy/go-common/middleware"
|
||||
"git.toowon.com/jimmy/go-common/sms"
|
||||
"git.toowon.com/jimmy/go-common/storage"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// WithStorage 注入自定义存储实现
|
||||
func WithStorage(s storage.Storage) Option {
|
||||
return func(f *Factory) {
|
||||
f.storage = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger 注入自定义日志对象
|
||||
func WithLogger(l *logger.Logger) Option {
|
||||
return func(f *Factory) {
|
||||
f.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// WithDatabase 注入自定义数据库连接
|
||||
func WithDatabase(db *gorm.DB) Option {
|
||||
return func(f *Factory) {
|
||||
f.db = db
|
||||
}
|
||||
}
|
||||
|
||||
// WithRedis 注入自定义 Redis 客户端
|
||||
func WithRedis(c *redis.Client) Option {
|
||||
return func(f *Factory) {
|
||||
f.redis = c
|
||||
}
|
||||
}
|
||||
|
||||
// WithEmail 注入自定义邮件客户端
|
||||
func WithEmail(e *email.Email) Option {
|
||||
return func(f *Factory) {
|
||||
f.email = e
|
||||
}
|
||||
}
|
||||
|
||||
// WithSMS 注入自定义短信客户端
|
||||
func WithSMS(s *sms.SMS) Option {
|
||||
return func(f *Factory) {
|
||||
f.sms = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithI18n 注入自定义国际化对象
|
||||
func WithI18n(i *i18n.I18n) Option {
|
||||
return func(f *Factory) {
|
||||
f.i18n = i
|
||||
}
|
||||
}
|
||||
|
||||
// WithMiddlewareChain 注入自定义中间件链
|
||||
func WithMiddlewareChain(c *middleware.Chain) Option {
|
||||
return func(f *Factory) {
|
||||
f.chain = c
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user