feat: 添加图片拼接功能和相关API文档更新
- 在应用中引入ImageCompositionController和ImageCompositionService - 实现图片拼接功能,支持多种排列方式和输出格式 - 更新Swagger文档,添加图片拼接API描述和示例 - 修改模板执行API描述,简化审核流程说明
This commit is contained in:
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"/api/v1/templates/code/{code}/execute": {
|
||||
"post": {
|
||||
"description": "\n 根据模板代码执行AI生成任务,支持同步和异步两种审核模式:\n - 同步模式:立即返回审核结果,若通过则开始执行,返回 executionId 和 taskId\n - 异步模式:返回 executionId 和 auditTaskId,等待审核回调完成后开始执行\n \n ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,同步模式时返回\n - auditTaskId: 审核服务任务ID,异步模式时返回\n ",
|
||||
"description": "\n 根据模板代码执行AI生成任务,直接执行模板无需后端审核。\n \n 参数说明:\n - imageUrl: 输入图片URL(必需)\n - auditTaskId: 前端审核任务ID(可选),用于追踪审核记录\n \n 返回ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,用于追踪模板执行状态\n - auditTaskId: 前端传入的审核任务ID\n ",
|
||||
"operationId": "TemplateController_executeTemplateByCode",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -131,7 +131,9 @@
|
||||
"data": {
|
||||
"executionId": 123,
|
||||
"taskId": "n8n_task_456",
|
||||
"status": "processing"
|
||||
"auditTaskId": "audit_task_789",
|
||||
"status": "processing",
|
||||
"message": "模板执行已启动"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,20 +160,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "图片审核未通过",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"example": {
|
||||
"code": 403,
|
||||
"message": "图片审核未通过: 包含不当内容",
|
||||
"data": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "任务数量限制",
|
||||
"content": {
|
||||
@@ -1094,52 +1082,95 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/enhanced/templates/code/{code}/execute": {
|
||||
"/api/v1/image-composition/compose": {
|
||||
"post": {
|
||||
"description": "统一异步模式:先提交审核,然后通过回调继续执行模板。支持实时状态查询。",
|
||||
"operationId": "EnhancedTemplateController_executeTemplateByCode",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "code",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"description": "将多张图片按指定位置拼接到一个画布上,支持层级、透明度等设置",
|
||||
"operationId": "ImageCompositionController_composeImages",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"description": "图片拼接配置 - 简化版本",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ImageCompositionDto"
|
||||
},
|
||||
"examples": {
|
||||
"horizontal": {
|
||||
"summary": "水平排列示例",
|
||||
"value": {
|
||||
"images": [
|
||||
"https://picsum.photos/300/200",
|
||||
"https://picsum.photos/300/200",
|
||||
"https://picsum.photos/300/200"
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"outputFormat": "jpeg",
|
||||
"maxWidth": 1920,
|
||||
"maxHeight": 1080,
|
||||
"quality": 80
|
||||
}
|
||||
},
|
||||
"vertical": {
|
||||
"summary": "垂直排列示例",
|
||||
"value": {
|
||||
"images": [
|
||||
"https://picsum.photos/200/300",
|
||||
"https://picsum.photos/200/300",
|
||||
"https://picsum.photos/200/300"
|
||||
],
|
||||
"direction": "vertical",
|
||||
"outputFormat": "webp",
|
||||
"maxWidth": 800,
|
||||
"maxHeight": 1200,
|
||||
"quality": 85
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"summary": "网格排列示例",
|
||||
"value": {
|
||||
"images": [
|
||||
"https://picsum.photos/200/200",
|
||||
"https://picsum.photos/200/200",
|
||||
"https://picsum.photos/200/200",
|
||||
"https://picsum.photos/200/200",
|
||||
"https://picsum.photos/200/200"
|
||||
],
|
||||
"direction": "grid",
|
||||
"outputFormat": "jpeg",
|
||||
"maxWidth": 1024,
|
||||
"maxHeight": 1024,
|
||||
"quality": 75
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "异步执行模板 (增强版)",
|
||||
"tags": [
|
||||
"AI模板系统 (增强版)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/enhanced/templates/{executionId}/status": {
|
||||
"get": {
|
||||
"operationId": "EnhancedTemplateController_getExecutionStatus",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "executionId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "图片拼接成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ImageCompositionResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误"
|
||||
},
|
||||
"401": {
|
||||
"description": "未授权访问"
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误"
|
||||
}
|
||||
},
|
||||
"summary": "查询执行状态 (增强版)",
|
||||
"summary": "图片拼接",
|
||||
"tags": [
|
||||
"AI模板系统 (增强版)"
|
||||
"图片处理"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -1778,7 +1809,7 @@
|
||||
],
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:3004",
|
||||
"url": "http://localhost:3003",
|
||||
"description": "开发环境"
|
||||
},
|
||||
{
|
||||
@@ -1850,6 +1881,11 @@
|
||||
"type": "string",
|
||||
"description": "输入图片URL",
|
||||
"example": "https://cdn.example.com/upload/image.jpg"
|
||||
},
|
||||
"auditTaskId": {
|
||||
"type": "string",
|
||||
"description": "审核任务ID(可选)",
|
||||
"example": "audit_task_12345"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -2227,6 +2263,115 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ImageCompositionDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"images": {
|
||||
"description": "图片URL或base64数据列表",
|
||||
"example": [
|
||||
"https://picsum.photos/300/200",
|
||||
"https://picsum.photos/300/200",
|
||||
"https://picsum.photos/300/200"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"description": "排列方向",
|
||||
"enum": [
|
||||
"horizontal",
|
||||
"vertical",
|
||||
"grid"
|
||||
],
|
||||
"example": "horizontal"
|
||||
},
|
||||
"outputFormat": {
|
||||
"type": "string",
|
||||
"description": "输出格式",
|
||||
"enum": [
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp"
|
||||
],
|
||||
"example": "png",
|
||||
"default": "png"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "number",
|
||||
"description": "最大输出宽度(像素),超过会等比压缩",
|
||||
"example": 1920,
|
||||
"default": 1920,
|
||||
"minimum": 100,
|
||||
"maximum": 4096
|
||||
},
|
||||
"maxHeight": {
|
||||
"type": "number",
|
||||
"description": "最大输出高度(像素),超过会等比压缩",
|
||||
"example": 1080,
|
||||
"default": 1080,
|
||||
"minimum": 100,
|
||||
"maximum": 4096
|
||||
},
|
||||
"quality": {
|
||||
"type": "number",
|
||||
"description": "图片质量(1-100),仅对JPEG/WebP有效",
|
||||
"example": 80,
|
||||
"default": 80,
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"images",
|
||||
"direction"
|
||||
]
|
||||
},
|
||||
"ImageCompositionResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imageUrl": {
|
||||
"type": "string",
|
||||
"description": "拼接后的图片URL",
|
||||
"example": "https://s3.amazonaws.com/bucket/composed-image-1234567890.png"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "图片格式",
|
||||
"example": "png"
|
||||
},
|
||||
"size": {
|
||||
"type": "number",
|
||||
"description": "图片大小(字节)",
|
||||
"example": 12345
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"description": "画布宽度",
|
||||
"example": 800
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"description": "画布高度",
|
||||
"example": 600
|
||||
},
|
||||
"processingTime": {
|
||||
"type": "number",
|
||||
"description": "处理耗时(毫秒)",
|
||||
"example": 150
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"imageUrl",
|
||||
"format",
|
||||
"size",
|
||||
"width",
|
||||
"height",
|
||||
"processingTime"
|
||||
]
|
||||
},
|
||||
"PlatformType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -41,9 +41,11 @@
|
||||
"axios": "^1.11.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"form-data": "^4.0.4",
|
||||
"mysql2": "^3.9.7",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.34.4",
|
||||
"typeorm": "^0.3.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -55,6 +57,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
|
||||
346
pnpm-lock.yaml
generated
346
pnpm-lock.yaml
generated
@@ -32,12 +32,6 @@ importers:
|
||||
'@nestjs/typeorm':
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))
|
||||
'@open-dy/open_api_credential':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
'@open-dy/open_api_sdk':
|
||||
specifier: 1.1.2
|
||||
version: 1.1.2
|
||||
axios:
|
||||
specifier: ^1.11.0
|
||||
version: 1.11.0
|
||||
@@ -47,6 +41,9 @@ importers:
|
||||
class-validator:
|
||||
specifier: ^0.14.2
|
||||
version: 0.14.2
|
||||
form-data:
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4
|
||||
mysql2:
|
||||
specifier: ^3.9.7
|
||||
version: 3.14.4
|
||||
@@ -56,6 +53,9 @@ importers:
|
||||
rxjs:
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.2
|
||||
sharp:
|
||||
specifier: ^0.34.4
|
||||
version: 0.34.4
|
||||
typeorm:
|
||||
specifier: ^0.3.20
|
||||
version: 0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2))
|
||||
@@ -84,6 +84,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.10.7
|
||||
version: 22.18.0
|
||||
'@types/sharp':
|
||||
specifier: ^0.32.0
|
||||
version: 0.32.0
|
||||
'@types/supertest':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.3
|
||||
@@ -132,9 +135,6 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@alicloud/tea-typescript@1.8.0':
|
||||
resolution: {integrity: sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -400,6 +400,132 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@img/colour@1.0.0':
|
||||
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.4':
|
||||
resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.4':
|
||||
resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.3':
|
||||
resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.3':
|
||||
resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.3':
|
||||
resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.3':
|
||||
resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.3':
|
||||
resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.3':
|
||||
resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.3':
|
||||
resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
|
||||
resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
|
||||
resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.4':
|
||||
resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.4':
|
||||
resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.4':
|
||||
resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.4':
|
||||
resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.4':
|
||||
resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.4':
|
||||
resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.4':
|
||||
resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-wasm32@0.34.4':
|
||||
resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.4':
|
||||
resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.4':
|
||||
resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.4':
|
||||
resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@inquirer/checkbox@4.2.2':
|
||||
resolution: {integrity: sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -816,15 +942,6 @@ packages:
|
||||
engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'}
|
||||
hasBin: true
|
||||
|
||||
'@open-dy/open_api_credential@1.0.0':
|
||||
resolution: {integrity: sha512-MmLrJL2QFYHV3RNJ2/hQEXcjzBCa6mtklW+AJXG+xS/x8cJ7/AgW3wQjvNwJ0oMRyxch1nQ10h9EnOGr1kew0g==}
|
||||
|
||||
'@open-dy/open_api_sdk@1.1.2':
|
||||
resolution: {integrity: sha512-Z+TzDxzFf+25Hw6E3crKLNRrFrN45I+BrfmnxPHm0QpGHO284FmvbEaug1lXq480jD32kPZMjJQR+r5d8LYW9g==}
|
||||
|
||||
'@open-dy/open_api_util@1.0.1':
|
||||
resolution: {integrity: sha512-vgUTsyNqB0mtn0LclEr7J2E0eoXTHym1Xh4hO5gXXGYS4MoZGpu9IbYlkR4LMVUSOmJs5ZhRxmNqvEt4D/5fLg==}
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
|
||||
@@ -936,12 +1053,6 @@ packages:
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/node@12.20.55':
|
||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||
|
||||
'@types/node@20.19.13':
|
||||
resolution: {integrity: sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==}
|
||||
|
||||
'@types/node@22.18.0':
|
||||
resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==}
|
||||
|
||||
@@ -957,6 +1068,10 @@ packages:
|
||||
'@types/serve-static@1.15.8':
|
||||
resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==}
|
||||
|
||||
'@types/sharp@0.32.0':
|
||||
resolution: {integrity: sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==}
|
||||
deprecated: This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/stack-utils@2.0.3':
|
||||
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
||||
|
||||
@@ -1076,49 +1191,41 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -1620,6 +1727,10 @@ packages:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
detect-libc@2.1.1:
|
||||
resolution: {integrity: sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-newline@3.1.0:
|
||||
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2037,9 +2148,6 @@ packages:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
httpx@2.3.3:
|
||||
resolution: {integrity: sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==}
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -2872,6 +2980,10 @@ packages:
|
||||
engines: {node: '>= 0.10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.34.4:
|
||||
resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3397,13 +3509,6 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alicloud/tea-typescript@1.8.0':
|
||||
dependencies:
|
||||
'@types/node': 12.20.55
|
||||
httpx: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
@@ -3713,6 +3818,94 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@img/colour@1.0.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.4':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.5.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.4':
|
||||
optional: true
|
||||
|
||||
'@inquirer/checkbox@4.2.2(@types/node@22.18.0)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.0(@types/node@22.18.0)
|
||||
@@ -4257,27 +4450,6 @@ snapshots:
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
|
||||
'@open-dy/open_api_credential@1.0.0':
|
||||
dependencies:
|
||||
axios: 1.11.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
'@open-dy/open_api_sdk@1.1.2':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
'@open-dy/open_api_credential': 1.0.0
|
||||
'@open-dy/open_api_util': 1.0.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
'@open-dy/open_api_util@1.0.1':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
@@ -4408,12 +4580,6 @@ snapshots:
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/node@12.20.55': {}
|
||||
|
||||
'@types/node@20.19.13':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@22.18.0':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@@ -4433,6 +4599,10 @@ snapshots:
|
||||
'@types/node': 22.18.0
|
||||
'@types/send': 0.17.5
|
||||
|
||||
'@types/sharp@0.32.0':
|
||||
dependencies:
|
||||
sharp: 0.34.4
|
||||
|
||||
'@types/stack-utils@2.0.3': {}
|
||||
|
||||
'@types/superagent@8.1.9':
|
||||
@@ -5103,6 +5273,8 @@ snapshots:
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
detect-libc@2.1.1: {}
|
||||
|
||||
detect-newline@3.1.0: {}
|
||||
|
||||
dezalgo@1.0.4:
|
||||
@@ -5577,13 +5749,6 @@ snapshots:
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
|
||||
httpx@2.3.3:
|
||||
dependencies:
|
||||
'@types/node': 20.19.13
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
@@ -6542,6 +6707,35 @@ snapshots:
|
||||
safe-buffer: 5.2.1
|
||||
to-buffer: 1.2.1
|
||||
|
||||
sharp@0.34.4:
|
||||
dependencies:
|
||||
'@img/colour': 1.0.0
|
||||
detect-libc: 2.1.1
|
||||
semver: 7.7.2
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.4
|
||||
'@img/sharp-darwin-x64': 0.34.4
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.3
|
||||
'@img/sharp-libvips-linux-arm': 1.2.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.3
|
||||
'@img/sharp-libvips-linux-x64': 1.2.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.3
|
||||
'@img/sharp-linux-arm': 0.34.4
|
||||
'@img/sharp-linux-arm64': 0.34.4
|
||||
'@img/sharp-linux-ppc64': 0.34.4
|
||||
'@img/sharp-linux-s390x': 0.34.4
|
||||
'@img/sharp-linux-x64': 0.34.4
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.4
|
||||
'@img/sharp-linuxmusl-x64': 0.34.4
|
||||
'@img/sharp-wasm32': 0.34.4
|
||||
'@img/sharp-win32-arm64': 0.34.4
|
||||
'@img/sharp-win32-ia32': 0.34.4
|
||||
'@img/sharp-win32-x64': 0.34.4
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
@@ -17,6 +17,8 @@ import { UserSubscriptionEntity } from './entities/user-subscription.entity';
|
||||
import { AdWatchEntity } from './entities/ad-watch.entity';
|
||||
import { N8nTemplateFactoryService } from './services/n8n-template-factory.service';
|
||||
import { TemplateController } from './controllers/template.controller';
|
||||
import { ImageCompositionController } from './controllers/image-composition.controller';
|
||||
import { ImageCompositionService } from './services/image-composition.service';
|
||||
import { PlatformModule } from './platform/platform.module';
|
||||
import { ContentModerationModule } from './content-moderation/content-moderation.module';
|
||||
import { PaymentModule } from './payment/payment.module';
|
||||
@@ -54,8 +56,8 @@ import { PaymentModule } from './payment/payment.module';
|
||||
ContentModerationModule,
|
||||
PaymentModule,
|
||||
],
|
||||
controllers: [AppController, TemplateController],
|
||||
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService],
|
||||
controllers: [AppController, TemplateController, ImageCompositionController],
|
||||
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService, ImageCompositionService],
|
||||
exports: [N8nTemplateFactoryService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ImageCompositionController } from '../image-composition.controller';
|
||||
import { ImageCompositionService } from '../../services/image-composition.service';
|
||||
import {
|
||||
ImageCompositionDto,
|
||||
ArrangementDirection,
|
||||
OutputFormat,
|
||||
} from '../../dto/image-composition.dto';
|
||||
|
||||
describe('ImageCompositionController', () => {
|
||||
let controller: ImageCompositionController;
|
||||
let service: ImageCompositionService;
|
||||
|
||||
const mockHttpService = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ImageCompositionController],
|
||||
providers: [
|
||||
ImageCompositionService,
|
||||
{
|
||||
provide: HttpService,
|
||||
useValue: mockHttpService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<ImageCompositionController>(
|
||||
ImageCompositionController,
|
||||
);
|
||||
service = module.get<ImageCompositionService>(ImageCompositionService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('composeImages', () => {
|
||||
it('should validate minimum requirements', async () => {
|
||||
const invalidDto: ImageCompositionDto = {
|
||||
images: [], // 空数组应该抛出错误
|
||||
direction: ArrangementDirection.HORIZONTAL,
|
||||
outputFormat: OutputFormat.PNG,
|
||||
};
|
||||
|
||||
await expect(controller.composeImages(invalidDto)).rejects.toThrow(
|
||||
'至少需要提供一张图片',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate image count limit', async () => {
|
||||
const images = Array.from(
|
||||
{ length: 25 },
|
||||
(_, i) => `https://example.com/test${i}.jpg`,
|
||||
);
|
||||
|
||||
const invalidDto: ImageCompositionDto = {
|
||||
images, // 超过20张图片限制
|
||||
direction: ArrangementDirection.HORIZONTAL,
|
||||
outputFormat: OutputFormat.PNG,
|
||||
};
|
||||
|
||||
await expect(controller.composeImages(invalidDto)).rejects.toThrow(
|
||||
'图片数量不能超过20张',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate arrangement direction', async () => {
|
||||
const invalidDto: any = {
|
||||
images: ['https://example.com/test.jpg'],
|
||||
direction: 'invalid-direction', // 无效的排列方向
|
||||
outputFormat: OutputFormat.PNG,
|
||||
};
|
||||
|
||||
await expect(controller.composeImages(invalidDto)).rejects.toThrow(
|
||||
'排列方向必须是以下值之一',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate image URL format', async () => {
|
||||
const invalidDto: ImageCompositionDto = {
|
||||
images: ['invalid-url'], // 无效的URL格式
|
||||
direction: ArrangementDirection.HORIZONTAL,
|
||||
outputFormat: OutputFormat.PNG,
|
||||
};
|
||||
|
||||
await expect(controller.composeImages(invalidDto)).rejects.toThrow(
|
||||
'图片必须是有效的HTTP(S) URL或base64格式',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
1
src/controllers/__tests__/image-composition.example.ts
Normal file
1
src/controllers/__tests__/image-composition.example.ts
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
215
src/controllers/image-composition.controller.ts
Normal file
215
src/controllers/image-composition.controller.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse as SwaggerApiResponse,
|
||||
ApiBody,
|
||||
} from '@nestjs/swagger';
|
||||
import { ImageCompositionService } from '../services/image-composition.service';
|
||||
import {
|
||||
ImageCompositionDto,
|
||||
ImageCompositionResponseDto,
|
||||
ArrangementDirection,
|
||||
} from '../dto/image-composition.dto';
|
||||
import { ApiCommonResponses } from '../decorators/api-common-responses.decorator';
|
||||
import { PlatformAuthGuard } from '../platform/guards/platform-auth.guard';
|
||||
import { ResponseUtil, ApiResponse } from '../utils/response.util';
|
||||
|
||||
@ApiTags('图片处理')
|
||||
@Controller('image-composition')
|
||||
@ApiCommonResponses()
|
||||
export class ImageCompositionController {
|
||||
constructor(
|
||||
private readonly imageCompositionService: ImageCompositionService,
|
||||
) {}
|
||||
|
||||
@Post('compose')
|
||||
@UseGuards(PlatformAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: '图片拼接',
|
||||
description: '将多张图片按指定位置拼接到一个画布上,支持层级、透明度等设置',
|
||||
})
|
||||
@ApiBody({
|
||||
type: ImageCompositionDto,
|
||||
description: '图片拼接配置 - 简化版本',
|
||||
examples: {
|
||||
horizontal: {
|
||||
summary: '水平排列示例',
|
||||
value: {
|
||||
images: [
|
||||
'https://picsum.photos/300/200',
|
||||
'https://picsum.photos/300/200',
|
||||
'https://picsum.photos/300/200'
|
||||
],
|
||||
direction: 'horizontal',
|
||||
outputFormat: 'jpeg',
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
quality: 80
|
||||
},
|
||||
},
|
||||
vertical: {
|
||||
summary: '垂直排列示例',
|
||||
value: {
|
||||
images: [
|
||||
'https://picsum.photos/200/300',
|
||||
'https://picsum.photos/200/300',
|
||||
'https://picsum.photos/200/300'
|
||||
],
|
||||
direction: 'vertical',
|
||||
outputFormat: 'webp',
|
||||
maxWidth: 800,
|
||||
maxHeight: 1200,
|
||||
quality: 85
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
summary: '网格排列示例',
|
||||
value: {
|
||||
images: [
|
||||
'https://picsum.photos/200/200',
|
||||
'https://picsum.photos/200/200',
|
||||
'https://picsum.photos/200/200',
|
||||
'https://picsum.photos/200/200',
|
||||
'https://picsum.photos/200/200'
|
||||
],
|
||||
direction: 'grid',
|
||||
outputFormat: 'jpeg',
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
quality: 75
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 200,
|
||||
description: '图片拼接成功',
|
||||
type: ImageCompositionResponseDto,
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 400,
|
||||
description: '请求参数错误',
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 401,
|
||||
description: '未授权访问',
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 500,
|
||||
description: '服务器内部错误',
|
||||
})
|
||||
async composeImages(
|
||||
@Body() compositionDto: ImageCompositionDto,
|
||||
): Promise<ApiResponse<ImageCompositionResponseDto>> {
|
||||
try {
|
||||
// 基础参数验证
|
||||
this.validateCompositionRequest(compositionDto);
|
||||
|
||||
const result = await this.imageCompositionService.composeImages(compositionDto);
|
||||
|
||||
return ResponseUtil.success(result, '图片拼接成功');
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
`图片拼接失败: ${error.message}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证拼接请求参数 - 简化版本
|
||||
*/
|
||||
private validateCompositionRequest(dto: ImageCompositionDto): void {
|
||||
// 验证图片数量
|
||||
if (!dto.images || dto.images.length === 0) {
|
||||
throw new HttpException(
|
||||
'至少需要提供一张图片',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (dto.images.length > 20) {
|
||||
throw new HttpException(
|
||||
'图片数量不能超过20张',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// 验证排列方向
|
||||
const validDirections = Object.values(ArrangementDirection);
|
||||
if (!validDirections.includes(dto.direction)) {
|
||||
throw new HttpException(
|
||||
`排列方向必须是以下值之一: ${validDirections.join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// 验证图片URL格式
|
||||
for (const imageUrl of dto.images) {
|
||||
if (!imageUrl || typeof imageUrl !== 'string') {
|
||||
throw new HttpException(
|
||||
'图片URL不能为空',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否为有效的URL或base64格式
|
||||
const isValidUrl = this.isValidImageUrl(imageUrl);
|
||||
const isValidBase64 = imageUrl.startsWith('data:image/');
|
||||
|
||||
if (!isValidUrl && !isValidBase64) {
|
||||
throw new HttpException(
|
||||
'图片必须是有效的HTTP(S) URL或base64格式',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证尺寸参数
|
||||
if (dto.maxWidth !== undefined && (dto.maxWidth < 100 || dto.maxWidth > 4096)) {
|
||||
throw new HttpException(
|
||||
'最大宽度必须在100-4096像素之间',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (dto.maxHeight !== undefined && (dto.maxHeight < 100 || dto.maxHeight > 4096)) {
|
||||
throw new HttpException(
|
||||
'最大高度必须在100-4096像素之间',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// 验证质量参数
|
||||
if (dto.quality !== undefined && (dto.quality < 1 || dto.quality > 100)) {
|
||||
throw new HttpException(
|
||||
'图片质量必须在1-100之间',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证图片URL格式
|
||||
*/
|
||||
private isValidImageUrl(url: string): boolean {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/dto/image-composition.dto.ts
Normal file
117
src/dto/image-composition.dto.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsArray, IsEnum, IsOptional, IsNumber, Min, Max } from 'class-validator';
|
||||
|
||||
/**
|
||||
* 排列方向枚举
|
||||
*/
|
||||
export enum ArrangementDirection {
|
||||
HORIZONTAL = 'horizontal', // 水平排列
|
||||
VERTICAL = 'vertical', // 垂直排列
|
||||
GRID = 'grid', // 网格排列(自动计算行列)
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出格式枚举
|
||||
*/
|
||||
export enum OutputFormat {
|
||||
JPEG = 'jpeg',
|
||||
PNG = 'png',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片拼接请求DTO - 简化版本
|
||||
*/
|
||||
export class ImageCompositionDto {
|
||||
@ApiProperty({
|
||||
description: '图片URL或base64数据列表',
|
||||
example: [
|
||||
'https://picsum.photos/300/200',
|
||||
'https://picsum.photos/300/200',
|
||||
'https://picsum.photos/300/200'
|
||||
]
|
||||
})
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
images: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: '排列方向',
|
||||
enum: ArrangementDirection,
|
||||
example: ArrangementDirection.HORIZONTAL
|
||||
})
|
||||
@IsEnum(ArrangementDirection)
|
||||
direction: ArrangementDirection;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '输出格式',
|
||||
enum: OutputFormat,
|
||||
example: OutputFormat.PNG,
|
||||
default: OutputFormat.PNG
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(OutputFormat)
|
||||
outputFormat?: OutputFormat = OutputFormat.PNG;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '最大输出宽度(像素),超过会等比压缩',
|
||||
example: 1920,
|
||||
default: 1920,
|
||||
minimum: 100,
|
||||
maximum: 4096
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(100)
|
||||
@Max(4096)
|
||||
maxWidth?: number = 1920;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '最大输出高度(像素),超过会等比压缩',
|
||||
example: 1080,
|
||||
default: 1080,
|
||||
minimum: 100,
|
||||
maximum: 4096
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(100)
|
||||
@Max(4096)
|
||||
maxHeight?: number = 1080;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '图片质量(1-100),仅对JPEG/WebP有效',
|
||||
example: 80,
|
||||
default: 80,
|
||||
minimum: 1,
|
||||
maximum: 100
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
quality?: number = 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片拼接响应DTO
|
||||
*/
|
||||
export class ImageCompositionResponseDto {
|
||||
@ApiProperty({ description: '拼接后的图片URL', example: 'https://s3.amazonaws.com/bucket/composed-image-1234567890.png' })
|
||||
imageUrl: string;
|
||||
|
||||
@ApiProperty({ description: '图片格式', example: 'png' })
|
||||
format: string;
|
||||
|
||||
@ApiProperty({ description: '图片大小(字节)', example: 12345 })
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ description: '画布宽度', example: 800 })
|
||||
width: number;
|
||||
|
||||
@ApiProperty({ description: '画布高度', example: 600 })
|
||||
height: number;
|
||||
|
||||
@ApiProperty({ description: '处理耗时(毫秒)', example: 150 })
|
||||
processingTime: number;
|
||||
}
|
||||
369
src/services/image-composition.service.ts
Normal file
369
src/services/image-composition.service.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||
import sharp from 'sharp';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import FormData from 'form-data';
|
||||
import {
|
||||
ImageCompositionDto,
|
||||
ImageCompositionResponseDto,
|
||||
ArrangementDirection,
|
||||
OutputFormat,
|
||||
} from '../dto/image-composition.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ImageCompositionService {
|
||||
private readonly logger = new Logger(ImageCompositionService.name);
|
||||
|
||||
constructor(private readonly httpService: HttpService) {}
|
||||
|
||||
/**
|
||||
* 拼接图片 - 简化版本
|
||||
*/
|
||||
async composeImages(
|
||||
compositionDto: ImageCompositionDto,
|
||||
): Promise<ImageCompositionResponseDto> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
this.logger.log('开始图片拼接处理');
|
||||
|
||||
// 加载所有图片并获取尺寸信息,同时进行预处理
|
||||
const imageInfos = await Promise.all(
|
||||
compositionDto.images.map(async (imageUrl) => {
|
||||
const buffer = await this.loadImage(imageUrl);
|
||||
let processedBuffer = buffer;
|
||||
|
||||
// 压缩单张图片如果太大
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
const originalWidth = metadata.width || 0;
|
||||
const originalHeight = metadata.height || 0;
|
||||
|
||||
// 如果单张图片超过800x800,先压缩
|
||||
if (originalWidth > 800 || originalHeight > 800) {
|
||||
processedBuffer = await sharp(buffer)
|
||||
.resize(800, 800, { fit: 'inside', withoutEnlargement: true })
|
||||
.jpeg({ quality: 85 })
|
||||
.toBuffer();
|
||||
|
||||
const newMetadata = await sharp(processedBuffer).metadata();
|
||||
this.logger.log(`压缩图片: ${originalWidth}x${originalHeight} -> ${newMetadata.width}x${newMetadata.height}`);
|
||||
|
||||
return {
|
||||
buffer: processedBuffer,
|
||||
width: newMetadata.width || 0,
|
||||
height: newMetadata.height || 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buffer: processedBuffer,
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// 根据排列方向计算画布尺寸和图片位置
|
||||
const layout = this.calculateLayout(imageInfos, compositionDto.direction);
|
||||
|
||||
// 创建画布
|
||||
const canvas = sharp({
|
||||
create: {
|
||||
width: layout.canvasWidth,
|
||||
height: layout.canvasHeight,
|
||||
channels: 4,
|
||||
background: '#ffffff',
|
||||
},
|
||||
});
|
||||
|
||||
// 准备所有图片的composite参数
|
||||
const compositeInputs = await Promise.all(
|
||||
imageInfos.map(async (imageInfo, index) => {
|
||||
const position = layout.positions[index];
|
||||
let processedImage = sharp(imageInfo.buffer);
|
||||
|
||||
// 如果需要调整尺寸
|
||||
if (
|
||||
position.width !== imageInfo.width ||
|
||||
position.height !== imageInfo.height
|
||||
) {
|
||||
processedImage = processedImage.resize(
|
||||
position.width,
|
||||
position.height,
|
||||
{ fit: 'fill' },
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
input: await processedImage.png().toBuffer(),
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// 执行图片合成
|
||||
let finalImage = canvas.composite(compositeInputs);
|
||||
|
||||
// 检查是否需要压缩最终图片
|
||||
const maxWidth = compositionDto.maxWidth || 1920;
|
||||
const maxHeight = compositionDto.maxHeight || 1080;
|
||||
|
||||
if (layout.canvasWidth > maxWidth || layout.canvasHeight > maxHeight) {
|
||||
this.logger.log(`最终图片需要压缩: ${layout.canvasWidth}x${layout.canvasHeight} -> 最大${maxWidth}x${maxHeight}`);
|
||||
finalImage = finalImage.resize(maxWidth, maxHeight, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
});
|
||||
}
|
||||
|
||||
// 根据输出格式处理
|
||||
const quality = compositionDto.quality || 80;
|
||||
switch (compositionDto.outputFormat) {
|
||||
case OutputFormat.JPEG:
|
||||
finalImage = finalImage.jpeg({ quality, progressive: true });
|
||||
break;
|
||||
case OutputFormat.WEBP:
|
||||
finalImage = finalImage.webp({ quality, effort: 6 });
|
||||
break;
|
||||
case OutputFormat.PNG:
|
||||
default:
|
||||
// PNG格式使用适度压缩
|
||||
finalImage = finalImage.png({ compressionLevel: 8, adaptiveFiltering: true });
|
||||
break;
|
||||
}
|
||||
|
||||
const resultBuffer = await finalImage.toBuffer();
|
||||
const metadata = await sharp(resultBuffer).metadata();
|
||||
|
||||
const processingTime = Date.now() - startTime;
|
||||
this.logger.log(`图片拼接完成,耗时: ${processingTime}ms`);
|
||||
|
||||
// 上传图片到S3并获取URL
|
||||
const imageUrl = await this.uploadToS3(
|
||||
resultBuffer,
|
||||
compositionDto.outputFormat || 'png',
|
||||
);
|
||||
|
||||
return {
|
||||
imageUrl,
|
||||
format: compositionDto.outputFormat || 'png',
|
||||
size: resultBuffer.length,
|
||||
width: metadata.width || layout.canvasWidth,
|
||||
height: metadata.height || layout.canvasHeight,
|
||||
processingTime,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('图片拼接失败', error);
|
||||
throw new BadRequestException(`图片拼接失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据排列方向计算布局
|
||||
*/
|
||||
private calculateLayout(
|
||||
imageInfos: Array<{ width: number; height: number }>,
|
||||
direction: ArrangementDirection,
|
||||
) {
|
||||
const positions: Array<{
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}> = [];
|
||||
let canvasWidth = 0;
|
||||
let canvasHeight = 0;
|
||||
|
||||
switch (direction) {
|
||||
case ArrangementDirection.HORIZONTAL:
|
||||
// 水平排列:所有图片保持原始高度,宽度累加
|
||||
const maxHeight = Math.max(...imageInfos.map((img) => img.height));
|
||||
canvasHeight = maxHeight;
|
||||
|
||||
let currentX = 0;
|
||||
imageInfos.forEach((imageInfo) => {
|
||||
positions.push({
|
||||
x: currentX,
|
||||
y: Math.floor((maxHeight - imageInfo.height) / 2), // 垂直居中
|
||||
width: imageInfo.width,
|
||||
height: imageInfo.height,
|
||||
});
|
||||
currentX += imageInfo.width;
|
||||
});
|
||||
canvasWidth = currentX;
|
||||
break;
|
||||
|
||||
case ArrangementDirection.VERTICAL:
|
||||
// 垂直排列:所有图片保持原始宽度,高度累加
|
||||
const maxWidth = Math.max(...imageInfos.map((img) => img.width));
|
||||
canvasWidth = maxWidth;
|
||||
|
||||
let currentY = 0;
|
||||
imageInfos.forEach((imageInfo) => {
|
||||
positions.push({
|
||||
x: Math.floor((maxWidth - imageInfo.width) / 2), // 水平居中
|
||||
y: currentY,
|
||||
width: imageInfo.width,
|
||||
height: imageInfo.height,
|
||||
});
|
||||
currentY += imageInfo.height;
|
||||
});
|
||||
canvasHeight = currentY;
|
||||
break;
|
||||
|
||||
case ArrangementDirection.GRID:
|
||||
// 网格排列:自动计算行列数
|
||||
const imageCount = imageInfos.length;
|
||||
const cols = Math.ceil(Math.sqrt(imageCount));
|
||||
const rows = Math.ceil(imageCount / cols);
|
||||
|
||||
// 计算每个格子的尺寸(使用最大图片尺寸)
|
||||
const cellWidth = Math.max(...imageInfos.map((img) => img.width));
|
||||
const cellHeight = Math.max(...imageInfos.map((img) => img.height));
|
||||
|
||||
canvasWidth = cols * cellWidth;
|
||||
canvasHeight = rows * cellHeight;
|
||||
|
||||
imageInfos.forEach((imageInfo, index) => {
|
||||
const row = Math.floor(index / cols);
|
||||
const col = index % cols;
|
||||
|
||||
positions.push({
|
||||
x: col * cellWidth + Math.floor((cellWidth - imageInfo.width) / 2),
|
||||
y:
|
||||
row * cellHeight +
|
||||
Math.floor((cellHeight - imageInfo.height) / 2),
|
||||
width: imageInfo.width,
|
||||
height: imageInfo.height,
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new BadRequestException(`不支持的排列方向: ${direction}`);
|
||||
}
|
||||
|
||||
return { canvasWidth, canvasHeight, positions };
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载图片
|
||||
* 支持URL和base64格式
|
||||
*/
|
||||
private async loadImage(imageUrl: string): Promise<Buffer> {
|
||||
try {
|
||||
// 检查是否为base64格式
|
||||
if (imageUrl.startsWith('data:image/')) {
|
||||
const base64Data = imageUrl.split(',')[1];
|
||||
return Buffer.from(base64Data, 'base64');
|
||||
}
|
||||
|
||||
// 检查是否为HTTP(S) URL
|
||||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get(imageUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 10000, // 10秒超时
|
||||
maxContentLength: 10 * 1024 * 1024, // 最大10MB
|
||||
}),
|
||||
);
|
||||
return Buffer.from(response.data);
|
||||
}
|
||||
|
||||
throw new Error('不支持的图片格式,请提供HTTP(S) URL或base64数据');
|
||||
} catch (error) {
|
||||
this.logger.error(`加载图片失败: ${imageUrl}`, error);
|
||||
throw new BadRequestException(`加载图片失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片到S3
|
||||
*/
|
||||
private async uploadToS3(
|
||||
imageBuffer: Buffer,
|
||||
format: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
this.logger.log('开始上传图片到S3');
|
||||
|
||||
// 创建FormData
|
||||
const formData = new FormData();
|
||||
const fileName = `composed-image-${Date.now()}.${format}`;
|
||||
|
||||
formData.append('file', imageBuffer, {
|
||||
filename: fileName,
|
||||
contentType: `image/${format}`,
|
||||
});
|
||||
|
||||
// 上传到S3
|
||||
const uploadResponse = await firstValueFrom(
|
||||
this.httpService.post(
|
||||
'https://bowongai-prod--text-video-agent-fastapi-app.modal.run/api/file/upload/s3',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
},
|
||||
timeout: 30000, // 30秒超时
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 记录S3上传状态
|
||||
this.logger.log('S3上传完成,状态:', uploadResponse.data.status);
|
||||
|
||||
// 检查响应并返回URL
|
||||
if (
|
||||
uploadResponse.data &&
|
||||
uploadResponse.data.status &&
|
||||
uploadResponse.data.data
|
||||
) {
|
||||
this.logger.log(`图片上传成功: ${uploadResponse.data.data}`);
|
||||
return uploadResponse.data.data;
|
||||
} else {
|
||||
this.logger.error('S3上传响应格式异常,响应数据:', uploadResponse.data);
|
||||
throw new Error('S3上传响应格式异常');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('上传图片到S3失败,详细错误:', {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
config: {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
headers: error.config?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
// 如果S3上传失败,返回base64格式作为备选方案
|
||||
this.logger.warn('S3上传失败,使用base64格式返回');
|
||||
return `data:image/${format};base64,${imageBuffer.toString('base64')}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证图片格式
|
||||
*/
|
||||
private async validateImage(buffer: Buffer): Promise<void> {
|
||||
try {
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
if (!metadata.format) {
|
||||
throw new Error('无法识别的图片格式');
|
||||
}
|
||||
|
||||
// 检查图片尺寸限制
|
||||
if (metadata.width && metadata.width > 4096) {
|
||||
throw new Error('图片宽度超过限制(最大4096px)');
|
||||
}
|
||||
if (metadata.height && metadata.height > 4096) {
|
||||
throw new Error('图片高度超过限制(最大4096px)');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new BadRequestException(`图片验证失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
253
swagger.json
253
swagger.json
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"/api/v1/templates/code/{code}/execute": {
|
||||
"post": {
|
||||
"description": "\n 根据模板代码执行AI生成任务,支持同步和异步两种审核模式:\n - 同步模式:立即返回审核结果,若通过则开始执行,返回 executionId 和 taskId\n - 异步模式:返回 executionId 和 auditTaskId,等待审核回调完成后开始执行\n \n ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,同步模式时返回\n - auditTaskId: 审核服务任务ID,异步模式时返回\n ",
|
||||
"description": "\n 根据模板代码执行AI生成任务,直接执行模板无需后端审核。\n \n 参数说明:\n - imageUrl: 输入图片URL(必需)\n - auditTaskId: 前端审核任务ID(可选),用于追踪审核记录\n \n 返回ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,用于追踪模板执行状态\n - auditTaskId: 前端传入的审核任务ID\n ",
|
||||
"operationId": "TemplateController_executeTemplateByCode",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -131,7 +131,9 @@
|
||||
"data": {
|
||||
"executionId": 123,
|
||||
"taskId": "n8n_task_456",
|
||||
"status": "processing"
|
||||
"auditTaskId": "audit_task_789",
|
||||
"status": "processing",
|
||||
"message": "模板执行已启动"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,20 +160,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "图片审核未通过",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"example": {
|
||||
"code": 403,
|
||||
"message": "图片审核未通过: 包含不当内容",
|
||||
"data": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "任务数量限制",
|
||||
"content": {
|
||||
@@ -1094,52 +1082,95 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/enhanced/templates/code/{code}/execute": {
|
||||
"/api/v1/image-composition/compose": {
|
||||
"post": {
|
||||
"description": "统一异步模式:先提交审核,然后通过回调继续执行模板。支持实时状态查询。",
|
||||
"operationId": "EnhancedTemplateController_executeTemplateByCode",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "code",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"description": "将多张图片按指定位置拼接到一个画布上,支持层级、透明度等设置",
|
||||
"operationId": "ImageCompositionController_composeImages",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"description": "图片拼接配置 - 简化版本",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ImageCompositionDto"
|
||||
},
|
||||
"examples": {
|
||||
"horizontal": {
|
||||
"summary": "水平排列示例",
|
||||
"value": {
|
||||
"images": [
|
||||
"https://picsum.photos/300/200",
|
||||
"https://picsum.photos/300/200",
|
||||
"https://picsum.photos/300/200"
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"outputFormat": "jpeg",
|
||||
"maxWidth": 1920,
|
||||
"maxHeight": 1080,
|
||||
"quality": 80
|
||||
}
|
||||
},
|
||||
"vertical": {
|
||||
"summary": "垂直排列示例",
|
||||
"value": {
|
||||
"images": [
|
||||
"https://picsum.photos/200/300",
|
||||
"https://picsum.photos/200/300",
|
||||
"https://picsum.photos/200/300"
|
||||
],
|
||||
"direction": "vertical",
|
||||
"outputFormat": "webp",
|
||||
"maxWidth": 800,
|
||||
"maxHeight": 1200,
|
||||
"quality": 85
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"summary": "网格排列示例",
|
||||
"value": {
|
||||
"images": [
|
||||
"https://picsum.photos/200/200",
|
||||
"https://picsum.photos/200/200",
|
||||
"https://picsum.photos/200/200",
|
||||
"https://picsum.photos/200/200",
|
||||
"https://picsum.photos/200/200"
|
||||
],
|
||||
"direction": "grid",
|
||||
"outputFormat": "jpeg",
|
||||
"maxWidth": 1024,
|
||||
"maxHeight": 1024,
|
||||
"quality": 75
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "异步执行模板 (增强版)",
|
||||
"tags": [
|
||||
"AI模板系统 (增强版)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/enhanced/templates/{executionId}/status": {
|
||||
"get": {
|
||||
"operationId": "EnhancedTemplateController_getExecutionStatus",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "executionId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "图片拼接成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ImageCompositionResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误"
|
||||
},
|
||||
"401": {
|
||||
"description": "未授权访问"
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误"
|
||||
}
|
||||
},
|
||||
"summary": "查询执行状态 (增强版)",
|
||||
"summary": "图片拼接",
|
||||
"tags": [
|
||||
"AI模板系统 (增强版)"
|
||||
"图片处理"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -1778,7 +1809,7 @@
|
||||
],
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:3004",
|
||||
"url": "http://localhost:3003",
|
||||
"description": "开发环境"
|
||||
},
|
||||
{
|
||||
@@ -1850,6 +1881,11 @@
|
||||
"type": "string",
|
||||
"description": "输入图片URL",
|
||||
"example": "https://cdn.example.com/upload/image.jpg"
|
||||
},
|
||||
"auditTaskId": {
|
||||
"type": "string",
|
||||
"description": "审核任务ID(可选)",
|
||||
"example": "audit_task_12345"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -2227,6 +2263,115 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ImageCompositionDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"images": {
|
||||
"description": "图片URL或base64数据列表",
|
||||
"example": [
|
||||
"https://picsum.photos/300/200",
|
||||
"https://picsum.photos/300/200",
|
||||
"https://picsum.photos/300/200"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"description": "排列方向",
|
||||
"enum": [
|
||||
"horizontal",
|
||||
"vertical",
|
||||
"grid"
|
||||
],
|
||||
"example": "horizontal"
|
||||
},
|
||||
"outputFormat": {
|
||||
"type": "string",
|
||||
"description": "输出格式",
|
||||
"enum": [
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp"
|
||||
],
|
||||
"example": "png",
|
||||
"default": "png"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "number",
|
||||
"description": "最大输出宽度(像素),超过会等比压缩",
|
||||
"example": 1920,
|
||||
"default": 1920,
|
||||
"minimum": 100,
|
||||
"maximum": 4096
|
||||
},
|
||||
"maxHeight": {
|
||||
"type": "number",
|
||||
"description": "最大输出高度(像素),超过会等比压缩",
|
||||
"example": 1080,
|
||||
"default": 1080,
|
||||
"minimum": 100,
|
||||
"maximum": 4096
|
||||
},
|
||||
"quality": {
|
||||
"type": "number",
|
||||
"description": "图片质量(1-100),仅对JPEG/WebP有效",
|
||||
"example": 80,
|
||||
"default": 80,
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"images",
|
||||
"direction"
|
||||
]
|
||||
},
|
||||
"ImageCompositionResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imageUrl": {
|
||||
"type": "string",
|
||||
"description": "拼接后的图片URL",
|
||||
"example": "https://s3.amazonaws.com/bucket/composed-image-1234567890.png"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "图片格式",
|
||||
"example": "png"
|
||||
},
|
||||
"size": {
|
||||
"type": "number",
|
||||
"description": "图片大小(字节)",
|
||||
"example": 12345
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"description": "画布宽度",
|
||||
"example": 800
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"description": "画布高度",
|
||||
"example": 600
|
||||
},
|
||||
"processingTime": {
|
||||
"type": "number",
|
||||
"description": "处理耗时(毫秒)",
|
||||
"example": 150
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"imageUrl",
|
||||
"format",
|
||||
"size",
|
||||
"width",
|
||||
"height",
|
||||
"processingTime"
|
||||
]
|
||||
},
|
||||
"PlatformType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
Reference in New Issue
Block a user