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
+363 -117
View File
@@ -56,28 +56,14 @@ async function seedStoreCategory(
path: string,
) {
return database.workspaceTransaction(workspaceId, async (tx) => {
const profile = await tx.query<{ target_store_id: string }>(
`SELECT target_store_id FROM atlas.product_platform_profiles
WHERE id = $1`,
[first.profileId],
);
const storeId = profile.rows[0]?.target_store_id;
if (!storeId) throw new Error('test profile store is missing');
const category = await tx.query<{ id: string }>(
`INSERT INTO atlas.store_product_categories
(workspace_id, store_id, platform_id, external_category_id, external_type_id, name, path)
VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, $4, $5, $6)
`INSERT INTO atlas.platform_product_categories
(platform_id, external_category_id, external_type_id, name, path)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
[
storeId,
platformId,
externalCategoryId,
externalTypeId,
name,
path,
],
[platformId, externalCategoryId, externalTypeId, name, path],
);
return { id: category.rows[0].id, storeId };
return { id: category.rows[0].id };
});
}
@@ -181,6 +167,40 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
await database.query('DELETE FROM atlas.workspaces WHERE id = $1', [
workspaceId,
]);
await database.query(
`DELETE FROM atlas.platform_catalog_sync_requests
WHERE requested_by_user_id = $1`,
[userId],
);
await database.query(
`DELETE FROM atlas.platform_catalog_sync_runs
WHERE requested_by_user_id = $1`,
[userId],
);
await database.query(
`DELETE FROM atlas.platform_catalog_resource_states
WHERE category_id = ANY($1::uuid[])`,
[seededCategoryIds],
);
await database.query(
`DELETE FROM atlas.platform_catalog_dictionary_values
WHERE platform_product_category_id = ANY($1::uuid[])`,
[seededCategoryIds],
);
await database.query(
`DELETE FROM atlas.platform_product_category_attributes
WHERE platform_product_category_id = ANY($1::uuid[])`,
[seededCategoryIds],
);
await database.query(
`DELETE FROM atlas.platform_product_categories WHERE id = ANY($1::uuid[])`,
[seededCategoryIds],
);
await database.query(
`DELETE FROM atlas.platform_catalog_sources
WHERE updated_by_user_id = $1 AND client_id = 'm35-test-client'`,
[userId],
);
await database.query('DELETE FROM atlas.users WHERE id = $1', [userId]);
await app.close();
});
@@ -879,46 +899,73 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
'箱包 › 背包 › M3.4 背包',
);
seededCategoryIds.push(categoryA.id, categoryB.id);
await database.workspaceTransaction(workspaceId, (tx) =>
tx.query(
`INSERT INTO atlas.store_product_category_attributes
(workspace_id, store_id, platform_id, store_product_category_id, platform_attribute_id, name,
const seededAttributes = await database.workspaceTransaction(
workspaceId,
(tx) =>
tx.query<{ id: string; platform_attribute_id: string }>(
`INSERT INTO atlas.platform_product_category_attributes
(platform_id, platform_product_category_id, platform_attribute_id, name,
value_type, is_required, is_collection, max_value_count, dictionary_id, is_aspect)
VALUES
(current_setting('app.workspace_id')::uuid, $1, $2, $3, 'material', '材质', 'dictionary', true, false, 1, $5, false),
(current_setting('app.workspace_id')::uuid, $1, $2, $3, 'color', '颜色', 'dictionary', true, false, 1, $6, true),
(current_setting('app.workspace_id')::uuid, $1, $2, $3, '85', '品牌', 'text', true, false, 1, NULL, false),
(current_setting('app.workspace_id')::uuid, $1, $2, $3, '9048', '型号名称', 'text', true, false, 1, NULL, false),
(current_setting('app.workspace_id')::uuid, $1, $2, $3, '8229', '类型', 'text', true, false, 1, NULL, false),
(current_setting('app.workspace_id')::uuid, $1, $2, $3, '23171', '主题标签', 'text', false, true, 10, NULL, false),
(current_setting('app.workspace_id')::uuid, $1, $2, $3, '11254', 'JSON 富内容', 'text', false, false, 1, NULL, false),
(current_setting('app.workspace_id')::uuid, $1, $2, $3, '4191', '简介', 'text', true, false, 1, NULL, false),
(current_setting('app.workspace_id')::uuid, $1, $2, $4, 'capacity', '容量', 'integer', false, false, 1, NULL, false)`,
($1, $2, 'material', '材质', 'dictionary', true, false, 1, $4, false),
($1, $2, 'color', '颜色', 'dictionary', true, false, 1, $5, true),
($1, $2, '85', '品牌', 'text', true, false, 1, NULL, false),
($1, $2, '9048', '型号名称', 'text', true, false, 1, NULL, false),
($1, $2, '8229', '类型', 'text', true, false, 1, NULL, false),
($1, $2, '23171', '主题标签', 'text', false, true, 10, NULL, false),
($1, $2, '11254', 'JSON 富内容', 'text', false, false, 1, NULL, false),
($1, $2, '4191', '简介', 'text', true, false, 1, NULL, false),
($1, $3, 'capacity', '容量', 'integer', false, false, 1, NULL, false)
RETURNING id, platform_attribute_id`,
[
platformId,
categoryA.id,
categoryB.id,
materialDictionaryId,
colorDictionaryId,
],
),
);
const materialAttributeId = seededAttributes.rows.find(
(item) => item.platform_attribute_id === 'material',
)!.id;
const colorAttributeId = seededAttributes.rows.find(
(item) => item.platform_attribute_id === 'color',
)!.id;
await database.workspaceTransaction(workspaceId, async (tx) => {
await tx.query(
`INSERT INTO atlas.platform_catalog_dictionary_values
(platform_id, platform_product_category_id, attribute_id, dictionary_id,
external_value_id, value)
VALUES ($1, $2, $3, $4, 'cotton', '棉'),
($1, $2, $5, $6, 'black', '黑色')`,
[
platformId,
categoryA.id,
materialAttributeId,
materialDictionaryId,
colorAttributeId,
colorDictionaryId,
],
);
await tx.query(
`INSERT INTO atlas.platform_catalog_resource_states
(platform_id, scope, scope_key, category_id, attribute_id,
last_success_at, is_complete)
VALUES
($1, 'attributes', 'attributes:' || $2::uuid, $2::uuid, NULL, now(), true),
($1, 'attributes', 'attributes:' || $3::uuid, $3::uuid, NULL, now(), true),
($1, 'dictionary', 'dictionary:' || $4::uuid, $2::uuid, $4::uuid, now(), true),
($1, 'dictionary', 'dictionary:' || $5::uuid, $2::uuid, $5::uuid, now(), true)`,
[
categoryA.storeId,
platformId,
categoryA.id,
categoryB.id,
materialDictionaryId,
colorDictionaryId,
materialAttributeId,
colorAttributeId,
],
),
);
await database.workspaceTransaction(workspaceId, (tx) =>
tx.query(
`INSERT INTO atlas.store_attribute_dictionary_values
(workspace_id, store_id, platform_id, store_product_category_id, dictionary_id, external_value_id, value)
VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, $4, 'cotton', '棉'),
(current_setting('app.workspace_id')::uuid, $1, $2, $3, $5, 'black', '黑色')`,
[
categoryA.storeId,
platformId,
categoryA.id,
materialDictionaryId,
colorDictionaryId,
],
),
);
);
});
const categories = await request(
`/api/product-platform-profiles/${first.profileId}/category-options?q=M3.4`,
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
@@ -968,7 +1015,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
title: before.body.data.profile.title,
description: before.body.data.profile.description,
tags: before.body.data.profile.tags,
store_product_category_id: categoryA.id,
platform_product_category_id: categoryA.id,
category_source: 'manual',
},
attributes: [
@@ -1024,56 +1071,56 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
);
assert.equal(schema.response.status, 200);
assert.equal(
schema.body.data.find(
schema.body.data.items.find(
(item: { platform_attribute_id: string }) =>
item.platform_attribute_id === 'color',
).is_aspect,
true,
);
assert.equal(
schema.body.data.find(
schema.body.data.items.find(
(item: { platform_attribute_id: string }) =>
item.platform_attribute_id === '85',
).editor_field,
'brand',
);
assert.equal(
schema.body.data.find(
schema.body.data.items.find(
(item: { platform_attribute_id: string }) =>
item.platform_attribute_id === '9048',
).editor_field,
'model',
);
assert.equal(
schema.body.data.find(
schema.body.data.items.find(
(item: { platform_attribute_id: string }) =>
item.platform_attribute_id === '8229',
).editor_field,
'category',
);
assert.equal(
schema.body.data.find(
schema.body.data.items.find(
(item: { platform_attribute_id: string }) =>
item.platform_attribute_id === '23171',
).editor_field,
'tags',
);
assert.equal(
schema.body.data.find(
schema.body.data.items.find(
(item: { platform_attribute_id: string }) =>
item.platform_attribute_id === '11254',
).editor_field,
'rich_content',
);
assert.equal(
schema.body.data.find(
schema.body.data.items.find(
(item: { platform_attribute_id: string }) =>
item.platform_attribute_id === '4191',
).editor_field,
'description',
);
const dictionary = await request(
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialDictionaryId}?category_id=${categoryA.id}`,
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialAttributeId}?category_id=${categoryA.id}`,
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
);
assert.deepEqual(dictionary.body.data.items, [
@@ -1081,29 +1128,34 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
]);
await database.workspaceTransaction(workspaceId, (tx) =>
tx.query(
`INSERT INTO atlas.store_attribute_dictionary_values
(workspace_id, store_id, platform_id, store_product_category_id,
`INSERT INTO atlas.platform_catalog_dictionary_values
(platform_id, platform_product_category_id, attribute_id,
dictionary_id, external_value_id, value)
SELECT current_setting('app.workspace_id')::uuid, $1, $2, $3, $4,
SELECT $1, $2, $3, $4,
'page-' || value::text, '测试选项' || lpad(value::text, 3, '0')
FROM generate_series(1, 205) AS series(value)`,
[categoryA.storeId, platformId, categoryA.id, materialDictionaryId],
[
platformId,
categoryA.id,
materialAttributeId,
materialDictionaryId,
],
),
);
const dictionaryPageOne = await request(
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialDictionaryId}?category_id=${categoryA.id}`,
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialAttributeId}?category_id=${categoryA.id}`,
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
);
assert.equal(dictionaryPageOne.body.data.items.length, 200);
assert.ok(dictionaryPageOne.body.data.next_cursor);
const dictionaryPageTwo = await request(
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialDictionaryId}?category_id=${categoryA.id}&cursor=${encodeURIComponent(dictionaryPageOne.body.data.next_cursor)}`,
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialAttributeId}?category_id=${categoryA.id}&cursor=${encodeURIComponent(dictionaryPageOne.body.data.next_cursor)}`,
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
);
assert.equal(dictionaryPageTwo.body.data.items.length, 6);
assert.equal(dictionaryPageTwo.body.data.next_cursor, null);
const invalidDictionaryCursor = await request(
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialDictionaryId}?category_id=${categoryA.id}&cursor=invalid`,
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialAttributeId}?category_id=${categoryA.id}&cursor=invalid`,
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
);
assert.equal(invalidDictionaryCursor.response.status, 400);
@@ -1254,7 +1306,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
afterSave.body.data.source_material_state.overridden_fields,
profile: {
...draft.profile,
store_product_category_id: categoryB.id,
platform_product_category_id: categoryB.id,
},
attributes: afterSave.body.data.attributes.map(
(attribute: any) => ({
@@ -1339,7 +1391,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
);
await tx.query(
`UPDATE atlas.product_platform_profiles
SET store_product_category_id = NULL, platform_type_id = NULL,
SET platform_product_category_id = NULL,
store_product_category_id = NULL, platform_type_id = NULL,
category_source = NULL
WHERE id = $1`,
[first.profileId],
@@ -1361,7 +1414,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
await database.workspaceTransaction(workspaceId, (tx) =>
tx.query(
`UPDATE atlas.product_platform_profiles
SET store_product_category_id = $1, platform_type_id = 'manual-type',
SET platform_product_category_id = $1, platform_type_id = 'manual-type',
category_source = 'manual'
WHERE id = $2`,
[manual.id, first.profileId],
@@ -1388,31 +1441,32 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
);
seededCategoryIds.push(category.id);
seededDictionaryIds.push(dictionaryId);
await database.workspaceTransaction(workspaceId, (tx) =>
tx.query(
`INSERT INTO atlas.store_product_category_attributes
(workspace_id, store_id, platform_id, store_product_category_id, platform_attribute_id, name,
value_type, is_required, is_collection, max_value_count, dictionary_id, is_aspect)
VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, 'm35-color', 'M3.5 颜色', 'dictionary', false, false, 1, $4, false)`,
[category.storeId, platformId, category.id, dictionaryId],
),
const dictionaryAttribute = await database.workspaceTransaction(
workspaceId,
(tx) =>
tx.query<{ id: string }>(
`INSERT INTO atlas.platform_product_category_attributes
(platform_id, platform_product_category_id, platform_attribute_id, name,
value_type, is_required, is_collection, max_value_count, dictionary_id, is_aspect)
VALUES ($1, $2, 'm35-color', 'M3.5 颜色', 'dictionary', false, false, 1, $3, false)
RETURNING id`,
[platformId, category.id, dictionaryId],
),
);
const dictionaryAttributeId = dictionaryAttribute.rows[0].id;
await database.workspaceTransaction(workspaceId, async (tx) => {
const profile = await tx.query<{ target_store_id: string }>(
'SELECT target_store_id FROM atlas.product_platform_profiles WHERE id = $1',
[first.profileId],
);
await tx.query(
`INSERT INTO atlas.store_secrets
(workspace_id, store_id, kind, secret_ciphertext, secret_nonce,
encryption_key_version, fingerprint, status)
VALUES (current_setting('app.workspace_id')::uuid, $1, 'seller_api_key',
$2, $3, 'test', $4, 'valid')`,
`INSERT INTO atlas.platform_catalog_sources
(platform_id, client_id, secret_ciphertext, secret_nonce,
secret_fingerprint, encryption_key_version, updated_by_user_id)
VALUES ($1, 'm35-test-client', $2, $3, $4, 'test', $5)
ON CONFLICT (platform_id) DO NOTHING`,
[
profile.rows[0].target_store_id,
platformId,
Buffer.from('m35-test-ciphertext'),
Buffer.from('m35-test-nonce'),
randomUUID(),
userId,
],
);
});
@@ -1427,12 +1481,9 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
},
);
assert.equal(schema.response.status, 202);
assert.equal(
schema.body.data.operation.kind,
'store_category_attribute_sync',
);
assert.equal(schema.body.data.run.scope, 'attributes');
const dictionary = await request(
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${dictionaryId}/syncs?category_id=${category.id}&q=%E9%BB%91%E8%89%B2`,
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${dictionaryAttributeId}/syncs?category_id=${category.id}&q=%E9%BB%91%E8%89%B2`,
{
method: 'POST',
headers: headers({
@@ -1442,12 +1493,9 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
},
);
assert.equal(dictionary.response.status, 202);
assert.equal(
dictionary.body.data.operation.kind,
'store_attribute_dictionary_sync',
);
assert.equal(dictionary.body.data.run.scope, 'dictionary_search');
const initialDictionary = await request(
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${dictionaryId}/syncs?category_id=${category.id}`,
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${dictionaryAttributeId}/syncs?category_id=${category.id}`,
{
method: 'POST',
headers: headers({
@@ -1459,34 +1507,231 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
assert.equal(initialDictionary.response.status, 202);
await database.workspaceTransaction(workspaceId, async (tx) => {
const events = await tx.query<{
event_type: string;
event_payload: unknown;
scope: string;
scope_key: string;
}>(
`SELECT event_type, event_payload FROM atlas.outbox_events
WHERE aggregate_id = $1 AND event_type LIKE 'store_%_sync.requested'
ORDER BY created_at`,
[first.profileId],
`SELECT run.scope, run.scope_key
FROM atlas.platform_catalog_outbox_events event
JOIN atlas.platform_catalog_sync_runs run ON run.id = event.run_id
WHERE run.requested_by_user_id = $1 AND run.category_id = $2
ORDER BY run.created_at`,
[userId, category.id],
);
assert.equal(events.rows.length, 3);
assert.deepEqual(
events.rows.map((event) => event.event_type).sort(),
[
'store_attribute_dictionary_sync.requested',
'store_attribute_dictionary_sync.requested',
'store_category_attribute_sync.requested',
],
);
assert.deepEqual(events.rows.map((event) => event.scope).sort(), [
'attributes',
'dictionary',
'dictionary_search',
]);
assert.ok(
events.rows.every(
(event) =>
!JSON.stringify(event.event_payload).includes(
'm35-test-ciphertext',
),
!JSON.stringify(event).includes('m35-test-ciphertext'),
),
);
});
});
it('发布预检只刷新当前类型和实际使用的字典,并按过期状态提升优先级', async () => {
const category = await seedStoreCategory(
`publish-preflight-${randomUUID()}`,
'publish-preflight-type',
'发布预检类目',
'测试 › 发布预检类目',
);
seededCategoryIds.push(category.id);
const definitions = await database.workspaceTransaction(
workspaceId,
(tx) =>
tx.query<{ id: string; platform_attribute_id: string }>(
`INSERT INTO atlas.platform_product_category_attributes
(platform_id, platform_product_category_id,
platform_attribute_id, name, value_type, is_required,
is_collection, max_value_count, dictionary_id, is_aspect)
VALUES
($1, $2, 'publish-used', '已用字典', 'dictionary', false,
false, 1, $3, false),
($1, $2, 'publish-unused', '未用字典', 'dictionary', false,
false, 1, $4, false)
RETURNING id, platform_attribute_id`,
[
platformId,
category.id,
`publish-used-${randomUUID()}`,
`publish-unused-${randomUUID()}`,
],
),
);
const usedAttributeId = definitions.rows.find(
(item) => item.platform_attribute_id === 'publish-used',
)!.id;
const unusedAttributeId = definitions.rows.find(
(item) => item.platform_attribute_id === 'publish-unused',
)!.id;
const editor = await request(
`/api/products/${first.productId}/editor`,
{
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
},
);
const variantId = editor.body.data.variants.find(
(item: { id: string; listing: { status: string } }) =>
['not_listed', 'failed'].includes(item.listing.status),
).id as string;
await database.workspaceTransaction(workspaceId, async (tx) => {
await tx.query(
`UPDATE atlas.product_platform_profiles
SET platform_product_category_id = $1,
platform_type_id = 'publish-preflight-type',
category_source = 'manual'
WHERE id = $2`,
[category.id, first.profileId],
);
await tx.query(
`DELETE FROM atlas.product_attributes WHERE profile_id = $1`,
[first.profileId],
);
await tx.query(
`INSERT INTO atlas.product_attributes
(workspace_id, product_id, profile_id, product_variant_id,
scope, platform_attribute_id, complex_id, values, source,
confirmation_status)
VALUES (current_setting('app.workspace_id')::uuid, $1, $2,
NULL, 'common', 'publish-used', '0', '["used-value"]'::jsonb,
'manual', 'confirmed')`,
[first.productId, first.profileId],
);
await tx.query(
`INSERT INTO atlas.platform_catalog_dictionary_values
(platform_id, platform_product_category_id, attribute_id,
dictionary_id, external_value_id, value)
SELECT $1, $2, id, dictionary_id, 'used-value', '已用候选值'
FROM atlas.platform_product_category_attributes WHERE id = $3`,
[platformId, category.id, usedAttributeId],
);
await tx.query(
`INSERT INTO atlas.platform_catalog_resource_states
(platform_id, scope, scope_key, category_id, attribute_id,
last_success_at, is_complete)
VALUES
($1, 'attributes', 'attributes:' || $2::uuid, $2, NULL,
now() - interval '15 days', true),
($1, 'dictionary', 'dictionary:' || $3::uuid, $2, $3,
now() - interval '15 days', true)`,
[platformId, category.id, usedAttributeId],
);
});
const stale = await request(
`/api/product-platform-profiles/${first.profileId}/publish-preflights`,
{
method: 'POST',
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
body: JSON.stringify({ variant_ids: [variantId] }),
},
);
assert.equal(stale.response.status, 200);
assert.equal(stale.body.data.catalog.schema.status, 'stale');
assert.deepEqual(
stale.body.data.catalog.dictionaries.map(
(item: { attribute_id: string; cache: { status: string } }) => [
item.attribute_id,
item.cache.status,
],
),
[[usedAttributeId, 'stale']],
);
assert.equal(stale.body.data.refresh_runs.length, 2);
assert.ok(
stale.body.data.refresh_runs.every(
(run: { priority: string; trigger_kind: string }) =>
run.priority === 'low' && run.trigger_kind === 'publish',
),
);
await database.query(
`UPDATE atlas.platform_catalog_dictionary_values
SET is_active = false WHERE attribute_id = $1`,
[usedAttributeId],
);
const invalidValue = await request(
`/api/product-platform-profiles/${first.profileId}/validation`,
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
);
assert.ok(
invalidValue.body.data.profile.issues.some(
(issue: { code: string }) =>
issue.code === 'ATTRIBUTE_DICTIONARY_VALUE_INVALID',
),
);
await database.query(
`UPDATE atlas.platform_catalog_dictionary_values
SET is_active = true WHERE attribute_id = $1`,
[usedAttributeId],
);
await database.query(
`UPDATE atlas.platform_catalog_resource_states
SET last_success_at = now() - interval '22 days'
WHERE platform_id = $1
AND scope_key = 'dictionary:' || $2::uuid`,
[platformId, usedAttributeId],
);
const expired = await request(
`/api/product-platform-profiles/${first.profileId}/publish-preflights`,
{
method: 'POST',
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
body: JSON.stringify({ variant_ids: [variantId] }),
},
);
assert.equal(expired.response.status, 202);
assert.equal(expired.body.data.status, 'refreshing');
assert.equal(expired.body.data.catalog.schema.status, 'stale');
assert.equal(
expired.body.data.catalog.dictionaries[0].cache.status,
'expired',
);
await database.query(
`UPDATE atlas.platform_catalog_resource_states
SET last_success_at = now() - interval '22 days'
WHERE platform_id = $1
AND scope_key = 'attributes:' || $2::uuid`,
[platformId, category.id],
);
const expiredSchema = await request(
`/api/product-platform-profiles/${first.profileId}/publish-preflights`,
{
method: 'POST',
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
body: JSON.stringify({ variant_ids: [variantId] }),
},
);
assert.equal(expiredSchema.response.status, 202);
assert.equal(expiredSchema.body.data.catalog.schema.status, 'expired');
assert.deepEqual(expiredSchema.body.data.catalog.dictionaries, []);
const runs = await database.query<{
scope_key: string;
priority: string;
}>(
`SELECT DISTINCT ON (scope_key) scope_key, priority
FROM atlas.platform_catalog_sync_runs
WHERE platform_id = $1
AND scope_key IN ('attributes:' || $2::uuid,
'dictionary:' || $3::uuid,
'dictionary:' || $4::uuid)
ORDER BY scope_key, created_at DESC`,
[platformId, category.id, usedAttributeId, unusedAttributeId],
);
assert.equal(runs.rows.length, 2);
assert.ok(runs.rows.every((run) => run.priority === 'high'));
assert.ok(
!runs.rows.some(
(run) => run.scope_key === `dictionary:${unusedAttributeId}`,
),
);
}, 20_000);
it('M3.6 仅允许当前 SKU 已转存候选作为有序正式素材', async () => {
const before = await request(
`/api/products/${first.productId}/editor`,
@@ -1550,7 +1795,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
title: before.body.data.profile.title,
description: before.body.data.profile.description,
tags: before.body.data.profile.tags,
store_product_category_id: before.body.data.profile.category.id,
platform_product_category_id:
before.body.data.profile.category.id,
category_source: before.body.data.profile.category.source,
},
attributes: before.body.data.attributes.map((attribute: any) => ({