Files
ai_site/platform/internal/meta/postgres.go
whm cb56e6847e 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.
2026-08-05 11:47:20 +08:00

230 lines
5.7 KiB
Go

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) 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")
}
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"
}