feat(ai-image): AI生图开发

This commit is contained in:
2026-09-27 22:40:52 +08:00
parent e670d06ca6
commit a29dd92676
105 changed files with 9943 additions and 361 deletions
@@ -0,0 +1,81 @@
import { describe, expect, it, vi } from 'vitest';
import { AspectRatioDerivativeService } from '../src/media/aspect-ratio-derivative.service.js';
const workspaceId = '30cf54b7-06a3-4e5f-a5fc-d74165115094';
const sourceAssetId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998';
const derivativeId = '68db226f-3fcb-4579-ab9e-4932b1d84067';
const productId = '733d0d73-3d39-4a98-ac0a-5c0e521d8998';
describe('一键 3:4 派生任务', () => {
it('在同一个租户事务写入派生记录、操作进度和 Outbox', async () => {
const statements: string[] = [];
const query = vi.fn(async (sql: string) => {
statements.push(sql);
if (sql.includes('SELECT id FROM atlas.media_assets')) return { rows: [{ id: sourceAssetId }] };
if (sql.includes('RETURNING id')) return { rows: [{ id: derivativeId }] };
return { rows: [] };
});
const run = vi.fn(async (_input: unknown, work: (tx: { query: typeof query }) => Promise<unknown>) =>
work({ query }));
const service = new AspectRatioDerivativeService(
{} as ConstructorParameters<typeof AspectRatioDerivativeService>[0],
{ run } as unknown as ConstructorParameters<typeof AspectRatioDerivativeService>[1],
{} as ConstructorParameters<typeof AspectRatioDerivativeService>[2],
);
const result = await service.create(
workspaceId,
'user-id',
sourceAssetId,
'idempotency-key',
{ preset: '3:4', fit: 'contain' },
);
expect(result).toMatchObject({
status: 202,
body: { data: { id: derivativeId, status: 'queued', source_asset_id: sourceAssetId } },
});
expect(run).toHaveBeenCalledOnce();
expect(statements.some((sql) => sql.includes('INSERT INTO atlas.media_aspect_ratio_derivatives'))).toBe(true);
expect(statements.some((sql) => sql.includes('INSERT INTO atlas.operations'))).toBe(true);
expect(statements.some((sql) => sql.includes('INSERT INTO atlas.outbox_events'))).toBe(true);
});
it('缺少或跨工作空间的源素材不会创建任务', async () => {
const query = vi.fn(async () => ({ rows: [] }));
const run = vi.fn(async (_input: unknown, work: (tx: { query: typeof query }) => Promise<unknown>) =>
work({ query }));
const service = new AspectRatioDerivativeService(
{} as ConstructorParameters<typeof AspectRatioDerivativeService>[0],
{ run } as unknown as ConstructorParameters<typeof AspectRatioDerivativeService>[1],
{} as ConstructorParameters<typeof AspectRatioDerivativeService>[2],
);
await expect(service.create(
workspaceId,
'user-id',
sourceAssetId,
'idempotency-key',
{ preset: '3:4', fit: 'contain' },
)).rejects.toMatchObject({ status: 404 });
expect(query).toHaveBeenCalledOnce();
});
it('将当前工作空间的商品关联到新派生任务', async () => {
const query = vi.fn(async (sql: string, values?: unknown[]) => {
if (sql.includes('SELECT id FROM atlas.media_assets')) return { rows: [{ id: sourceAssetId }] };
if (sql.includes('SELECT id FROM atlas.products')) return { rows: [{ id: productId }] };
if (sql.includes('INSERT INTO atlas.media_aspect_ratio_derivatives')) {
expect(values).toEqual([productId, sourceAssetId, 'user-id']);
return { rows: [{ id: derivativeId }] };
}
return { rows: [] };
});
const run = vi.fn(async (_input: unknown, work: (tx: { query: typeof query }) => Promise<unknown>) => work({ query }));
const service = new AspectRatioDerivativeService(
{} as ConstructorParameters<typeof AspectRatioDerivativeService>[0],
{ run } as unknown as ConstructorParameters<typeof AspectRatioDerivativeService>[1],
{} as ConstructorParameters<typeof AspectRatioDerivativeService>[2],
);
await service.create(workspaceId, 'user-id', sourceAssetId, 'key',
{ preset: '3:4', fit: 'contain', product_id: productId });
expect(query.mock.calls.some(([sql]) => String(sql).includes('INSERT INTO atlas.media_aspect_ratio_derivatives'))).toBe(true);
});
});