feat(edit): 编辑上报细节优化
This commit is contained in:
@@ -6,12 +6,14 @@ import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { afterAll, beforeAll, describe, it } from 'vitest';
|
||||
import { DATABASE } from '../src/database/database.module.js';
|
||||
import { createApplication } from '../src/main.js';
|
||||
import { PRODUCT_VARIANT_LIMIT } from '@atlas/contracts';
|
||||
|
||||
let app: NestExpressApplication;
|
||||
let database: Database;
|
||||
let baseUrl = '';
|
||||
let workspaceId = '';
|
||||
let userId = '';
|
||||
let webAccessToken = '';
|
||||
async function request(path: string, init: RequestInit = {}) {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
redirect: 'manual',
|
||||
@@ -124,7 +126,7 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
assert.equal(registration.response.status, 201);
|
||||
userId = registration.body.data.user.id;
|
||||
workspaceId = registration.body.data.workspace.id;
|
||||
const webAccessToken = registration.body.data.access_token as string;
|
||||
webAccessToken = registration.body.data.access_token as string;
|
||||
const session = await request('/api/extension/session', {
|
||||
headers: { Authorization: `Bearer ${webAccessToken}` },
|
||||
});
|
||||
@@ -166,11 +168,12 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
],
|
||||
evidence: [{ ref: 'page', kind: 'page', path: 'test' }],
|
||||
};
|
||||
const firstKey = randomUUID();
|
||||
const first = await request('/api/extension/materials', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${webAccessToken}`,
|
||||
'Idempotency-Key': randomUUID(),
|
||||
'Idempotency-Key': firstKey,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
@@ -179,6 +182,22 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
assert.equal(first.body.data.collection_item.status, 'ready_for_edit');
|
||||
assert.equal(first.body.data.materialization.pending_selection, 1);
|
||||
assert.equal(first.body.data.materialization.queued, 0);
|
||||
const editorEntry = new URL(first.body.data.editor_entry_path, 'http://atlas.example.test');
|
||||
assert.equal(editorEntry.pathname, `/products/${first.body.data.product_id}/editor`);
|
||||
assert.equal(editorEntry.searchParams.get('collection_item_id'), first.body.data.collection_item.id);
|
||||
const replay = await request('/api/extension/materials', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${webAccessToken}`,
|
||||
'Idempotency-Key': firstKey,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
assert.deepEqual(replay.body, first.body);
|
||||
const profilesBeforeEntry = await database.workspaceTransaction(workspaceId, (tx) =>
|
||||
tx.query('SELECT id FROM atlas.product_platform_profiles WHERE product_id=$1', [first.body.data.product_id]),
|
||||
);
|
||||
assert.equal(profilesBeforeEntry.rowCount, 0);
|
||||
const source1688 = {
|
||||
source: {
|
||||
platform: '1688', itemKey: '1688-price-image-test',
|
||||
@@ -260,7 +279,7 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
);
|
||||
});
|
||||
const editorContext = await request(
|
||||
`/api/collection-items/${first.body.data.collection_item.id}/editor-contexts`,
|
||||
`/api/collection-items/${editorEntry.searchParams.get('collection_item_id')}/editor-contexts`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -271,6 +290,39 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
},
|
||||
);
|
||||
assert.equal(editorContext.response.status, 201);
|
||||
assert.equal(editorContext.body.data.product_id, first.body.data.product_id);
|
||||
assert.equal(editorContext.body.data.editor_path, editorEntry.pathname);
|
||||
const readEditor = (productId: string) => request(
|
||||
`/api/products/${productId}/editor`,
|
||||
{ headers: {
|
||||
Authorization: `Bearer ${webAccessToken}`,
|
||||
'X-Atlas-Platform-Id': targetPlatform.id,
|
||||
} },
|
||||
);
|
||||
const initialEditor = await readEditor(first.body.data.product_id);
|
||||
assert.equal(initialEditor.response.status, 200);
|
||||
assert.equal(initialEditor.body.data.product.common.purchase_url, payload.source.canonicalUrl);
|
||||
const sourceLink = await database.workspaceTransaction(workspaceId, (tx) => tx.query(
|
||||
'SELECT purchase_url FROM atlas.products WHERE id=$1',
|
||||
[captured1688.body.data.product_id],
|
||||
));
|
||||
assert.equal(sourceLink.rows[0].purchase_url, source1688.source.canonicalUrl);
|
||||
await request(`/api/collection-items/${captured1688.body.data.collection_item.id}/editor-contexts`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${webAccessToken}`,
|
||||
'X-Atlas-Platform-Id': targetPlatform.id,
|
||||
'Idempotency-Key': randomUUID(),
|
||||
},
|
||||
});
|
||||
// 模拟历史采集商品:原来只有采集记录保存了页面地址,商品货源字段为空。
|
||||
await database.workspaceTransaction(workspaceId, (tx) => tx.query(
|
||||
'UPDATE atlas.products SET purchase_url=NULL WHERE id=$1',
|
||||
[captured1688.body.data.product_id],
|
||||
));
|
||||
const historicalEditor = await readEditor(captured1688.body.data.product_id);
|
||||
assert.equal(historicalEditor.response.status, 200);
|
||||
assert.equal(historicalEditor.body.data.product.common.purchase_url, source1688.source.canonicalUrl);
|
||||
// Simulate real draft work before the source page is collected again.
|
||||
// Re-collection must neither delete this SKU nor overwrite its Atlas
|
||||
// fields / platform mapping just because a newer source snapshot arrives.
|
||||
@@ -281,7 +333,7 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
[first.body.data.product_id],
|
||||
);
|
||||
await tx.query(
|
||||
`UPDATE atlas.products SET source_title = '人工确认商品名'
|
||||
`UPDATE atlas.products SET source_title = '人工确认商品名', purchase_url = 'https://detail.1688.com/offer/888888888.html'
|
||||
WHERE id = $1`,
|
||||
[first.body.data.product_id],
|
||||
);
|
||||
@@ -366,6 +418,8 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
assert.equal(second.response.status, 200);
|
||||
assert.equal(second.body.data.reused, true);
|
||||
assert.equal(second.body.data.refreshed, true);
|
||||
assert.equal(second.body.data.product_id, first.body.data.product_id);
|
||||
assert.equal(second.body.data.editor_entry_path, first.body.data.editor_entry_path);
|
||||
assert.equal(
|
||||
second.body.data.collection_item.id,
|
||||
first.body.data.collection_item.id,
|
||||
@@ -382,6 +436,10 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
},
|
||||
);
|
||||
assert.equal(editorAfterRefresh.response.status, 200);
|
||||
assert.equal(
|
||||
editorAfterRefresh.body.data.product.common.purchase_url,
|
||||
'https://detail.1688.com/offer/888888888.html',
|
||||
);
|
||||
assert.equal(
|
||||
editorAfterRefresh.body.data.product.common.source_title,
|
||||
'人工确认商品名',
|
||||
@@ -494,4 +552,98 @@ describe('扩展授权与采集 HTTP 集成测试', () => {
|
||||
);
|
||||
assert.equal(transferWorkspace.rows[0]?.workspace_id, workspaceId);
|
||||
});
|
||||
|
||||
it('完整采集并保存 1000 个 SKU,复采保持身份,超限请求原子拒绝', async () => {
|
||||
const variants = Array.from({ length: PRODUCT_VARIANT_LIMIT }, (_, index) => ({
|
||||
sourceVariantKey: String(8_000_000_000_000 + index),
|
||||
sourceSku: String(8_000_000_000_000 + index),
|
||||
displayName: `来源 SKU ${index}`,
|
||||
selections: [], confidence: 'verified', evidenceRefs: [],
|
||||
sourcePrice: { amount: '20.00', currencyCode: 'CNY' },
|
||||
}));
|
||||
const payload = {
|
||||
source: {
|
||||
platform: '1688', itemKey: `large-${randomUUID()}`,
|
||||
canonicalUrl: 'https://detail.1688.com/offer/123456789.html',
|
||||
collectedAt: new Date().toISOString(), parserVersion: 'large-sku-test',
|
||||
},
|
||||
common: { title: { value: '多 SKU 来源商品', confidence: 'verified' }, attributes: [], categories: [] },
|
||||
variantAxes: [], variants, assets: [], evidence: [],
|
||||
};
|
||||
const capture = (body: unknown) => request('/api/extension/materials', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${webAccessToken}`, 'Idempotency-Key': randomUUID() },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const captured = await capture(payload);
|
||||
assert.equal(captured.response.status, 201, JSON.stringify(captured.body));
|
||||
assert.equal(captured.body.data.collection_item.variant_count, PRODUCT_VARIANT_LIMIT);
|
||||
const collectionId = captured.body.data.collection_item.id;
|
||||
const productId = captured.body.data.product_id;
|
||||
const platforms = await request('/api/platforms', { headers: { Authorization: `Bearer ${webAccessToken}` } });
|
||||
const platformId = platforms.body.data.find((platform: { code: string }) => platform.code === 'OZON').id;
|
||||
const context = await request(`/api/collection-items/${collectionId}/editor-contexts`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': platformId,
|
||||
'Idempotency-Key': randomUUID(),
|
||||
},
|
||||
});
|
||||
assert.equal(context.response.status, 201, JSON.stringify(context.body));
|
||||
const read = () => request(`/api/products/${productId}/editor`, {
|
||||
headers: { Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': platformId },
|
||||
});
|
||||
const before = await read();
|
||||
assert.equal(before.response.status, 200);
|
||||
const snapshot = before.body.data;
|
||||
assert.equal(snapshot.variants.length, PRODUCT_VARIANT_LIMIT);
|
||||
assert.equal(snapshot.variants.at(-1).source_sku, variants.at(-1)!.sourceSku);
|
||||
const draft = {
|
||||
ai_source_material: snapshot.source_material,
|
||||
ai_source_overridden_fields: [],
|
||||
common: {
|
||||
...snapshot.product.common,
|
||||
default_physical_spec: snapshot.product.default_physical_spec,
|
||||
default_package_spec: snapshot.product.default_package_spec,
|
||||
},
|
||||
profile: {
|
||||
target_store_id: snapshot.profile.target_store.id,
|
||||
fulfillment: snapshot.profile.fulfillment, title: null, description: null, tags: [],
|
||||
platform_product_category_id: null,
|
||||
},
|
||||
variants: snapshot.variants.map((variant: any, index: number) => ({
|
||||
id: variant.id, client_key: null,
|
||||
display_name: index === PRODUCT_VARIANT_LIMIT - 1 ? '末尾 SKU 已编辑' : variant.display_name,
|
||||
source_sku: variant.source_sku, specification_values: variant.specification_values,
|
||||
purchase_cost_cny: variant.purchase_cost_cny, physical_spec: variant.physical_spec,
|
||||
package_spec: variant.package_spec, tags: [], description: null,
|
||||
offer_id: null, media_asset_ids: [],
|
||||
})),
|
||||
};
|
||||
const saved = await request(`/api/products/${productId}/editor-draft`, {
|
||||
method: 'PUT', headers: {
|
||||
Authorization: `Bearer ${webAccessToken}`, 'X-Atlas-Platform-Id': platformId,
|
||||
'Idempotency-Key': randomUUID(),
|
||||
'If-Match': `"${snapshot.product.revision}:${snapshot.profile.revision}"`,
|
||||
}, body: JSON.stringify(draft),
|
||||
});
|
||||
assert.equal(saved.response.status, 200, JSON.stringify(saved.body));
|
||||
const after = await read();
|
||||
assert.equal(after.body.data.variants.length, PRODUCT_VARIANT_LIMIT);
|
||||
assert.equal(after.body.data.variants.at(-1).display_name, '末尾 SKU 已编辑');
|
||||
const refreshed = await capture(payload);
|
||||
assert.equal(refreshed.response.status, 200);
|
||||
assert.equal(refreshed.body.data.product_id, productId);
|
||||
assert.equal(refreshed.body.data.collection_item.variant_count, PRODUCT_VARIANT_LIMIT);
|
||||
const reread = await read();
|
||||
assert.deepEqual(reread.body.data.variants.map((v: any) => v.id), snapshot.variants.map((v: any) => v.id));
|
||||
assert.equal(reread.body.data.variants.at(-1).display_name, '末尾 SKU 已编辑');
|
||||
const revision = reread.body.data.source_material.source_snapshot_revision;
|
||||
const rejected = await capture({ ...payload, variants: [...variants, { ...variants[0], sourceVariantKey: 'extra-sku' }] });
|
||||
assert.equal(rejected.response.status, 422);
|
||||
assert.equal(rejected.body.error.details[0].field, 'variants');
|
||||
const unchanged = await read();
|
||||
assert.equal(unchanged.body.data.source_material.source_snapshot_revision, revision);
|
||||
assert.equal(unchanged.body.data.variants.length, PRODUCT_VARIANT_LIMIT);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user