feat: add Z12/Z13 bind APIs, stock import, and sync docs
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.
This commit is contained in:
85
platform/internal/meta/backfill_import.go
Normal file
85
platform/internal/meta/backfill_import.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// BackfillImportResult Z9e 扫库补齐 import/export 的结果。
|
||||
type BackfillImportResult struct {
|
||||
Scanned int `json:"scanned"`
|
||||
Updated int `json:"updated"`
|
||||
Skipped int `json:"skipped"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
Slugs []string `json:"updated_slugs,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// PublishedLister 列出全部已发布应用(跨租户,供 Z9e 启动扫库)。
|
||||
type PublishedLister interface {
|
||||
ListPublishedApps(ctx context.Context) ([]*AppRecord, error)
|
||||
}
|
||||
|
||||
// BackfillDefaultImportExport 遍历已发布蓝图,对可写 resource 补齐 import/export 并落库(Z9e)。
|
||||
// dryRun=true 只统计不写库。tenantID>0 时仅处理该租户。
|
||||
func BackfillDefaultImportExport(ctx context.Context, store Store, dryRun bool) (*BackfillImportResult, error) {
|
||||
return BackfillDefaultImportExportForTenant(ctx, store, 0, dryRun)
|
||||
}
|
||||
|
||||
// BackfillDefaultImportExportForTenant 同 BackfillDefaultImportExport;tenantID=0 表示全库。
|
||||
func BackfillDefaultImportExportForTenant(ctx context.Context, store Store, tenantID int64, dryRun bool) (*BackfillImportResult, error) {
|
||||
if store == nil {
|
||||
return nil, fmt.Errorf("meta store nil")
|
||||
}
|
||||
lister, ok := store.(PublishedLister)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("meta store does not support ListPublishedApps")
|
||||
}
|
||||
apps, err := lister.ListPublishedApps(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &BackfillImportResult{DryRun: dryRun, Slugs: make([]string, 0)}
|
||||
for _, app := range apps {
|
||||
if app == nil || app.Blueprint == nil {
|
||||
continue
|
||||
}
|
||||
if tenantID > 0 && app.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
out.Scanned++
|
||||
before, _ := json.Marshal(app.Blueprint)
|
||||
app.Blueprint.EnsureDefaultImportExport()
|
||||
after, _ := json.Marshal(app.Blueprint)
|
||||
if string(before) == string(after) {
|
||||
out.Skipped++
|
||||
continue
|
||||
}
|
||||
out.Updated++
|
||||
out.Slugs = append(out.Slugs, fmt.Sprintf("%d/%s", app.TenantID, app.Slug))
|
||||
if dryRun {
|
||||
continue
|
||||
}
|
||||
if err := store.Save(ctx, app); err != nil {
|
||||
return out, fmt.Errorf("save %s: %w", app.Slug, err)
|
||||
}
|
||||
}
|
||||
out.Message = fmt.Sprintf("scanned=%d updated=%d skipped=%d dry_run=%v", out.Scanned, out.Updated, out.Skipped, dryRun)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RunBackfillDefaultImportExportOnBoot 启动时扫库(失败只打日志,不阻断启动)。
|
||||
func RunBackfillDefaultImportExportOnBoot(ctx context.Context, store Store) {
|
||||
res, err := BackfillDefaultImportExport(ctx, store, false)
|
||||
if err != nil {
|
||||
log.Printf("Z9e backfill import ops: %v", err)
|
||||
return
|
||||
}
|
||||
if res != nil && res.Updated > 0 {
|
||||
log.Printf("Z9e backfill import ops: %s slugs=%v", res.Message, res.Slugs)
|
||||
} else if res != nil {
|
||||
log.Printf("Z9e backfill import ops: %s", res.Message)
|
||||
}
|
||||
}
|
||||
59
platform/internal/meta/backfill_import_test.go
Normal file
59
platform/internal/meta/backfill_import_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"aijianzhan/platform/internal/blueprint"
|
||||
)
|
||||
|
||||
func TestBackfillDefaultImportExport(t *testing.T) {
|
||||
store := NewMemoryStore()
|
||||
bp := &blueprint.Blueprint{}
|
||||
bp.Meta.Name = "coerce"
|
||||
bp.Meta.Slug = "coerce_fields"
|
||||
bp.Apis.Resources = []blueprint.APIResource{{
|
||||
Path: "items",
|
||||
Entity: "item",
|
||||
Operations: []string{"list", "get", "create", "update", "delete"},
|
||||
}}
|
||||
bp.Entities = []blueprint.Entity{{Name: "item", Table: "item", PrimaryKey: "id"}}
|
||||
bp.Pages = []blueprint.Page{{ID: "list1", Type: "list", Layout: &blueprint.PageLayout{Actions: []string{"create", "refresh"}}}}
|
||||
_ = store.Save(context.Background(), &AppRecord{
|
||||
AppID: "a1", TenantID: 1, Slug: "coerce_fields", Name: "coerce",
|
||||
Status: StatusPublished, Blueprint: bp,
|
||||
})
|
||||
|
||||
dry, err := BackfillDefaultImportExport(context.Background(), store, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dry.Updated != 1 {
|
||||
t.Fatalf("dry updated=%d", dry.Updated)
|
||||
}
|
||||
app, _ := store.GetBySlug(context.Background(), 1, "coerce_fields")
|
||||
if hasImport(app.Blueprint.Apis.Resources[0].Operations) {
|
||||
t.Fatal("dry run should not persist")
|
||||
}
|
||||
|
||||
res, err := BackfillDefaultImportExport(context.Background(), store, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Updated != 1 {
|
||||
t.Fatalf("updated=%d", res.Updated)
|
||||
}
|
||||
app, _ = store.GetBySlug(context.Background(), 1, "coerce_fields")
|
||||
if !hasImport(app.Blueprint.Apis.Resources[0].Operations) {
|
||||
t.Fatalf("ops=%v", app.Blueprint.Apis.Resources[0].Operations)
|
||||
}
|
||||
}
|
||||
|
||||
func hasImport(ops []string) bool {
|
||||
for _, op := range ops {
|
||||
if op == "import" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -75,6 +75,29 @@ ORDER BY updated_at DESC`
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *PostgresStore) ListPublishedApps(ctx context.Context) ([]*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 status = $1
|
||||
ORDER BY tenant_id, slug`
|
||||
rows, err := s.DB.QueryContext(ctx, q, string(StatusPublished))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]*AppRecord, 0)
|
||||
for rows.Next() {
|
||||
rec, err := scanApp(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, rec)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *PostgresStore) Save(ctx context.Context, app *AppRecord) error {
|
||||
if app == nil {
|
||||
return fmt.Errorf("app is nil")
|
||||
|
||||
@@ -140,6 +140,19 @@ func (s *MemoryStore) ListByTenant(_ context.Context, tenantID int64) ([]AppSumm
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user