Files
ai_site/platform/internal/svc/servicecontext.go
whm d195aa4804 feat: add Yuheng ticket bind, trial SMS off, shared bindings
Ship ticket-exchange and bind/policy for Z13, keep trial binds SMS-free, allow shared company bindings, and align SyncPage plus sync docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 15:09:49 +08:00

234 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
"aijianzhan/platform/internal/yuhticket"
_ "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
YuhengJTI *yuhticket.JTIStore
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
}
if jti, err := yuhticket.NewJTIStore(dir); err != nil {
log.Printf("yuheng ticket jti: %v", err)
} else {
ctx.YuhengJTI = jti
}
}
}
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()
}