feat(edit): 编辑页类目树逻辑开发
This commit is contained in:
@@ -49,6 +49,31 @@ async function createCollectionItem(title: string) {
|
||||
};
|
||||
}
|
||||
|
||||
async function seedStoreCategory(
|
||||
externalCategoryId: string,
|
||||
externalTypeId: string,
|
||||
name: string,
|
||||
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)
|
||||
RETURNING id`,
|
||||
[storeId, platformId, externalCategoryId, externalTypeId, name, path],
|
||||
);
|
||||
return { id: category.rows[0].id, storeId };
|
||||
});
|
||||
}
|
||||
|
||||
describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
beforeAll(async () => {
|
||||
Object.assign(process.env, {
|
||||
@@ -117,6 +142,10 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
'DELETE FROM atlas.product_platform_profiles WHERE workspace_id = $1',
|
||||
[workspaceId],
|
||||
);
|
||||
await tx.query(
|
||||
'DELETE FROM atlas.store_product_categories WHERE workspace_id = $1',
|
||||
[workspaceId],
|
||||
);
|
||||
await tx.query(
|
||||
'DELETE FROM atlas.products WHERE workspace_id = $1',
|
||||
[workspaceId],
|
||||
@@ -137,19 +166,6 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
workspaceId,
|
||||
]);
|
||||
await database.query('DELETE FROM atlas.users WHERE id = $1', [userId]);
|
||||
if (seededDictionaryIds.length) {
|
||||
await database.query(
|
||||
`DELETE FROM atlas.platform_attribute_dictionary_values
|
||||
WHERE platform_id = $1 AND dictionary_id = ANY($2::text[])`,
|
||||
[platformId, seededDictionaryIds],
|
||||
);
|
||||
}
|
||||
if (seededCategoryIds.length) {
|
||||
await database.query(
|
||||
'DELETE FROM atlas.platform_product_categories WHERE id = ANY($1::uuid[])',
|
||||
[seededCategoryIds],
|
||||
);
|
||||
}
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -834,44 +850,33 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
const materialDictionaryId = `m34-material-${dictionarySuffix}`;
|
||||
const colorDictionaryId = `m34-color-${dictionarySuffix}`;
|
||||
seededDictionaryIds.push(materialDictionaryId, colorDictionaryId);
|
||||
const categoryA = await database.query<{ id: string }>(
|
||||
`INSERT INTO atlas.platform_product_categories
|
||||
(platform_id, external_category_id, external_type_id, name, path)
|
||||
VALUES ($1, $2, 'type-a', 'M3.4 收纳箱', '家居 › 收纳 › M3.4 收纳箱')
|
||||
RETURNING id`,
|
||||
[platformId, `m34-a-${randomUUID()}`],
|
||||
);
|
||||
const categoryB = await database.query<{ id: string }>(
|
||||
`INSERT INTO atlas.platform_product_categories
|
||||
(platform_id, external_category_id, external_type_id, name, path)
|
||||
VALUES ($1, $2, 'type-b', 'M3.4 背包', '箱包 › 背包 › M3.4 背包')
|
||||
RETURNING id`,
|
||||
[platformId, `m34-b-${randomUUID()}`],
|
||||
);
|
||||
seededCategoryIds.push(categoryA.rows[0].id, categoryB.rows[0].id);
|
||||
await database.query(
|
||||
`INSERT INTO atlas.platform_product_category_attributes
|
||||
(platform_id, platform_product_category_id, platform_attribute_id, name,
|
||||
const categoryA = await seedStoreCategory(`m34-a-${randomUUID()}`, 'type-a', 'M3.4 收纳箱', '家居 › 收纳 › M3.4 收纳箱');
|
||||
const categoryB = await seedStoreCategory(`m34-b-${randomUUID()}`, 'type-b', 'M3.4 背包', '箱包 › 背包 › 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,
|
||||
value_type, is_required, is_collection, max_value_count, dictionary_id, is_aspect)
|
||||
VALUES
|
||||
($1, $2, 'material', '材质', 'dictionary', true, false, 1, $4, false),
|
||||
($1, $2, 'color', '颜色', 'dictionary', true, false, 1, $5, true),
|
||||
($1, $3, 'capacity', '容量', 'integer', false, false, 1, NULL, false)`,
|
||||
(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, $4, 'capacity', '容量', 'integer', false, false, 1, NULL, false)`,
|
||||
[
|
||||
categoryA.storeId,
|
||||
platformId,
|
||||
categoryA.rows[0].id,
|
||||
categoryB.rows[0].id,
|
||||
categoryA.id,
|
||||
categoryB.id,
|
||||
materialDictionaryId,
|
||||
colorDictionaryId,
|
||||
],
|
||||
);
|
||||
await database.query(
|
||||
`INSERT INTO atlas.platform_attribute_dictionary_values
|
||||
(platform_id, dictionary_id, external_value_id, value)
|
||||
VALUES ($1, $2, 'cotton', '棉'),
|
||||
($1, $3, 'black', '黑色')`,
|
||||
[platformId, materialDictionaryId, colorDictionaryId],
|
||||
);
|
||||
));
|
||||
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 }) },
|
||||
@@ -879,7 +884,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
assert.equal(categories.response.status, 200);
|
||||
assert.ok(
|
||||
categories.body.data.some(
|
||||
(item: { id: string }) => item.id === categoryA.rows[0].id,
|
||||
(item: { id: string }) => item.id === categoryA.id,
|
||||
),
|
||||
);
|
||||
const before = await request(
|
||||
@@ -917,7 +922,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,
|
||||
platform_product_category_id: categoryA.rows[0].id,
|
||||
store_product_category_id: categoryA.id,
|
||||
category_source: 'manual',
|
||||
},
|
||||
attributes: [
|
||||
@@ -958,7 +963,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
);
|
||||
assert.equal(saved.response.status, 200);
|
||||
const schema = await request(
|
||||
`/api/product-platform-profiles/${first.profileId}/attribute-schema?category_id=${categoryA.rows[0].id}`,
|
||||
`/api/product-platform-profiles/${first.profileId}/attribute-schema?category_id=${categoryA.id}`,
|
||||
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
|
||||
);
|
||||
assert.equal(schema.response.status, 200);
|
||||
@@ -970,7 +975,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
true,
|
||||
);
|
||||
const dictionary = await request(
|
||||
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialDictionaryId}?category_id=${categoryA.rows[0].id}`,
|
||||
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${materialDictionaryId}?category_id=${categoryA.id}`,
|
||||
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
|
||||
);
|
||||
assert.deepEqual(dictionary.body.data.items, [
|
||||
@@ -984,7 +989,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
);
|
||||
assert.equal(
|
||||
afterSave.body.data.profile.category.id,
|
||||
categoryA.rows[0].id,
|
||||
categoryA.id,
|
||||
);
|
||||
assert.equal(afterSave.body.data.profile.category.name, 'M3.4 收纳箱');
|
||||
assert.equal(afterSave.body.data.attributes.length, 2);
|
||||
@@ -996,7 +1001,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
afterSave.body.data.source_material_state.overridden_fields,
|
||||
profile: {
|
||||
...draft.profile,
|
||||
platform_product_category_id: categoryB.rows[0].id,
|
||||
store_product_category_id: categoryB.id,
|
||||
},
|
||||
attributes: afterSave.body.data.attributes.map(
|
||||
(attribute: any) => ({
|
||||
@@ -1045,24 +1050,83 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('M3.5 为类目树、类目属性和关键词字典搜索创建无密钥 outbox 任务', async () => {
|
||||
it('M3.5 按 Ozon 采集层级回填空类目,且绝不覆盖已保存选择', async () => {
|
||||
const suffix = randomUUID();
|
||||
const captured = await seedStoreCategory(`m35-source-${suffix}`, 'source-type', 'M3.5 采集叶', 'M3.5 来源 › M3.5 采集叶');
|
||||
const manual = await seedStoreCategory(`m35-manual-${suffix}`, 'manual-type', 'M3.5 手动叶', 'M3.5 手动 › M3.5 手动叶');
|
||||
seededCategoryIds.push(captured.id, manual.id);
|
||||
await database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
await tx.query(
|
||||
`UPDATE atlas.collection_items
|
||||
SET source_platform_id = $1,
|
||||
normalized_payload = $2::jsonb
|
||||
WHERE id = $3`,
|
||||
[
|
||||
platformId,
|
||||
JSON.stringify({
|
||||
common: {
|
||||
categories: [
|
||||
{ key: 'source-root', name: ' M3.5 来源 ' },
|
||||
{ key: 'source-leaf', name: 'M3.5 采集叶' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
first.collectionItemId,
|
||||
],
|
||||
);
|
||||
await tx.query(
|
||||
`UPDATE atlas.product_platform_profiles
|
||||
SET store_product_category_id = NULL, platform_type_id = NULL,
|
||||
category_source = NULL
|
||||
WHERE id = $1`,
|
||||
[first.profileId],
|
||||
);
|
||||
});
|
||||
const resolved = await request(`/api/products/${first.productId}/editor`, {
|
||||
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
|
||||
});
|
||||
assert.equal(resolved.response.status, 200);
|
||||
assert.equal(
|
||||
resolved.body.data.profile.category.id,
|
||||
captured.id,
|
||||
);
|
||||
assert.equal(
|
||||
resolved.body.data.profile.category.source,
|
||||
'source_capture',
|
||||
);
|
||||
|
||||
await database.workspaceTransaction(workspaceId, (tx) =>
|
||||
tx.query(
|
||||
`UPDATE atlas.product_platform_profiles
|
||||
SET store_product_category_id = $1, platform_type_id = 'manual-type',
|
||||
category_source = 'manual'
|
||||
WHERE id = $2`,
|
||||
[manual.id, first.profileId],
|
||||
),
|
||||
);
|
||||
const preserved = await request(`/api/products/${first.productId}/editor`, {
|
||||
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
|
||||
});
|
||||
assert.equal(preserved.response.status, 200);
|
||||
assert.equal(
|
||||
preserved.body.data.profile.category.id,
|
||||
manual.id,
|
||||
);
|
||||
assert.equal(preserved.body.data.profile.category.source, 'manual');
|
||||
});
|
||||
|
||||
it('M3.5 为类目属性和关键词字典搜索创建无密钥 outbox 任务', async () => {
|
||||
const dictionaryId = `m35-dictionary-${randomUUID()}`;
|
||||
const category = await database.query<{ id: string }>(
|
||||
`INSERT INTO atlas.platform_product_categories
|
||||
(platform_id, external_category_id, external_type_id, name, path)
|
||||
VALUES ($1, $2, 'm35-type', 'M3.5 测试类目', '测试 › M3.5 测试类目')
|
||||
RETURNING id`,
|
||||
[platformId, `m35-category-${randomUUID()}`],
|
||||
);
|
||||
seededCategoryIds.push(category.rows[0].id);
|
||||
const category = await seedStoreCategory(`m35-category-${randomUUID()}`, 'm35-type', 'M3.5 测试类目', '测试 › M3.5 测试类目');
|
||||
seededCategoryIds.push(category.id);
|
||||
seededDictionaryIds.push(dictionaryId);
|
||||
await database.query(
|
||||
`INSERT INTO atlas.platform_product_category_attributes
|
||||
(platform_id, platform_product_category_id, platform_attribute_id, name,
|
||||
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 ($1, $2, 'm35-color', 'M3.5 颜色', 'dictionary', false, false, 1, $3, false)`,
|
||||
[platformId, category.rows[0].id, dictionaryId],
|
||||
);
|
||||
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],
|
||||
));
|
||||
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',
|
||||
@@ -1082,20 +1146,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
],
|
||||
);
|
||||
});
|
||||
const tree = await request(
|
||||
`/api/product-platform-profiles/${first.profileId}/category-syncs`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: headers({
|
||||
'X-Atlas-Platform-Id': platformId,
|
||||
'Idempotency-Key': randomUUID(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
assert.equal(tree.response.status, 202);
|
||||
assert.equal(tree.body.data.operation.kind, 'platform_category_tree_sync');
|
||||
const schema = await request(
|
||||
`/api/product-platform-profiles/${first.profileId}/attribute-schema-syncs?category_id=${category.rows[0].id}`,
|
||||
`/api/product-platform-profiles/${first.profileId}/attribute-schema-syncs?category_id=${category.id}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: headers({
|
||||
@@ -1105,9 +1157,9 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
},
|
||||
);
|
||||
assert.equal(schema.response.status, 202);
|
||||
assert.equal(schema.body.data.operation.kind, 'platform_category_attribute_sync');
|
||||
assert.equal(schema.body.data.operation.kind, 'store_category_attribute_sync');
|
||||
const dictionary = await request(
|
||||
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${dictionaryId}/syncs?category_id=${category.rows[0].id}&q=%E9%BB%91%E8%89%B2`,
|
||||
`/api/product-platform-profiles/${first.profileId}/attribute-dictionaries/${dictionaryId}/syncs?category_id=${category.id}&q=%E9%BB%91%E8%89%B2`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: headers({
|
||||
@@ -1117,21 +1169,20 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
},
|
||||
);
|
||||
assert.equal(dictionary.response.status, 202);
|
||||
assert.equal(dictionary.body.data.operation.kind, 'platform_attribute_dictionary_sync');
|
||||
assert.equal(dictionary.body.data.operation.kind, 'store_attribute_dictionary_sync');
|
||||
await database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
const events = await tx.query<{ event_type: string; event_payload: unknown }>(
|
||||
`SELECT event_type, event_payload FROM atlas.outbox_events
|
||||
WHERE aggregate_id = $1 AND event_type LIKE 'platform_%_sync.requested'
|
||||
WHERE aggregate_id = $1 AND event_type LIKE 'store_%_sync.requested'
|
||||
ORDER BY created_at`,
|
||||
[first.profileId],
|
||||
);
|
||||
assert.equal(events.rows.length, 3);
|
||||
assert.equal(events.rows.length, 2);
|
||||
assert.deepEqual(
|
||||
events.rows.map((event) => event.event_type),
|
||||
[
|
||||
'platform_category_tree_sync.requested',
|
||||
'platform_category_attribute_sync.requested',
|
||||
'platform_attribute_dictionary_sync.requested',
|
||||
'store_category_attribute_sync.requested',
|
||||
'store_attribute_dictionary_sync.requested',
|
||||
],
|
||||
);
|
||||
assert.ok(
|
||||
|
||||
Reference in New Issue
Block a user