package meta import ( "context" "database/sql" "encoding/json" "errors" "fmt" "time" "aijianzhan/platform/internal/blueprint" "github.com/lib/pq" ) type PostgresStore struct { DB *sql.DB } func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} } func (s *PostgresStore) GetBySlug(ctx context.Context, tenantID int64, slug string) (*AppRecord, error) { const q = ` SELECT app_id, tenant_id, slug, name, schema_name, COALESCE(database_name,''), engine, status, blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at FROM platform_meta.tenant_apps WHERE tenant_id = $1 AND slug = $2 LIMIT 1` row := s.DB.QueryRowContext(ctx, q, tenantID, slug) rec, err := scanApp(row) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("app not found") } return rec, err } func (s *PostgresStore) FindPublishedBySlug(ctx context.Context, slug string) (*AppRecord, error) { const q = ` SELECT app_id, tenant_id, slug, name, schema_name, COALESCE(database_name,''), engine, status, blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at FROM platform_meta.tenant_apps WHERE slug = $1 AND status = $2 ORDER BY updated_at DESC LIMIT 1` row := s.DB.QueryRowContext(ctx, q, slug, string(StatusPublished)) rec, err := scanApp(row) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("app not found") } return rec, err } func (s *PostgresStore) ListByTenant(ctx context.Context, tenantID int64) ([]AppSummary, error) { const q = ` SELECT app_id, tenant_id, slug, name, schema_name, COALESCE(database_name,''), engine, status, blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at FROM platform_meta.tenant_apps WHERE tenant_id = $1 ORDER BY updated_at DESC` rows, err := s.DB.QueryContext(ctx, q, tenantID) if err != nil { return nil, err } defer rows.Close() out := make([]AppSummary, 0) for rows.Next() { rec, err := scanApp(rows) if err != nil { return nil, err } out = append(out, summarizeApp(rec)) } return out, rows.Err() } func (s *PostgresStore) Save(ctx context.Context, app *AppRecord) error { if app == nil { return fmt.Errorf("app is nil") } if app.Blueprint == nil { return fmt.Errorf("blueprint is nil") } bpRaw, err := json.Marshal(app.Blueprint) if err != nil { return err } ddlRaw, err := json.Marshal(app.DDL) if err != nil { return err } epRaw, err := json.Marshal(app.Endpoints) if err != nil { return err } if app.CreatedAt.IsZero() { app.CreatedAt = time.Now().UTC() } app.UpdatedAt = time.Now().UTC() const q = ` INSERT INTO platform_meta.tenant_apps ( app_id, tenant_id, slug, name, schema_name, database_name, engine, status, blueprint_json, ddl_json, endpoints_json, error_msg, created_at, updated_at ) VALUES ( $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14 ) ON CONFLICT (tenant_id, slug) DO UPDATE SET name = EXCLUDED.name, schema_name = EXCLUDED.schema_name, database_name = EXCLUDED.database_name, engine = EXCLUDED.engine, status = EXCLUDED.status, blueprint_json = EXCLUDED.blueprint_json, ddl_json = EXCLUDED.ddl_json, endpoints_json = EXCLUDED.endpoints_json, error_msg = EXCLUDED.error_msg, updated_at = EXCLUDED.updated_at, app_id = platform_meta.tenant_apps.app_id, created_at = platform_meta.tenant_apps.created_at RETURNING app_id, created_at, updated_at` err = s.DB.QueryRowContext(ctx, q, app.AppID, app.TenantID, app.Slug, app.Name, app.SchemaName, app.DatabaseName, app.Engine, string(app.Status), bpRaw, ddlRaw, epRaw, app.Error, app.CreatedAt, app.UpdatedAt, ).Scan(&app.AppID, &app.CreatedAt, &app.UpdatedAt) if err != nil { return fmt.Errorf("meta save: %w", err) } return nil } func (s *PostgresStore) 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) } type scannable interface { Scan(dest ...any) error } func scanApp(row scannable) (*AppRecord, error) { var ( rec AppRecord status string bpRaw, ddlRaw, epRaw []byte errMsg sql.NullString ) err := row.Scan( &rec.AppID, &rec.TenantID, &rec.Slug, &rec.Name, &rec.SchemaName, &rec.DatabaseName, &rec.Engine, &status, &bpRaw, &ddlRaw, &epRaw, &errMsg, &rec.CreatedAt, &rec.UpdatedAt, ) if err != nil { return nil, err } rec.Status = AppStatus(status) if errMsg.Valid { rec.Error = errMsg.String } var bp blueprint.Blueprint if err := json.Unmarshal(bpRaw, &bp); err != nil { return nil, fmt.Errorf("blueprint json: %w", err) } rec.Blueprint = &bp if len(ddlRaw) > 0 { _ = json.Unmarshal(ddlRaw, &rec.DDL) } if len(epRaw) > 0 { _ = json.Unmarshal(epRaw, &rec.Endpoints) } return &rec, nil } // IsUniqueViolation 便于上层识别冲突(预留)。 func IsUniqueViolation(err error) bool { var pqErr *pq.Error return errors.As(err, &pqErr) && pqErr.Code == "23505" }