feat: 完成 SKU 图片选择及素材导入后端

This commit is contained in:
R524809
2026-09-24 17:46:34 +08:00
parent d24ce8f4e0
commit e670d06ca6
26 changed files with 2287 additions and 119 deletions
+26 -3
View File
@@ -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;
+1 -1
View File
@@ -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());