feat(category): 类目树、类目属性、属性字典维护系统构建完成

This commit is contained in:
R524809
2026-09-20 15:11:32 +08:00
parent 388429dbd0
commit 880e441bc6
48 changed files with 5127 additions and 1159 deletions
@@ -0,0 +1,135 @@
SET search_path TO atlas, public;
-- 平台目录刷新阈值独立于门店和工作空间;修改策略不主动创建同步任务。
CREATE TABLE platform_catalog_refresh_policies (
platform_id UUID PRIMARY KEY REFERENCES platforms(id) ON DELETE CASCADE,
attribute_fresh_days INTEGER NOT NULL DEFAULT 14 CHECK (attribute_fresh_days BETWEEN 1 AND 365),
attribute_expire_days INTEGER NOT NULL DEFAULT 21 CHECK (attribute_expire_days BETWEEN 1 AND 365),
dictionary_fresh_days INTEGER NOT NULL DEFAULT 14 CHECK (dictionary_fresh_days BETWEEN 1 AND 365),
dictionary_expire_days INTEGER NOT NULL DEFAULT 21 CHECK (dictionary_expire_days BETWEEN 1 AND 365),
updated_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (attribute_expire_days > attribute_fresh_days),
CHECK (dictionary_expire_days > dictionary_fresh_days)
);
INSERT INTO platform_catalog_refresh_policies (platform_id)
SELECT id FROM platforms
ON CONFLICT (platform_id) DO NOTHING;
-- 成功检查时间属于资源状态,不能从数据行 updated_at 推断。
CREATE TABLE platform_catalog_resource_states (
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', 'dictionary_search')),
scope_key TEXT NOT NULL,
category_id UUID,
attribute_id UUID,
search_key TEXT,
last_success_at TIMESTAMPTZ,
last_success_run_id UUID REFERENCES platform_catalog_sync_runs(id) ON DELETE SET NULL,
is_complete BOOLEAN NOT NULL DEFAULT false,
last_error_code TEXT,
last_error_message TEXT,
last_error_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (platform_id, scope_key),
FOREIGN KEY (platform_id, category_id)
REFERENCES platform_product_categories(platform_id, id) ON DELETE CASCADE,
FOREIGN KEY (attribute_id, platform_id, category_id)
REFERENCES platform_product_category_attributes(id, platform_id, platform_product_category_id)
ON DELETE CASCADE,
CHECK (
(scope = 'tree' AND category_id IS NULL AND attribute_id IS NULL AND search_key IS NULL)
OR (scope = 'attributes' AND category_id IS NOT NULL AND attribute_id IS NULL AND search_key IS NULL)
OR (scope = 'dictionary' AND category_id IS NOT NULL AND attribute_id IS NOT NULL AND search_key IS NULL)
OR (scope = 'dictionary_search' AND category_id IS NOT NULL AND attribute_id IS NOT NULL AND search_key IS NOT NULL)
)
);
CREATE INDEX ix_platform_catalog_resource_states_status
ON platform_catalog_resource_states (platform_id, scope, last_success_at);
-- 保留旧 scheduled 触发值用于历史任务;新任务细分每周、批次和交互来源。
ALTER TABLE platform_catalog_sync_runs
DROP CONSTRAINT platform_catalog_sync_runs_trigger_kind_check;
ALTER TABLE platform_catalog_sync_runs
ADD CONSTRAINT platform_catalog_sync_runs_trigger_kind_check
CHECK (trigger_kind IN (
'manual', 'scheduled', 'weekly', 'source_configured',
'admin_batch', 'user_access', 'publish'
));
ALTER TABLE platform_catalog_sync_runs
ADD COLUMN priority TEXT NOT NULL DEFAULT 'low'
CHECK (priority IN ('high', 'low')),
ADD COLUMN promoted_at TIMESTAMPTZ;
-- 管理员批次固定启动瞬间的类型集合,后续树变化不改变既有批次范围。
CREATE TABLE platform_catalog_attribute_batches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
platform_id UUID NOT NULL REFERENCES platforms(id) ON DELETE CASCADE,
mode TEXT NOT NULL CHECK (mode IN ('missing_expired', 'missing_stale_expired', 'all')),
status operation_status NOT NULL DEFAULT 'queued',
source_revision INTEGER NOT NULL CHECK (source_revision > 0),
attribute_fresh_days INTEGER NOT NULL CHECK (attribute_fresh_days BETWEEN 1 AND 365),
attribute_expire_days INTEGER NOT NULL CHECK (attribute_expire_days BETWEEN 1 AND 365),
requested_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
idempotency_key UUID NOT NULL,
total_count INTEGER NOT NULL CHECK (total_count > 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
UNIQUE (platform_id, id),
UNIQUE (requested_by_user_id, idempotency_key),
CHECK (attribute_expire_days > attribute_fresh_days)
);
CREATE UNIQUE INDEX uq_platform_catalog_active_attribute_batch
ON platform_catalog_attribute_batches (platform_id)
WHERE status IN ('queued', 'running');
CREATE INDEX ix_platform_catalog_attribute_batches_recent
ON platform_catalog_attribute_batches (platform_id, created_at DESC);
CREATE TABLE platform_catalog_attribute_batch_items (
batch_id UUID NOT NULL,
platform_id UUID NOT NULL,
category_id UUID NOT NULL,
run_id UUID REFERENCES platform_catalog_sync_runs(id) ON DELETE RESTRICT,
linked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (batch_id, category_id),
FOREIGN KEY (platform_id, batch_id)
REFERENCES platform_catalog_attribute_batches(platform_id, id) ON DELETE CASCADE,
FOREIGN KEY (platform_id, category_id)
REFERENCES platform_product_categories(platform_id, id) ON DELETE RESTRICT,
CHECK ((run_id IS NULL) = (linked_at IS NULL))
);
CREATE INDEX ix_platform_catalog_attribute_batch_items_pending
ON platform_catalog_attribute_batch_items (batch_id, created_at, category_id)
WHERE run_id IS NULL;
CREATE INDEX ix_platform_catalog_attribute_batch_items_run
ON platform_catalog_attribute_batch_items (run_id)
WHERE run_id IS NOT NULL;
-- 现有成功任务建立初始状态,避免部署后把已缓存资源误判为 missing。
WITH latest AS (
SELECT DISTINCT ON (run.platform_id, run.scope_key)
run.platform_id, run.scope, run.scope_key, run.category_id, run.attribute_id,
run.id AS run_id, run.completed_at, run.result
FROM platform_catalog_sync_runs run
WHERE run.status = 'succeeded' AND run.completed_at IS NOT NULL
ORDER BY run.platform_id, run.scope_key, run.completed_at DESC, run.id DESC
)
INSERT INTO platform_catalog_resource_states
(platform_id, scope, scope_key, category_id, attribute_id,
last_success_at, last_success_run_id, is_complete)
SELECT platform_id, scope, scope_key, category_id, attribute_id,
completed_at, run_id,
CASE WHEN scope IN ('tree', 'attributes') THEN true
ELSE COALESCE((result->>'is_complete')::boolean, false) END
FROM latest
ON CONFLICT (platform_id, scope_key) DO UPDATE
SET last_success_at = EXCLUDED.last_success_at,
last_success_run_id = EXCLUDED.last_success_run_id,
is_complete = EXCLUDED.is_complete,
updated_at = now();
@@ -0,0 +1,14 @@
SET search_path TO atlas, public;
-- 阈值调整会改变缓存可用性判断,保留不可变的前后值用于系统级审计。
CREATE TABLE platform_catalog_refresh_policy_revisions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
platform_id UUID NOT NULL REFERENCES platforms(id) ON DELETE CASCADE,
changed_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
previous_policy JSONB NOT NULL,
next_policy JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_platform_catalog_refresh_policy_revisions_recent
ON platform_catalog_refresh_policy_revisions (platform_id, created_at DESC);
@@ -0,0 +1,46 @@
SET search_path TO atlas, public;
-- 商品编辑器恢复使用平台级类目引用;门店级列暂时保留用于旧客户端兼容和回滚。
UPDATE product_platform_profiles profile
SET platform_product_category_id = platform_category.id,
platform_type_id = platform_category.external_type_id,
updated_at = now()
FROM store_product_categories store_category
JOIN platform_product_categories platform_category
ON platform_category.platform_id = store_category.platform_id
AND platform_category.external_category_id = store_category.external_category_id
AND platform_category.external_type_id IS NOT DISTINCT FROM store_category.external_type_id
WHERE profile.store_product_category_id = store_category.id
AND profile.platform_id = platform_category.platform_id
AND profile.platform_product_category_id IS NULL;
UPDATE product_unmapped_attributes unmapped
SET original_platform_category_id = platform_category.id
FROM store_product_categories store_category
JOIN platform_product_categories platform_category
ON platform_category.platform_id = store_category.platform_id
AND platform_category.external_category_id = store_category.external_category_id
AND platform_category.external_type_id IS NOT DISTINCT FROM store_category.external_type_id
WHERE unmapped.original_store_product_category_id = store_category.id
AND unmapped.platform_id = platform_category.platform_id
AND unmapped.original_platform_category_id IS NULL;
-- 部分大字典的关键词补齐是独立缓存作用域,不能刷新整个字典的新鲜时间。
ALTER TABLE platform_catalog_sync_runs
ADD COLUMN search_key TEXT;
ALTER TABLE platform_catalog_sync_runs
DROP CONSTRAINT platform_catalog_sync_runs_scope_check,
DROP CONSTRAINT platform_catalog_sync_runs_check;
ALTER TABLE platform_catalog_sync_runs
ADD CONSTRAINT platform_catalog_sync_runs_scope_check
CHECK (scope IN ('tree', 'attributes', 'dictionary', 'dictionary_search')),
ADD CONSTRAINT platform_catalog_sync_runs_target_check CHECK (
(scope = 'tree' AND category_id IS NULL AND attribute_id IS NULL AND search_key IS NULL)
OR (scope = 'attributes' AND category_id IS NOT NULL AND attribute_id IS NULL AND search_key IS NULL)
OR (scope = 'dictionary' AND category_id IS NOT NULL AND attribute_id IS NOT NULL AND search_key IS NULL)
OR (scope = 'dictionary_search' AND category_id IS NOT NULL AND attribute_id IS NOT NULL
AND length(trim(search_key)) >= 2)
);