package meta import ( "context" "encoding/json" "fmt" "sync" "time" "aijianzhan/platform/internal/blueprint" ) type AppStatus string const ( StatusDraft AppStatus = "draft" // 在建:已生成蓝图、尚未成功发布 StatusValidating AppStatus = "validating" StatusProvisioning AppStatus = "provisioning" StatusPublished AppStatus = "published" StatusFailed AppStatus = "failed" ) // StatusLabelCN 管理端展示用中文状态。 func StatusLabelCN(s AppStatus) string { switch s { case StatusPublished: return "已发布" case StatusFailed: return "失败" case StatusDraft, StatusValidating, StatusProvisioning: return "在建" default: if s == "" { return "未知" } return string(s) } } // IsBuilding 是否视为在建(含草稿与发布中)。 func IsBuilding(s AppStatus) bool { return s == StatusDraft || s == StatusValidating || s == StatusProvisioning } type AppRecord struct { AppID string `json:"app_id"` TenantID int64 `json:"tenant_id"` Slug string `json:"slug"` Name string `json:"name"` SchemaName string `json:"schema_name"` DatabaseName string `json:"database_name,omitempty"` Engine string `json:"engine"` Status AppStatus `json:"status"` Blueprint *blueprint.Blueprint `json:"blueprint"` DDL []string `json:"ddl,omitempty"` Endpoints []string `json:"endpoints,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Error string `json:"error,omitempty"` } type ResourceRef struct { App *AppRecord Entity blueprint.Entity Resource blueprint.APIResource } type AppSummary struct { AppID string `json:"app_id"` Slug string `json:"slug"` Name string `json:"name"` Status AppStatus `json:"status"` SchemaName string `json:"schema_name,omitempty"` DatabaseName string `json:"database_name,omitempty"` PageCount int `json:"page_count"` EntityCount int `json:"entity_count"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` } type Store interface { GetBySlug(ctx context.Context, tenantID int64, slug string) (*AppRecord, error) FindPublishedBySlug(ctx context.Context, slug string) (*AppRecord, error) ListByTenant(ctx context.Context, tenantID int64) ([]AppSummary, error) Save(ctx context.Context, app *AppRecord) error ResolveResource(ctx context.Context, tenantID int64, slug, resource string) (*ResourceRef, error) } type MemoryStore struct { mu sync.RWMutex apps map[string]*AppRecord // key: tenantID:slug } func NewMemoryStore() *MemoryStore { return &MemoryStore{apps: map[string]*AppRecord{}} } func key(tenantID int64, slug string) string { return fmt.Sprintf("%d:%s", tenantID, slug) } func (s *MemoryStore) GetBySlug(_ context.Context, tenantID int64, slug string) (*AppRecord, error) { s.mu.RLock() defer s.mu.RUnlock() app, ok := s.apps[key(tenantID, slug)] if !ok { return nil, fmt.Errorf("app not found") } return cloneApp(app), nil } func (s *MemoryStore) FindPublishedBySlug(_ context.Context, slug string) (*AppRecord, error) { s.mu.RLock() defer s.mu.RUnlock() var best *AppRecord for _, app := range s.apps { if app == nil || app.Slug != slug || app.Status != StatusPublished { continue } if best == nil || app.UpdatedAt.After(best.UpdatedAt) { best = app } } if best == nil { return nil, fmt.Errorf("app not found") } return cloneApp(best), nil } func (s *MemoryStore) ListByTenant(_ context.Context, tenantID int64) ([]AppSummary, error) { s.mu.RLock() defer s.mu.RUnlock() out := make([]AppSummary, 0) for _, app := range s.apps { if app == nil || app.TenantID != tenantID { continue } out = append(out, summarizeApp(app)) } return out, nil } func (s *MemoryStore) ListPublishedApps(_ context.Context) ([]*AppRecord, error) { s.mu.RLock() defer s.mu.RUnlock() out := make([]*AppRecord, 0) for _, app := range s.apps { if app == nil || app.Status != StatusPublished { continue } out = append(out, cloneApp(app)) } return out, nil } func summarizeApp(app *AppRecord) AppSummary { sum := AppSummary{ AppID: app.AppID, Slug: app.Slug, Name: app.Name, Status: app.Status, SchemaName: app.SchemaName, DatabaseName: app.DatabaseName, UpdatedAt: app.UpdatedAt, CreatedAt: app.CreatedAt, } if app.Blueprint != nil { sum.PageCount = len(app.Blueprint.Pages) sum.EntityCount = len(app.Blueprint.Entities) } return sum } func (s *MemoryStore) Save(_ context.Context, app *AppRecord) error { s.mu.Lock() defer s.mu.Unlock() s.apps[key(app.TenantID, app.Slug)] = cloneApp(app) return nil } func (s *MemoryStore) ResolveResource(ctx context.Context, tenantID int64, slug, resource string) (*ResourceRef, error) { app, err := s.GetBySlug(ctx, tenantID, slug) if err != nil { return nil, err } return resolveResource(app, resource) } func resolveResource(app *AppRecord, resource string) (*ResourceRef, error) { if app.Status != StatusPublished { return nil, fmt.Errorf("app not published") } if app.Blueprint == nil { return nil, fmt.Errorf("blueprint missing") } for _, r := range app.Blueprint.Apis.Resources { path := r.Path if len(path) > 0 && path[0] == '/' { path = path[1:] } if path != resource { continue } for _, e := range app.Blueprint.Entities { if e.Name == r.Entity { return &ResourceRef{App: app, Entity: e, Resource: r}, nil } } return nil, fmt.Errorf("entity missing for resource") } return nil, fmt.Errorf("resource not found") } func cloneApp(app *AppRecord) *AppRecord { if app == nil { return nil } cp := *app if app.Blueprint != nil { raw, _ := json.Marshal(app.Blueprint) var bp blueprint.Blueprint _ = json.Unmarshal(raw, &bp) cp.Blueprint = &bp } if app.DDL != nil { cp.DDL = append([]string{}, app.DDL...) } if app.Endpoints != nil { cp.Endpoints = append([]string{}, app.Endpoints...) } return &cp }