feat(ai-image): AI生图开发
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { CreateWatermarkDerivativeRequest, WatermarkDerivative } from '@atlas/contracts';
|
||||
import type { Database } from '@atlas/database';
|
||||
import { ApiError } from '../common/api-error.js';
|
||||
import { IdempotencyService } from '../common/idempotency.service.js';
|
||||
import { DATABASE } from '../database/database.module.js';
|
||||
import { MediaService } from './media.service.js';
|
||||
|
||||
type DerivativeRow = {
|
||||
id: string;
|
||||
status: WatermarkDerivative['status'];
|
||||
source_asset_id: string;
|
||||
watermark_template_id: string;
|
||||
watermark_template_version: number;
|
||||
output_asset_id: string | null;
|
||||
error_code: string | null;
|
||||
content_type: string | null;
|
||||
byte_size: string | null;
|
||||
width_px: number | null;
|
||||
height_px: number | null;
|
||||
asset_created_at: Date | string | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WatermarkDerivativeService {
|
||||
public constructor(
|
||||
@Inject(DATABASE) private readonly database: Database,
|
||||
@Inject(IdempotencyService) private readonly idempotency: IdempotencyService,
|
||||
@Inject(MediaService) private readonly media: MediaService,
|
||||
) {}
|
||||
|
||||
/** 创建任务时固定模板版本,并与进度和 Outbox 一起提交。 */
|
||||
public async create(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
assetId: string,
|
||||
key: string,
|
||||
input: CreateWatermarkDerivativeRequest,
|
||||
) {
|
||||
return this.idempotency.run({
|
||||
workspaceId,
|
||||
userId,
|
||||
method: 'POST',
|
||||
path: `/api/media-assets/${assetId}/watermark-derivatives`,
|
||||
key,
|
||||
request: input,
|
||||
}, async (tx) => {
|
||||
const source = await tx.query<{ id: string }>(
|
||||
`SELECT id FROM atlas.media_assets
|
||||
WHERE id = $1 AND content_type IN ('image/jpeg', 'image/png', 'image/webp')
|
||||
AND byte_size <= 20971520 AND width_px BETWEEN 1 AND 10000
|
||||
AND height_px BETWEEN 1 AND 10000`,
|
||||
[assetId],
|
||||
);
|
||||
if (!source.rows[0]) throw new ApiError(404, 'RESOURCE_NOT_FOUND', '图片素材不存在或不可处理');
|
||||
if (input.product_id) {
|
||||
const product = await tx.query<{ id: string }>(
|
||||
'SELECT id FROM atlas.products WHERE id = $1',
|
||||
[input.product_id],
|
||||
);
|
||||
if (!product.rows[0]) throw new ApiError(404, 'RESOURCE_NOT_FOUND', '商品不存在');
|
||||
}
|
||||
const template = await tx.query<{ id: string; current_version: number }>(
|
||||
`SELECT id, current_version FROM atlas.watermark_templates
|
||||
WHERE id = $1 AND status = 'active' AND deleted_at IS NULL`,
|
||||
[input.watermark_template_id],
|
||||
);
|
||||
if (!template.rows[0]) throw new ApiError(404, 'RESOURCE_NOT_FOUND', '水印模板不存在');
|
||||
const version = template.rows[0].current_version;
|
||||
const created = await tx.query<{ id: string }>(
|
||||
`INSERT INTO atlas.media_watermark_derivatives
|
||||
(workspace_id, product_id, source_asset_id, watermark_template_id,
|
||||
watermark_template_version, created_by_user_id)
|
||||
VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, $4, $5)
|
||||
RETURNING id`,
|
||||
[input.product_id ?? null, assetId, input.watermark_template_id, version, userId],
|
||||
);
|
||||
const id = created.rows[0].id;
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.operations
|
||||
(id, workspace_id, kind, resource_type, resource_id, created_by_user_id)
|
||||
VALUES ($1, current_setting('app.workspace_id')::uuid,
|
||||
'media_watermark_derivative', 'media_derivative', $1, $2)`,
|
||||
[id, userId],
|
||||
);
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.outbox_events
|
||||
(workspace_id, aggregate_type, aggregate_id, event_type, event_payload, deduplication_key)
|
||||
VALUES (current_setting('app.workspace_id')::uuid, 'media_derivative', $1,
|
||||
'media.watermark_derivative.requested',
|
||||
jsonb_build_object('derivative_id', $1::uuid), $1::text)`,
|
||||
[id],
|
||||
);
|
||||
return { status: 202, body: { data: {
|
||||
id,
|
||||
status: 'queued',
|
||||
source_asset_id: assetId,
|
||||
watermark_template_id: input.watermark_template_id,
|
||||
watermark_template_version: version,
|
||||
output_asset: null,
|
||||
error_code: null,
|
||||
} satisfies WatermarkDerivative } };
|
||||
});
|
||||
}
|
||||
|
||||
/** 成功结果读取时才签发短时预览地址。 */
|
||||
public async read(workspaceId: string, derivativeId: string): Promise<WatermarkDerivative> {
|
||||
const row = await this.database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
const result = await tx.query<DerivativeRow>(
|
||||
`SELECT d.id, d.status, d.source_asset_id, d.watermark_template_id,
|
||||
d.watermark_template_version, d.output_asset_id, d.error_code,
|
||||
a.content_type, a.byte_size::text, a.width_px, a.height_px,
|
||||
a.created_at AS asset_created_at
|
||||
FROM atlas.media_watermark_derivatives d
|
||||
LEFT JOIN atlas.media_assets a ON a.id = d.output_asset_id
|
||||
WHERE d.id = $1`,
|
||||
[derivativeId],
|
||||
);
|
||||
return result.rows[0] ?? null;
|
||||
});
|
||||
if (!row) throw new ApiError(404, 'RESOURCE_NOT_FOUND', '水印处理任务不存在');
|
||||
const access = row.output_asset_id
|
||||
? await this.media.accessUrl(workspaceId, row.output_asset_id)
|
||||
: null;
|
||||
return {
|
||||
id: row.id,
|
||||
status: row.status,
|
||||
source_asset_id: row.source_asset_id,
|
||||
watermark_template_id: row.watermark_template_id,
|
||||
watermark_template_version: row.watermark_template_version,
|
||||
error_code: row.error_code,
|
||||
output_asset: row.output_asset_id && access ? {
|
||||
id: row.output_asset_id,
|
||||
content_type: row.content_type!,
|
||||
byte_size: Number(row.byte_size),
|
||||
width_px: row.width_px,
|
||||
height_px: row.height_px,
|
||||
created_at: row.asset_created_at instanceof Date
|
||||
? row.asset_created_at.toISOString() : String(row.asset_created_at),
|
||||
preview_url: access.url,
|
||||
preview_url_expires_at: access.expires_at,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user