feat(category): 类目树、属性、属性字典维护,新增系统管理员管理类目数据
This commit is contained in:
@@ -65,6 +65,14 @@ export type ProductAttributeSchema = {
|
||||
max_value_count: number;
|
||||
dictionary_id: string | null;
|
||||
is_aspect: boolean;
|
||||
editor_field:
|
||||
| 'category'
|
||||
| 'brand'
|
||||
| 'model'
|
||||
| 'tags'
|
||||
| 'rich_content'
|
||||
| 'description'
|
||||
| null;
|
||||
};
|
||||
|
||||
export type ProductAttributeDictionaryValue = {
|
||||
@@ -72,6 +80,28 @@ export type ProductAttributeDictionaryValue = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type ProductEditorValidationIssue = {
|
||||
code: string;
|
||||
field: string;
|
||||
message: string;
|
||||
platform_attribute_id?: string;
|
||||
};
|
||||
|
||||
export type ProductEditorValidation = {
|
||||
ready: boolean;
|
||||
profile: {
|
||||
ready: boolean;
|
||||
issues: ProductEditorValidationIssue[];
|
||||
};
|
||||
variants: Array<{
|
||||
variant_id: string;
|
||||
display_name: string | null;
|
||||
status: 'ready' | 'blocked' | 'not_publishable_again';
|
||||
ready: boolean;
|
||||
issues: ProductEditorValidationIssue[];
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* M3.0 editor entry and read-model contracts. Draft mutation, pricing, and
|
||||
* publishing DTOs intentionally stay out of this first editor slice.
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
SET search_path TO atlas, public;
|
||||
|
||||
-- 全局管理员授权独立于工作空间成员身份;日常应用角色只能读取授权。
|
||||
CREATE TABLE system_admin_grants (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
granted_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
revoked_at TIMESTAMPTZ
|
||||
);
|
||||
REVOKE INSERT, UPDATE, DELETE ON system_admin_grants FROM atlas_app;
|
||||
|
||||
-- 每个平台只配置一组受控参考源凭据,不挂载到客户门店。
|
||||
CREATE TABLE platform_catalog_sources (
|
||||
platform_id UUID PRIMARY KEY REFERENCES platforms(id) ON DELETE CASCADE,
|
||||
client_id TEXT NOT NULL CHECK (length(trim(client_id)) BETWEEN 1 AND 128),
|
||||
secret_ciphertext BYTEA NOT NULL,
|
||||
secret_nonce BYTEA NOT NULL,
|
||||
secret_fingerprint TEXT NOT NULL,
|
||||
encryption_key_version TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL DEFAULT 1 CHECK (revision > 0),
|
||||
updated_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 复用既有平台类目和属性定义表;补齐管理页面及同步所需的属性元数据。
|
||||
ALTER TABLE platform_product_category_attributes
|
||||
ADD COLUMN description TEXT,
|
||||
ADD COLUMN group_id TEXT,
|
||||
ADD COLUMN group_name TEXT,
|
||||
ADD COLUMN category_dependent BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD CONSTRAINT uq_platform_catalog_attribute_scope
|
||||
UNIQUE (id, platform_id, platform_product_category_id);
|
||||
|
||||
-- 旧字典表仅以 dictionary_id 唯一,无法表达类目相关候选值;新表明确归属属性。
|
||||
CREATE TABLE platform_catalog_dictionary_values (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
platform_id UUID NOT NULL,
|
||||
platform_product_category_id UUID NOT NULL,
|
||||
attribute_id UUID NOT NULL,
|
||||
dictionary_id TEXT NOT NULL,
|
||||
external_value_id TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
info TEXT,
|
||||
picture TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
source_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
FOREIGN KEY (attribute_id, platform_id, platform_product_category_id)
|
||||
REFERENCES platform_product_category_attributes(id, platform_id, platform_product_category_id)
|
||||
ON DELETE RESTRICT,
|
||||
UNIQUE (attribute_id, external_value_id)
|
||||
);
|
||||
CREATE INDEX ix_platform_catalog_dictionary_values_lookup
|
||||
ON platform_catalog_dictionary_values (attribute_id, is_active, value, external_value_id);
|
||||
|
||||
-- 平台任务不能借用带 workspace_id 的租户 operation/outbox。
|
||||
CREATE TABLE platform_catalog_sync_runs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
platform_id UUID NOT NULL REFERENCES platforms(id) ON DELETE CASCADE,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('tree', 'attributes', 'dictionary')),
|
||||
scope_key TEXT NOT NULL,
|
||||
category_id UUID REFERENCES platform_product_categories(id) ON DELETE RESTRICT,
|
||||
attribute_id UUID REFERENCES platform_product_category_attributes(id) ON DELETE RESTRICT,
|
||||
trigger_kind TEXT NOT NULL CHECK (trigger_kind IN ('manual', 'scheduled', 'source_configured')),
|
||||
status operation_status NOT NULL DEFAULT 'queued',
|
||||
source_revision INTEGER NOT NULL CHECK (source_revision > 0),
|
||||
requested_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
idempotency_key UUID,
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
||||
result JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
error_code TEXT,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
CHECK (
|
||||
(scope = 'tree' AND category_id IS NULL AND attribute_id IS NULL)
|
||||
OR (scope = 'attributes' AND category_id IS NOT NULL AND attribute_id IS NULL)
|
||||
OR (scope = 'dictionary' AND category_id IS NOT NULL AND attribute_id IS NOT NULL)
|
||||
),
|
||||
CHECK (idempotency_key IS NULL OR requested_by_user_id IS NOT NULL),
|
||||
UNIQUE (requested_by_user_id, idempotency_key)
|
||||
);
|
||||
CREATE UNIQUE INDEX uq_platform_catalog_active_sync
|
||||
ON platform_catalog_sync_runs (platform_id, scope_key)
|
||||
WHERE status IN ('queued', 'running');
|
||||
CREATE INDEX ix_platform_catalog_sync_recent
|
||||
ON platform_catalog_sync_runs (platform_id, scope, scope_key, created_at DESC);
|
||||
|
||||
-- 一个手动请求可复用已有的定时任务;幂等键因此独立映射到任务,而不是只放在任务行。
|
||||
CREATE TABLE platform_catalog_sync_requests (
|
||||
requested_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
idempotency_key UUID NOT NULL,
|
||||
platform_id UUID NOT NULL REFERENCES platforms(id) ON DELETE CASCADE,
|
||||
scope_key TEXT NOT NULL,
|
||||
run_id UUID NOT NULL REFERENCES platform_catalog_sync_runs(id) ON DELETE RESTRICT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (requested_by_user_id, idempotency_key)
|
||||
);
|
||||
CREATE INDEX ix_platform_catalog_sync_requests_run
|
||||
ON platform_catalog_sync_requests (run_id);
|
||||
|
||||
CREATE TABLE platform_catalog_outbox_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
run_id UUID NOT NULL UNIQUE REFERENCES platform_catalog_sync_runs(id) ON DELETE CASCADE,
|
||||
platform_id UUID NOT NULL REFERENCES platforms(id) ON DELETE CASCADE,
|
||||
status outbox_event_status NOT NULL DEFAULT 'pending',
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
||||
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
locked_by TEXT,
|
||||
locked_until TIMESTAMPTZ,
|
||||
dispatched_at TIMESTAMPTZ,
|
||||
last_error_code TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (
|
||||
(status = 'dispatching' AND locked_by IS NOT NULL AND locked_until IS NOT NULL)
|
||||
OR (status <> 'dispatching' AND locked_by IS NULL AND locked_until IS NULL)
|
||||
)
|
||||
);
|
||||
CREATE INDEX ix_platform_catalog_outbox_dispatch
|
||||
ON platform_catalog_outbox_events (status, next_attempt_at, created_at)
|
||||
WHERE status IN ('pending', 'dispatching');
|
||||
Reference in New Issue
Block a user