feat(edit): 编辑上报细节优化
This commit is contained in:
@@ -100,6 +100,10 @@ export class MediaService {
|
||||
request: input,
|
||||
},
|
||||
async (tx) => {
|
||||
if (input.purpose === 'product_image') {
|
||||
if (!input.product_id) throw new ApiError(422, 'VALIDATION_FAILED', '商品图片必须绑定商品');
|
||||
await this.requireProduct(tx, input.product_id);
|
||||
}
|
||||
await this.ensureBucket();
|
||||
const id = randomUUID();
|
||||
const objectKey = `uploads/${workspaceId}/${input.purpose === 'product_image' ? 'product-images' : 'watermark-images'}/${id}`;
|
||||
@@ -107,9 +111,9 @@ export class MediaService {
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.media_upload_intents
|
||||
(id, workspace_id, purpose, storage_provider, bucket, object_key, expected_content_type,
|
||||
expected_byte_size, expected_checksum_sha256, expires_at, created_by_user_id)
|
||||
expected_byte_size, expected_checksum_sha256, expires_at, created_by_user_id, product_id)
|
||||
VALUES ($1, current_setting('app.workspace_id')::uuid, $2, $10,
|
||||
$3, $4, $5, $6, $7, $8, $9)`,
|
||||
$3, $4, $5, $6, $7, $8, $9, $11)`,
|
||||
[
|
||||
id,
|
||||
input.purpose,
|
||||
@@ -121,6 +125,7 @@ export class MediaService {
|
||||
expiresAt,
|
||||
userId,
|
||||
this.config.OBJECT_STORAGE_PROVIDER,
|
||||
input.product_id ?? null,
|
||||
],
|
||||
);
|
||||
const url = await getSignedUrl(
|
||||
@@ -295,7 +300,7 @@ export class MediaService {
|
||||
object_key: string;
|
||||
content_type: string;
|
||||
}>(
|
||||
`SELECT storage_provider, bucket, object_key, content_type FROM atlas.media_assets WHERE id = $1`,
|
||||
`SELECT storage_provider, bucket, object_key, content_type FROM atlas.media_assets WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[assetId],
|
||||
);
|
||||
if (!asset.rows[0])
|
||||
@@ -315,18 +320,19 @@ export class MediaService {
|
||||
});
|
||||
}
|
||||
|
||||
/** 返回工作空间最近上传的商品素材,预览 URL 仅在本次响应中签发。 */
|
||||
public async productUploads(workspaceId: string) {
|
||||
/** 返回当前商品最近上传的素材,预览 URL 仅在本次响应中签发。 */
|
||||
public async productUploads(workspaceId: string, productId: string) {
|
||||
return this.database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
const uploads = await tx.query<AssetRow & { storage_provider: string; bucket: string; object_key: string }>(
|
||||
`SELECT a.id, a.storage_provider, a.content_type, a.byte_size::text, a.width_px, a.height_px,
|
||||
a.created_at, a.bucket, a.object_key, max(i.completed_at) AS uploaded_at
|
||||
FROM atlas.media_assets a
|
||||
JOIN atlas.media_upload_intents i ON i.asset_id = a.id
|
||||
WHERE i.purpose = 'product_image' AND i.status = 'ready'
|
||||
WHERE i.purpose = 'product_image' AND i.status = 'ready' AND i.product_id = $1 AND a.deleted_at IS NULL
|
||||
GROUP BY a.id
|
||||
ORDER BY uploaded_at DESC, a.id DESC
|
||||
LIMIT 100`,
|
||||
[productId],
|
||||
);
|
||||
const expiresAt = new Date(Date.now() + 10 * 60_000).toISOString();
|
||||
return Promise.all(uploads.rows.map(async (asset) => ({
|
||||
@@ -354,6 +360,7 @@ export class MediaService {
|
||||
return this.idempotency.run(
|
||||
{ workspaceId, userId, method: 'POST', path: '/api/media-assets/product-imports', key, request: input },
|
||||
async (tx) => {
|
||||
await this.requireProduct(tx, input.product_id);
|
||||
const image = await downloadProductImage(input.url);
|
||||
const checksum = createHash('sha256').update(image.bytes).digest('hex');
|
||||
const existing = await tx.query<{ id: string }>(
|
||||
@@ -363,7 +370,7 @@ export class MediaService {
|
||||
let assetId = existing.rows[0]?.id ?? randomUUID();
|
||||
if (!existing.rows[0]) {
|
||||
await this.ensureBucket();
|
||||
const objectKey = `product-imports/${workspaceId}/${checksum}`;
|
||||
const objectKey = `product-imports/${workspaceId}/${checksum}/${randomUUID()}`;
|
||||
await this.s3.send(new PutObjectCommand({
|
||||
Bucket: this.config.OBJECT_STORAGE_BUCKET,
|
||||
Key: objectKey,
|
||||
@@ -388,11 +395,11 @@ export class MediaService {
|
||||
}
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.product_image_imports
|
||||
(workspace_id, source_url, asset_id, created_by_user_id)
|
||||
VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3)
|
||||
ON CONFLICT (workspace_id, source_url)
|
||||
(workspace_id, source_url, asset_id, created_by_user_id, product_id)
|
||||
VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, $4)
|
||||
ON CONFLICT (workspace_id, product_id, source_url)
|
||||
DO UPDATE SET asset_id = EXCLUDED.asset_id, updated_at = now()`,
|
||||
[input.url, assetId, userId],
|
||||
[input.url, assetId, userId, input.product_id],
|
||||
);
|
||||
const asset = await this.requireAsset(tx, assetId);
|
||||
const stored = await tx.query<{ storage_provider: string; bucket: string; object_key: string }>(
|
||||
@@ -410,7 +417,7 @@ export class MediaService {
|
||||
);
|
||||
}
|
||||
|
||||
public async productImports(workspaceId: string) {
|
||||
public async productImports(workspaceId: string, productId: string) {
|
||||
return this.database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
const imports = await tx.query<AssetRow & {
|
||||
storage_provider: string;
|
||||
@@ -423,11 +430,13 @@ export class MediaService {
|
||||
FROM (
|
||||
SELECT DISTINCT ON (asset_id) asset_id, source_url, updated_at, id
|
||||
FROM atlas.product_image_imports
|
||||
WHERE product_id = $1
|
||||
ORDER BY asset_id, updated_at DESC, id DESC
|
||||
) i
|
||||
JOIN atlas.media_assets a ON a.id = i.asset_id
|
||||
JOIN atlas.media_assets a ON a.id = i.asset_id AND a.deleted_at IS NULL
|
||||
ORDER BY i.updated_at DESC, i.id DESC
|
||||
LIMIT 100`,
|
||||
[productId],
|
||||
);
|
||||
const expiresAt = new Date(Date.now() + 10 * 60_000).toISOString();
|
||||
return Promise.all(imports.rows.map(async (asset) => ({
|
||||
@@ -445,6 +454,11 @@ export class MediaService {
|
||||
});
|
||||
}
|
||||
|
||||
private async requireProduct(tx: DatabaseTransaction, productId: string) {
|
||||
const result = await tx.query('SELECT id FROM atlas.products WHERE id = $1', [productId]);
|
||||
if (!result.rows.length) throw new ApiError(404, 'RESOURCE_NOT_FOUND', '商品不存在');
|
||||
}
|
||||
|
||||
private async lockIntent(
|
||||
tx: DatabaseTransaction,
|
||||
intentId: string,
|
||||
@@ -463,7 +477,7 @@ export class MediaService {
|
||||
private async requireAsset(tx: DatabaseTransaction, assetId: string) {
|
||||
const rows = await tx.query<AssetRow>(
|
||||
`SELECT id, content_type, byte_size, width_px, height_px, created_at
|
||||
FROM atlas.media_assets WHERE id = $1`,
|
||||
FROM atlas.media_assets WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[assetId],
|
||||
);
|
||||
if (!rows.rows[0])
|
||||
|
||||
Reference in New Issue
Block a user