feat: 完成 SKU 图片选择及素材导入后端
This commit is contained in:
@@ -5,17 +5,21 @@ const checksum = z
|
||||
.regex(/^[a-f0-9]{64}$/, '必须是 SHA-256 十六进制摘要');
|
||||
export const createMediaUploadIntentRequestSchema = z
|
||||
.object({
|
||||
purpose: z.literal('watermark_image'),
|
||||
purpose: z.enum(['watermark_image', 'product_image']),
|
||||
filename: z.string().trim().min(1).max(255),
|
||||
content_type: z.enum(['image/png', 'image/jpeg', 'image/webp']),
|
||||
byte_size: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(5 * 1024 * 1024),
|
||||
.max(20 * 1024 * 1024),
|
||||
checksum_sha256: checksum,
|
||||
})
|
||||
.strict();
|
||||
.strict()
|
||||
.refine(
|
||||
(value) => value.purpose !== 'watermark_image' || value.byte_size <= 5 * 1024 * 1024,
|
||||
{ path: ['byte_size'], message: '水印图片不能超过 5 MiB' },
|
||||
);
|
||||
export const completeMediaUploadIntentRequestSchema = z
|
||||
.object({
|
||||
checksum_sha256: checksum,
|
||||
@@ -26,6 +30,18 @@ export const createMediaAccessUrlRequestSchema = z
|
||||
purpose: z.literal('preview'),
|
||||
})
|
||||
.strict();
|
||||
export const importProductImageRequestSchema = z.object({
|
||||
url: z.url().max(2048).refine((value) => {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === 'https:' && !url.username && !url.password && !url.port
|
||||
&& /\.(?:jpe?g|png)$/i.test(url.pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, '仅支持 HTTPS 的 JPG 或 PNG 图片链接'),
|
||||
}).strict();
|
||||
export type ImportProductImageRequest = z.infer<typeof importProductImageRequestSchema>;
|
||||
|
||||
export type CreateMediaUploadIntentRequest = z.infer<
|
||||
typeof createMediaUploadIntentRequestSchema
|
||||
@@ -60,6 +76,13 @@ export type MediaAsset = {
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
/** 工作空间已上传的商品图片,预览地址过期后需重新读取。 */
|
||||
export type ProductUploadImage = MediaAsset & {
|
||||
preview_url: string;
|
||||
preview_url_expires_at: string;
|
||||
};
|
||||
export type ProductImportedImage = ProductUploadImage & { source_url: string };
|
||||
|
||||
/** POST /api/media-assets/{asset_id}/access-urls 响应(短时预览 URL) */
|
||||
export type MediaAccessUrl = {
|
||||
url: string;
|
||||
|
||||
@@ -552,7 +552,7 @@ export const saveProductEditorDraftRequestSchema = z
|
||||
offer_id: nullableText(500),
|
||||
media_asset_ids: z
|
||||
.array(uuid)
|
||||
.max(15)
|
||||
.max(30)
|
||||
.refine(
|
||||
(ids) => new Set(ids).size === ids.length,
|
||||
'SKU 图片不能重复',
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
SET search_path TO atlas, public;
|
||||
|
||||
-- SKU 图片上限扩展到 30 张,保持与编辑草稿契约一致。
|
||||
ALTER TABLE product_variant_listing_media
|
||||
DROP CONSTRAINT product_variant_listing_media_sort_order_check;
|
||||
ALTER TABLE product_variant_listing_media
|
||||
ADD CONSTRAINT product_variant_listing_media_sort_order_check
|
||||
CHECK (sort_order >= 0 AND sort_order < 30);
|
||||
@@ -0,0 +1,21 @@
|
||||
SET search_path TO atlas, public;
|
||||
|
||||
-- 网络图片导入记录与正式素材分开,保留来源 URL 供候选区回显。
|
||||
CREATE TABLE product_image_imports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
|
||||
source_url TEXT NOT NULL,
|
||||
asset_id UUID NOT NULL,
|
||||
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
FOREIGN KEY (workspace_id, asset_id) REFERENCES media_assets(workspace_id, id) ON DELETE RESTRICT,
|
||||
UNIQUE (workspace_id, source_url)
|
||||
);
|
||||
CREATE INDEX ix_product_image_imports_recent
|
||||
ON product_image_imports (workspace_id, updated_at DESC, id DESC);
|
||||
ALTER TABLE product_image_imports ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE product_image_imports FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY workspace_isolation ON product_image_imports
|
||||
USING (workspace_id = app_current_workspace_id())
|
||||
WITH CHECK (workspace_id = app_current_workspace_id());
|
||||
Reference in New Issue
Block a user