feat(edit): 采集插件、编辑页SKU 属性动态展示、颜色展示开发

This commit is contained in:
R524809
2026-09-22 10:11:57 +08:00
parent 05747d6f22
commit 2b06ef0692
12 changed files with 390 additions and 11 deletions
@@ -15,6 +15,7 @@ import { z } from 'zod';
import {
productPublishPreflightRequestSchema,
saveProductEditorDraftRequestSchema,
saveProductSourceDescriptionRequestSchema,
type Workspace,
} from '@atlas/contracts';
import { ApiError } from '../common/api-error.js';
@@ -275,6 +276,43 @@ export class ProductEditorController {
)
).body;
}
/** 独立保存右侧采集面板的商品描述,不要求整页草稿处于可保存状态。 */
@Put('products/:productId/source-description')
async saveSourceDescription(
@Param('productId') productId: string,
@Body() body: unknown,
@Headers('x-atlas-platform-id') platformId: string | undefined,
@Headers('idempotency-key') key: string | undefined,
@Headers('if-match') ifMatch: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
) {
const parsed = saveProductSourceDescriptionRequestSchema.safeParse(body);
if (!parsed.success) {
throw new ApiError(
422,
'VALIDATION_FAILED',
'请求参数不符合要求',
parsed.error.issues.map((issue) => ({
field: issue.path.join('.'),
code: 'INVALID_FIELD',
message: issue.message,
})),
);
}
return (
await this.editor.saveSourceDescription(
workspace.id,
request.auth.userId,
requiredUuid('product_id', productId),
requiredUuid('X-Atlas-Platform-Id', platformId),
requiredUuid('Idempotency-Key', key),
parseEditorRevision(ifMatch),
parsed.data,
)
).body;
}
}
/** 校验路由参数或请求头中的 UUID,并返回原始值。 */
@@ -3,6 +3,7 @@ import type {
ProductEditorContext,
ProductEditorSnapshot,
SaveProductEditorDraftRequest,
SaveProductSourceDescriptionRequest,
} from '@atlas/contracts';
import type { Database, DatabaseTransaction } from '@atlas/database';
import { ApiError } from '../common/api-error.js';
@@ -1031,6 +1032,121 @@ export class ProductEditorService {
);
}
/** 保存采集描述覆盖,避免未完成的整页草稿阻塞该独立编辑动作。 */
async saveSourceDescription(
workspaceId: string,
userId: string,
productId: string,
platformId: string,
key: string,
expected: { product: number; profile: number },
input: SaveProductSourceDescriptionRequest,
) {
return this.idempotency.run(
{
workspaceId,
userId,
method: 'PUT',
path: `/api/products/${productId}/source-description`,
key,
request: {
if_match: expected,
platform_id: platformId,
...input,
},
},
async (transaction) => {
await this.requireTargetPlatform(transaction, platformId);
const product = await this.requireProduct(transaction, productId, true);
const profile = await this.findProfile(transaction, productId, platformId);
if (!profile)
throw new ApiError(
409,
'EDITOR_CONTEXT_REQUIRED',
'请先从采集箱进入商品编辑器',
);
if (Number(product.revision) !== expected.product)
await this.throwProductRevisionConflict(
transaction,
productId,
expected.product,
);
if (Number(profile.revision) !== expected.profile)
await this.throwProfileRevisionConflict(
transaction,
profile.id,
expected.profile,
);
if (
input.source_snapshot_revision !==
Number(product.source_snapshot_revision)
) {
throw new ApiError(
409,
'SOURCE_SNAPSHOT_CHANGED',
'采集来源已更新,请重新加载并确认变更',
[
{
field: 'source_snapshot_revision',
code: 'SOURCE_SNAPSHOT_CHANGED',
message: `当前采集版本为 ${product.source_snapshot_revision}`,
current_revision: Number(product.source_snapshot_revision),
expected_revision: input.source_snapshot_revision,
},
],
);
}
const currentSource = resolveSourceMaterial(product);
const overriddenFields = new Set(currentSource.state.overridden_fields);
if (input.description === currentSource.state.latest.description) {
overriddenFields.delete('description');
} else {
overriddenFields.add('description');
}
const overrides = selectSourceMaterialOverrides(
{ ...currentSource.effective, description: input.description },
[...overriddenFields],
);
const updatedProduct = await transaction.query<{ revision: number }>(
`UPDATE atlas.products
SET ai_source_material_overrides = $1::jsonb,
ai_source_material_base_revision = $2,
revision = revision + 1, updated_at = now()
WHERE id = $3 AND revision = $4
RETURNING revision`,
[
JSON.stringify(overrides),
input.source_snapshot_revision,
productId,
expected.product,
],
);
if (!updatedProduct.rows[0])
await this.throwProductRevisionConflict(
transaction,
productId,
expected.product,
);
const savedSource = resolveSourceMaterial({
...product,
ai_source_material_overrides: overrides,
ai_source_material_base_revision: input.source_snapshot_revision,
});
return {
status: 200,
body: {
data: {
product_revision: Number(updatedProduct.rows[0].revision),
profile_revision: Number(profile.revision),
source_material: savedSource.effective,
source_material_state: savedSource.state,
},
},
};
},
);
}
/** 确保每个未删除 SKU 都有当前发布配置对应的上架记录。 */
private async ensureListings(
transaction: DatabaseTransaction,
+66
View File
@@ -1844,4 +1844,70 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
[asset],
);
});
it('独立保存采集描述会持久化覆盖值并保持幂等', async () => {
const created = await createCollectionItem('独立保存描述测试商品');
const entered = await request(
`/api/collection-items/${created.collectionItemId}/editor-contexts`,
{
method: 'POST',
headers: headers({
'X-Atlas-Platform-Id': platformId,
'Idempotency-Key': randomUUID(),
}),
},
);
assert.equal(entered.response.status, 201);
const before = await request(`/api/products/${created.productId}/editor`, {
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
});
const key = randomUUID();
const body = {
description: '独立持久化后的采集商品描述',
source_snapshot_revision:
before.body.data.source_material.source_snapshot_revision,
};
const saved = await request(
`/api/products/${created.productId}/source-description`,
{
method: 'PUT',
headers: headers({
'X-Atlas-Platform-Id': platformId,
'Idempotency-Key': key,
'If-Match': `"${before.body.data.product.revision}:${before.body.data.profile.revision}"`,
}),
body: JSON.stringify(body),
},
);
assert.equal(saved.response.status, 200);
assert.equal(
saved.body.data.source_material.description,
'独立持久化后的采集商品描述',
);
assert.ok(
saved.body.data.source_material_state.overridden_fields.includes(
'description',
),
);
const replay = await request(
`/api/products/${created.productId}/source-description`,
{
method: 'PUT',
headers: headers({
'X-Atlas-Platform-Id': platformId,
'Idempotency-Key': key,
'If-Match': `"${before.body.data.product.revision}:${before.body.data.profile.revision}"`,
}),
body: JSON.stringify(body),
},
);
assert.deepEqual(replay.body, saved.body);
const after = await request(`/api/products/${created.productId}/editor`, {
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
});
assert.equal(
after.body.data.source_material.description,
'独立持久化后的采集商品描述',
);
});
});