feat(category): 类目树、类目属性、属性字典维护系统构建完成
This commit is contained in:
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user