Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
233 lines
6.1 KiB
Go
233 lines
6.1 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"`
|
|
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
|
|
}
|