feat(edit): 编辑页上报ozon 开发

This commit is contained in:
R524809
2026-09-30 15:57:54 +08:00
parent 0a303414d8
commit 45871584b3
48 changed files with 3700 additions and 887 deletions
+132
View File
@@ -0,0 +1,132 @@
import { createHash, createHmac } from 'node:crypto';
import {
GetObjectCommand,
PutObjectCommand,
type S3Client,
} from '@aws-sdk/client-s3';
import sharp from 'sharp';
import type { OzonPublishItemSnapshot } from '@atlas/contracts';
export type PublishMediaConfig = {
provider: string;
bucket: string;
downloadOrigin: string;
accessKey: string;
secretKey: string;
ttlSeconds: number;
};
/** 七牛私有下载凭证签署包含过期时间的完整 URL,凭证只在提交期间保留。 */
export function qiniuPrivateDownloadUrl(
origin: string,
key: string,
deadline: number,
accessKey: string,
secretKey: string,
): string {
const base = new URL(origin);
if (
base.protocol !== 'https:' ||
base.username ||
base.password ||
base.search ||
base.hash ||
base.pathname !== '/' ||
!accessKey ||
!secretKey ||
!Number.isSafeInteger(deadline) ||
deadline <= 0
)
throw new Error('请配置七牛公网 HTTPS 下载域名及签名凭据');
const url = `${base.origin}/${key.split('/').map(encodeURIComponent).join('/')}?e=${deadline}`;
const signature = createHmac('sha1', secretKey)
.update(url)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_');
return `${url}&token=${accessKey}:${signature}`;
}
/** 为本次发布复制不可变图片,源 checksum 不符时禁止交付;WebP 转为 JPEG。 */
export async function prepareOzonImages(
storage: S3Client,
snapshot: OzonPublishItemSnapshot,
workspaceId: string,
jobId: string,
config: PublishMediaConfig,
): Promise<string[]> {
if (
config.provider !== 'qiniu_kodo' ||
!config.downloadOrigin ||
!Number.isSafeInteger(config.ttlSeconds) ||
config.ttlSeconds < 3600
)
throw new Error(
'上架图片交付尚未配置,请配置七牛 HTTPS 下载域名和有效期',
);
const urls: string[] = [];
for (const media of snapshot.media) {
if (
media.storage_provider !== config.provider ||
media.bucket !== config.bucket
)
throw new Error('图片存储环境与当前七牛配置不一致,请先迁移素材');
const object = await storage.send(
new GetObjectCommand({
Bucket: media.bucket,
Key: media.object_key,
}),
{ abortSignal: AbortSignal.timeout(60_000) },
);
if (!object.Body) throw new Error('发布源图片不存在');
const chunks: Buffer[] = [];
let size = 0;
for await (const chunk of object.Body as AsyncIterable<Uint8Array>) {
size += chunk.byteLength;
if (size > 20 * 1024 * 1024) throw new Error('发布图片超过 20 MB');
chunks.push(Buffer.from(chunk));
}
const source = Buffer.concat(chunks);
if (
createHash('sha256').update(source).digest('hex') !==
media.checksum_sha256
)
throw new Error('图片内容已变更,请重新保存草稿后上架');
const image = sharp(source, {
limitInputPixels: 100_000_000,
failOn: 'error',
});
const meta = await image.metadata();
if (!['jpeg', 'png', 'webp'].includes(meta.format ?? ''))
throw new Error('发布图片实际格式不受支持');
const webp = meta.format === 'webp';
const bytes = webp
? await image
.rotate()
.flatten({ background: '#ffffff' })
.jpeg({ quality: 95 })
.toBuffer()
: source;
const ext = meta.format === 'png' ? 'png' : 'jpg';
const key = `publishing/${workspaceId}/${jobId}/${media.asset_id}-${media.checksum_sha256}.${ext}`;
await storage.send(
new PutObjectCommand({
Bucket: media.bucket,
Key: key,
Body: bytes,
ContentType: ext === 'png' ? 'image/png' : 'image/jpeg',
}),
{ abortSignal: AbortSignal.timeout(60_000) },
);
urls.push(
qiniuPrivateDownloadUrl(
config.downloadOrigin,
key,
Math.floor(Date.now() / 1000) + config.ttlSeconds,
config.accessKey,
config.secretKey,
),
);
}
return urls;
}