Files
ai_site/platform/internal/meta/store.go
2026-07-31 10:19:22 +08:00

218 lines
5.7 KiB
Go

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"`
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 summarizeApp(app *AppRecord) AppSummary {
sum := AppSummary{
AppID: app.AppID,
Slug: app.Slug,
Name: app.Name,
Status: app.Status,
SchemaName: app.SchemaName,
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
}