feat(category): 类目树、属性、属性字典维护,新增系统管理员管理类目数据

This commit is contained in:
R524809
2026-09-18 18:55:16 +08:00
parent fcddb11da0
commit 388429dbd0
58 changed files with 4347 additions and 640 deletions
+30
View File
@@ -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');