Add backend foundation and config-driven workbench

This commit is contained in:
2026-04-01 15:01:44 +08:00
parent 88b8f05f03
commit 94a1f0ba78
68 changed files with 10833 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
package app
import (
"context"
"net/http"
"cmr-backend/internal/httpapi"
"cmr-backend/internal/platform/jwtx"
"cmr-backend/internal/platform/wechatmini"
"cmr-backend/internal/service"
"cmr-backend/internal/store/postgres"
)
type App struct {
router http.Handler
store *postgres.Store
}
func New(ctx context.Context, cfg Config) (*App, error) {
pool, err := postgres.Open(ctx, cfg.DatabaseURL)
if err != nil {
return nil, err
}
store := postgres.NewStore(pool)
jwtManager := jwtx.NewManager(cfg.JWTIssuer, cfg.JWTAccessSecret, cfg.JWTAccessTTL)
wechatMiniClient := wechatmini.NewClient(cfg.WechatMiniAppID, cfg.WechatMiniSecret, cfg.WechatMiniDevPrefix)
authService := service.NewAuthService(service.AuthSettings{
AppEnv: cfg.AppEnv,
RefreshTTL: cfg.RefreshTTL,
SMSCodeTTL: cfg.SMSCodeTTL,
SMSCodeCooldown: cfg.SMSCodeCooldown,
SMSProvider: cfg.SMSProvider,
DevSMSCode: cfg.DevSMSCode,
WechatMini: wechatMiniClient,
}, store, jwtManager)
entryService := service.NewEntryService(store)
entryHomeService := service.NewEntryHomeService(store)
eventService := service.NewEventService(store)
eventPlayService := service.NewEventPlayService(store)
configService := service.NewConfigService(store, cfg.LocalEventDir, cfg.AssetBaseURL)
homeService := service.NewHomeService(store)
profileService := service.NewProfileService(store)
resultService := service.NewResultService(store)
sessionService := service.NewSessionService(store)
devService := service.NewDevService(cfg.AppEnv, store)
meService := service.NewMeService(store)
router := httpapi.NewRouter(cfg.AppEnv, jwtManager, authService, entryService, entryHomeService, eventService, eventPlayService, configService, homeService, profileService, resultService, sessionService, devService, meService)
return &App{
router: router,
store: store,
}, nil
}
func (a *App) Router() http.Handler {
return a.router
}
func (a *App) Close() {
if a.store != nil {
a.store.Close()
}
}

View File

@@ -0,0 +1,73 @@
package app
import (
"fmt"
"os"
"path/filepath"
"time"
)
type Config struct {
AppEnv string
HTTPAddr string
DatabaseURL string
JWTIssuer string
JWTAccessSecret string
JWTAccessTTL time.Duration
RefreshTTL time.Duration
SMSCodeTTL time.Duration
SMSCodeCooldown time.Duration
SMSProvider string
DevSMSCode string
WechatMiniAppID string
WechatMiniSecret string
WechatMiniDevPrefix string
LocalEventDir string
AssetBaseURL string
}
func LoadConfigFromEnv() (Config, error) {
cfg := Config{
AppEnv: getEnv("APP_ENV", "development"),
HTTPAddr: getEnv("HTTP_ADDR", ":8080"),
DatabaseURL: os.Getenv("DATABASE_URL"),
JWTIssuer: getEnv("JWT_ISSUER", "cmr-backend"),
JWTAccessSecret: getEnv("JWT_ACCESS_SECRET", "change-me-in-production"),
JWTAccessTTL: getDurationEnv("JWT_ACCESS_TTL", 2*time.Hour),
RefreshTTL: getDurationEnv("AUTH_REFRESH_TTL", 30*24*time.Hour),
SMSCodeTTL: getDurationEnv("AUTH_SMS_CODE_TTL", 10*time.Minute),
SMSCodeCooldown: getDurationEnv("AUTH_SMS_COOLDOWN", 60*time.Second),
SMSProvider: getEnv("AUTH_SMS_PROVIDER", "console"),
DevSMSCode: os.Getenv("AUTH_DEV_SMS_CODE"),
WechatMiniAppID: getEnv("WECHAT_MINI_APP_ID", ""),
WechatMiniSecret: getEnv("WECHAT_MINI_APP_SECRET", ""),
WechatMiniDevPrefix: getEnv("WECHAT_MINI_DEV_PREFIX", "dev-"),
LocalEventDir: getEnv("LOCAL_EVENT_DIR", filepath.Clean("..\\event")),
AssetBaseURL: getEnv("ASSET_BASE_URL", "https://oss-mbh5.colormaprun.com/gotomars"),
}
if cfg.DatabaseURL == "" {
return Config{}, fmt.Errorf("DATABASE_URL is required")
}
if cfg.JWTAccessSecret == "" {
return Config{}, fmt.Errorf("JWT_ACCESS_SECRET is required")
}
return cfg, nil
}
func getEnv(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
func getDurationEnv(key string, fallback time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if parsed, err := time.ParseDuration(value); err == nil {
return parsed
}
}
return fallback
}