package svc import ( "context" "database/sql" "log" "net/url" "strings" "time" "aijianzhan/platform/internal/agentstore" "aijianzhan/platform/internal/audit" "aijianzhan/platform/internal/authx" "aijianzhan/platform/internal/bindcodestore" "aijianzhan/platform/internal/config" "aijianzhan/platform/internal/crud" "aijianzhan/platform/internal/dbsync" "aijianzhan/platform/internal/invitestore" "aijianzhan/platform/internal/license" "aijianzhan/platform/internal/meta" "aijianzhan/platform/internal/orgunitstore" "aijianzhan/platform/internal/ratelimit" "aijianzhan/platform/internal/rolestore" "aijianzhan/platform/internal/schema" "aijianzhan/platform/internal/smsstore" "aijianzhan/platform/internal/storage" "aijianzhan/platform/internal/tenantperm" "aijianzhan/platform/internal/userstore" _ "github.com/lib/pq" ) type ServiceContext struct { Config config.Config Meta meta.Store Schema schema.Runner CRUD crud.Engine Users userstore.Store Agents agentstore.Store Roles rolestore.Store Invites invitestore.Store OrgUnits orgunitstore.Store Audit audit.Store Objects storage.Store Limiter *ratelimit.Limiter DBPool *crud.DBPool MemoryMode bool DB *sql.DB JWT authx.JWTConfig DBSync *dbsync.Manager BindCodes bindcodestore.Store TenantPerm tenantperm.Store SMS *smsstore.Store License *license.Manager } func NewServiceContext(c config.Config) *ServiceContext { users := userstore.NewMemoryStore() aud := audit.NewMemoryStore() agents := agentstore.NewMemoryStore() roles := rolestore.NewMemoryStore() invites := invitestore.NewMemoryStore() orgUnits := orgunitstore.NewMemoryStore() pubBase := strings.TrimRight(c.PublicBaseURL, "/") if pubBase == "" { pubBase = "http://127.0.0.1:8180" } storageBase := c.Storage.PublicBase if storageBase == "" { storageBase = pubBase + "/api/v1/storage" } obj, err := storage.NewLocalStore(c.Storage.LocalRoot, storageBase) if err != nil { log.Printf("storage init: %v", err) obj, _ = storage.NewLocalStore("./data/uploads", storageBase) } ctx := &ServiceContext{ Config: c, Meta: meta.NewMemoryStore(), Schema: schema.NoopRunner{}, CRUD: crud.NewMemoryEngine(), Users: users, Agents: agents, Roles: roles, Invites: invites, OrgUnits: orgUnits, Audit: aud, TenantPerm: tenantperm.NewMemoryStore(), Objects: obj, Limiter: ratelimit.New(c.RateLimitPerMin), MemoryMode: true, SMS: smsstore.New( time.Duration(c.SMS.CodeTTLSeconds)*time.Second, time.Duration(c.SMS.ResendSeconds)*time.Second, c.SMS.DevFixedCode, ), JWT: authx.JWTConfig{ AccessSecret: c.Auth.AccessSecret, AccessExpire: c.Auth.AccessExpire, }, } if ctx.JWT.AccessSecret == "" { ctx.JWT.AccessSecret = "dev-only-change-me" } if c.DataSource != "" && !c.DryRun { db, err := sql.Open("postgres", c.DataSource) if err != nil { log.Printf("postgres open failed, fallback memory: %v", err) } else if err := db.Ping(); err != nil { log.Printf("postgres ping failed, fallback memory: %v", err) _ = db.Close() } else { db.SetMaxOpenConns(20) db.SetMaxIdleConns(5) migCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if err := meta.EnsureSchema(migCtx, db); err != nil { log.Printf("meta migrate failed: %v", err) ctx.DB = db ctx.Schema = &schema.PostgresRunner{DB: db, AdminDSN: adminDSN(c.DataSource)} pool := crud.NewDBPool(db, c.DataSource) ctx.DBPool = pool ctx.CRUD = crud.NewPostgresEngine(db, pool) ctx.MemoryMode = false } else { ctx.DB = db ctx.Schema = &schema.PostgresRunner{DB: db, AdminDSN: adminDSN(c.DataSource)} pool := crud.NewDBPool(db, c.DataSource) ctx.DBPool = pool ctx.CRUD = crud.NewPostgresEngine(db, pool) ctx.Meta = meta.NewPostgresStore(db) ctx.Users = userstore.NewPostgresStore(db) ctx.Agents = agentstore.NewPostgresStore(db) ctx.Roles = rolestore.NewPostgresStore(db) ctx.Invites = invitestore.NewPostgresStore(db) ctx.OrgUnits = orgunitstore.NewPostgresStore(db) ctx.Audit = audit.NewPostgresStore(db) ctx.TenantPerm = tenantperm.NewPostgresStore(db) ctx.MemoryMode = false log.Printf("postgres engine + meta + users + agents + roles + invites + org_units + audit + tenant_perm enabled") } } } userstore.EnsureDemoUser(context.Background(), ctx.Users) userstore.EnsurePlatformAdminUser(context.Background(), ctx.Users) if ctx.Users != nil { if err := ctx.Users.EnsureTenantSlugs(context.Background()); err != nil { log.Printf("ensure tenant slugs: %v", err) } } _ = ctx.Roles.EnsureDefaults(context.Background(), 1) if ctx.TenantPerm != nil { _ = ctx.TenantPerm.EnsureDefault(context.Background(), 1) authx.EntitlementChecker = func(c context.Context, tenantID int64, perm string) bool { ok, err := ctx.TenantPerm.Allows(c, tenantID, perm) if err != nil { log.Printf("entitlement check: %v", err) return false } return ok } } // Z9e:存量已发布模块补齐 import/export(不依赖用户再发布) if ctx.Meta != nil && !ctx.MemoryMode { bfCtx, bfCancel := context.WithTimeout(context.Background(), 30*time.Second) meta.RunBackfillDefaultImportExportOnBoot(bfCtx, ctx.Meta) bfCancel() } if c.DryRun { ctx.Schema = schema.NoopRunner{} } // 跨库同步中间件(默认启用) if c.DBSync.Enabled { dir := c.DBSync.DataDir if dir == "" { dir = "./data/dbsync" } store, err := dbsync.NewFileStore(dir) if err != nil { log.Printf("dbsync store: %v", err) } else { ctx.DBSync = dbsync.NewManager(store) log.Printf("dbsync middleware enabled (dir=%s)", dir) if bc, err := bindcodestore.NewFileStore(dir); err != nil { log.Printf("bind code store: %v", err) } else { ctx.BindCodes = bc } } } licCfg := c.License if licCfg.Enabled { if strings.TrimSpace(licCfg.ControlSecret) == "" { licCfg.ControlSecret = c.Auth.IssueSecret } if strings.TrimSpace(licCfg.SignSecret) == "" { licCfg.SignSecret = licCfg.ControlSecret } } lic, err := license.NewManager(licCfg) if err != nil { log.Printf("license manager: %v", err) } else { ctx.License = lic if lic.Enabled() { if ctx.DB != nil { lic.AttachMirror(license.NewPGMirror(ctx.DB)) } log.Printf("license lease enabled (leases=%s state=%s)", lic.Path(), lic.StatePath()) } } return ctx } func adminDSN(dsn string) string { u, err := url.Parse(dsn) if err != nil { return dsn } u.Path = "/postgres" return u.String() }