feat(edit): AI套图生成、图片切七牛
This commit is contained in:
@@ -4,6 +4,7 @@ import type { AddressInfo } from 'node:net';
|
||||
import type { Database } from '@atlas/database';
|
||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import type { ProductPublishJob } from '@atlas/contracts';
|
||||
import { ozonHashtagProblem } from '@atlas/contracts';
|
||||
import type { S3Client } from '@aws-sdk/client-s3';
|
||||
import { beforeAll, afterAll, describe, it } from 'vitest';
|
||||
import { DATABASE } from '../src/database/database.module.js';
|
||||
@@ -204,7 +205,7 @@ describe('首次发布 HTTP / 数据库 / Worker 闭环', () => {
|
||||
)
|
||||
).rows[0].id;
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.platform_product_category_attributes(platform_id,platform_product_category_id,platform_attribute_id,name,value_type,is_required,is_aspect,max_value_count) VALUES($1,$2,'9048','型号','text',true,false,1)`,
|
||||
`INSERT INTO atlas.platform_product_category_attributes(platform_id,platform_product_category_id,platform_attribute_id,name,value_type,is_required,is_aspect,max_value_count) VALUES($1,$2,'9048','型号','text',true,false,1),($1,$2,'23171','#主题标签','text',false,false,1)`,
|
||||
[platform, category],
|
||||
);
|
||||
await tx.query(
|
||||
@@ -217,6 +218,167 @@ describe('首次发布 HTTP / 数据库 / Worker 闭环', () => {
|
||||
await app?.close();
|
||||
});
|
||||
|
||||
it('多个主题标签合并为一个 Ozon 属性值,SKU 覆盖与继承均保留', async () => {
|
||||
const f = await fixture();
|
||||
await db.workspaceTransaction(workspace, async (tx) => {
|
||||
await tx.query(
|
||||
'UPDATE atlas.product_platform_profiles SET tags=$2::jsonb WHERE id=$1',
|
||||
[f.profile, JSON.stringify(['#boho', '#loft'])],
|
||||
);
|
||||
await tx.query(
|
||||
'UPDATE atlas.product_variant_platform_listings SET tags=$3::jsonb WHERE profile_id=$1 AND product_variant_id=$2',
|
||||
[f.profile, f.ids[0], JSON.stringify(['#kpop', '#oldmoney'])],
|
||||
);
|
||||
});
|
||||
const created = await publish(f);
|
||||
assert.equal(created.status, 202, JSON.stringify(created));
|
||||
const job = created.body.data.publish_job as ProductPublishJob;
|
||||
await run(job, async (_client, _key, path, body) => {
|
||||
if (path === '/v3/product/info/list') return { items: [] };
|
||||
assert.equal(path, '/v3/product/import');
|
||||
const items = body.items as Array<{
|
||||
attributes: Array<{
|
||||
id: number;
|
||||
values: Array<{ value: string }>;
|
||||
}>;
|
||||
}>;
|
||||
assert.deepEqual(
|
||||
items.map(
|
||||
(item) =>
|
||||
item.attributes.find(
|
||||
(attribute) => attribute.id === 23171,
|
||||
)?.values,
|
||||
),
|
||||
[[{ value: '#kpop #oldmoney' }], [{ value: '#boho #loft' }]],
|
||||
);
|
||||
return { result: { task_id: 23171 } };
|
||||
});
|
||||
});
|
||||
|
||||
it('主题标签支持 30 个标签,空标签不提交属性', async () => {
|
||||
for (const tags of [
|
||||
[],
|
||||
Array.from({ length: 30 }, (_, index) => `#tag${index}`),
|
||||
]) {
|
||||
const f = await fixture(1);
|
||||
await db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
'UPDATE atlas.product_platform_profiles SET tags=$2::jsonb WHERE id=$1',
|
||||
[f.profile, JSON.stringify(tags)],
|
||||
),
|
||||
);
|
||||
const created = await publish(f);
|
||||
assert.equal(created.status, 202, JSON.stringify(created));
|
||||
const result = await db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query<{
|
||||
snapshot: {
|
||||
attributes: Array<{
|
||||
id: number;
|
||||
values: Array<{ value: string }>;
|
||||
}>;
|
||||
};
|
||||
}>(
|
||||
'SELECT snapshot FROM atlas.product_publish_job_items WHERE job_id=$1',
|
||||
[created.body.data.publish_job.id],
|
||||
),
|
||||
);
|
||||
const attribute = result.rows[0].snapshot.attributes.find(
|
||||
(item) => item.id === 23171,
|
||||
);
|
||||
assert.deepEqual(
|
||||
attribute?.values,
|
||||
tags.length ? [{ value: tags.join(' ') }] : undefined,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('主题标签只接受单个井号、字母数字下划线及 30 字符边界', () => {
|
||||
assert.equal(ozonHashtagProblem(['#стиль_2026', '#boho']), null);
|
||||
assert.equal(ozonHashtagProblem(['#' + 'я'.repeat(29)]), null);
|
||||
for (const tag of [
|
||||
'стиль',
|
||||
'#стиль мода',
|
||||
'#стиль-мода',
|
||||
'#стиль/мода',
|
||||
'#стиль#мода',
|
||||
'#стиль😀',
|
||||
'#',
|
||||
'#' + 'я'.repeat(30),
|
||||
])
|
||||
assert.ok(ozonHashtagProblem([tag]), tag);
|
||||
assert.ok(
|
||||
ozonHashtagProblem(Array.from({ length: 31 }, () => '#стиль')),
|
||||
);
|
||||
});
|
||||
|
||||
it('历史不合规主题标签在预检与创建发布时拦截,不影响合法 SKU 覆盖', async () => {
|
||||
const f = await fixture();
|
||||
await db.workspaceTransaction(workspace, async (tx) => {
|
||||
await tx.query(
|
||||
'UPDATE atlas.product_platform_profiles SET tags=$2::jsonb WHERE id=$1',
|
||||
[f.profile, JSON.stringify(['спортивный стиль'])],
|
||||
);
|
||||
await tx.query(
|
||||
'UPDATE atlas.product_variant_platform_listings SET tags=$3::jsonb WHERE profile_id=$1 AND product_variant_id=$2',
|
||||
[f.profile, f.ids[0], JSON.stringify(['#спортивный_стиль'])],
|
||||
);
|
||||
});
|
||||
const preflight = await request(
|
||||
`/product-platform-profiles/${f.profile}/publish-preflights`,
|
||||
'POST',
|
||||
{ variant_ids: f.ids },
|
||||
);
|
||||
assert.equal(preflight.status, 200, JSON.stringify(preflight));
|
||||
assert.equal(preflight.body.data.status, 'blocked');
|
||||
const validation = preflight.body.data.validation;
|
||||
assert.equal(
|
||||
validation.variants.find(
|
||||
(v: { variant_id: string }) => v.variant_id === f.ids[0],
|
||||
).ready,
|
||||
true,
|
||||
);
|
||||
assert.ok(
|
||||
validation.variants
|
||||
.find((v: { variant_id: string }) => v.variant_id === f.ids[1])
|
||||
.issues.some(
|
||||
(issue: { code: string; field: string }) =>
|
||||
issue.code === 'OZON_HASHTAG_INVALID' &&
|
||||
issue.field === 'profile.tags',
|
||||
),
|
||||
);
|
||||
const rejected = await publish(f);
|
||||
assert.equal(rejected.status, 422);
|
||||
assert.ok(
|
||||
rejected.body.error.details.some(
|
||||
(issue: { code: string }) =>
|
||||
issue.code === 'OZON_HASHTAG_INVALID',
|
||||
),
|
||||
);
|
||||
const selected = await publish(f, [f.ids[0]]);
|
||||
assert.equal(selected.status, 202, JSON.stringify(selected));
|
||||
|
||||
const invalidSku = await fixture(1);
|
||||
await db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
'UPDATE atlas.product_variant_platform_listings SET tags=$3::jsonb WHERE profile_id=$1 AND product_variant_id=$2',
|
||||
[
|
||||
invalidSku.profile,
|
||||
invalidSku.ids[0],
|
||||
JSON.stringify(['#стиль-мода']),
|
||||
],
|
||||
),
|
||||
);
|
||||
const response = await publish(invalidSku);
|
||||
assert.equal(response.status, 422);
|
||||
assert.ok(
|
||||
response.body.error.details.some(
|
||||
(issue: { code: string; field: string }) =>
|
||||
issue.code === 'OZON_HASHTAG_INVALID' &&
|
||||
issue.field === `variants.${invalidSku.ids[0]}.tags`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('预检、并发幂等、精度快照、Outbox 与跨租户隔离', async () => {
|
||||
const f = await fixture();
|
||||
const key = randomUUID();
|
||||
@@ -241,12 +403,21 @@ describe('首次发布 HTTP / 数据库 / Worker 闭环', () => {
|
||||
[job.id],
|
||||
)
|
||||
).rows,
|
||||
listings: (
|
||||
await tx.query(
|
||||
'SELECT selling_price::text, list_price::text FROM atlas.product_variant_platform_listings WHERE profile_id=$1',
|
||||
[f.profile],
|
||||
)
|
||||
).rows,
|
||||
}));
|
||||
assert.equal(facts.events.length, 1);
|
||||
assert.deepEqual(facts.events[0].event_payload, {
|
||||
publish_job_id: job.id,
|
||||
});
|
||||
assert.equal(facts.items[0].snapshot.price, '1999.1234');
|
||||
assert.equal(facts.items[0].snapshot.price, '2999.1234');
|
||||
assert.equal(facts.listings[0].selling_price, '1999.1234');
|
||||
assert.equal(facts.listings[0].list_price, '2999.1234');
|
||||
assert.ok(!('old_price' in facts.items[0].snapshot));
|
||||
assert.equal(facts.items[0].snapshot.depth, 240);
|
||||
assert.ok(!('vat' in facts.items[0].snapshot));
|
||||
assert.ok(!('images' in facts.items[0].snapshot));
|
||||
@@ -297,6 +468,51 @@ describe('首次发布 HTTP / 数据库 / Worker 闭环', () => {
|
||||
'VERSION_CONFLICT',
|
||||
);
|
||||
});
|
||||
it('首次价格使用划线价;零折扣仍可提交,缺失或零划线价阻断预检和创建', async () => {
|
||||
const equal = await fixture(1);
|
||||
await db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
'UPDATE atlas.product_variant_platform_listings SET list_price=selling_price WHERE profile_id=$1',
|
||||
[equal.profile],
|
||||
),
|
||||
);
|
||||
const created = await publish(equal);
|
||||
assert.equal(created.status, 202, JSON.stringify(created));
|
||||
await run(created.body.data.publish_job, async (_client, _key, path, body) => {
|
||||
if (path === '/v3/product/info/list') return { items: [] };
|
||||
assert.equal(path, '/v3/product/import');
|
||||
const item = (body.items as any[])[0];
|
||||
assert.equal(item.price, '1999.1234');
|
||||
assert.ok(!('old_price' in item));
|
||||
return { result: { task_id: 987654 } };
|
||||
});
|
||||
for (const listPrice of [null, '0']) {
|
||||
const f = await fixture(1);
|
||||
await db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
'UPDATE atlas.product_variant_platform_listings SET list_price=$2, selling_price=CASE WHEN $2::numeric=0 THEN 0 ELSE selling_price END WHERE profile_id=$1',
|
||||
[f.profile, listPrice],
|
||||
),
|
||||
);
|
||||
const preflight = await request(
|
||||
`/product-platform-profiles/${f.profile}/publish-preflights`,
|
||||
'POST',
|
||||
{ variant_ids: f.ids },
|
||||
{ 'Idempotency-Key': randomUUID(), 'If-Match': '"1:1"' },
|
||||
);
|
||||
assert.equal(preflight.status, 200, JSON.stringify(preflight));
|
||||
assert.equal(preflight.body.data.validation.variants[0].ready, false);
|
||||
assert.ok(preflight.body.data.validation.variants[0].issues.some(
|
||||
(issue: { code: string; field: string }) =>
|
||||
issue.code === 'LIST_PRICE_REQUIRED' && issue.field === `variants.${f.ids[0]}.list_price`,
|
||||
));
|
||||
const rejected = await publish(f);
|
||||
assert.equal(rejected.status, 422);
|
||||
assert.ok(rejected.body.error.details.some(
|
||||
(issue: { code: string }) => issue.code === 'LIST_PRICE_REQUIRED',
|
||||
));
|
||||
}
|
||||
});
|
||||
it('只校验所选 SKU;不就绪的未选 SKU 不阻断部分上架', async () => {
|
||||
const f = await fixture();
|
||||
await db.workspaceTransaction(workspace, (tx) =>
|
||||
@@ -322,7 +538,8 @@ describe('首次发布 HTTP / 数据库 / Worker 闭环', () => {
|
||||
imports++;
|
||||
const items = body.items as any[];
|
||||
assert.equal(items.length, 2);
|
||||
assert.equal(items[0].price, '1999.1234');
|
||||
assert.equal(items[0].price, '2999.1234');
|
||||
assert.ok(!('old_price' in items[0]));
|
||||
assert.ok(!('vat' in items[0]));
|
||||
assert.ok(!('barcode' in items[0]));
|
||||
assert.ok(!('description' in items[0]));
|
||||
|
||||
Reference in New Issue
Block a user