ERPTurbo_Poster/lib/routes.js
shenyifei dc940d2598 feat(api): 添加海报和PDF生成功能
- 新增海报生成接口,支持从网页URL或HTML内容生成海报图像
- 新增PDF生成接口,支持从网页URL或HTML内容生成PDF文档
- 添加Swagger API文档注释,完善接口描述和参数说明
- 实现HTML内容参数支持,允许直接传入HTML结构生成海报/PDF
- 添加输入验证和标准化响应格式
- 引入DOMPurify库对HTML内容进行安全过滤
- 更新环境变量配置,支持API密钥认证和CORS设置
- 优化上传逻辑,统一返回标准响应结构
- 添加构建脚本支持Docker镜像打包和推送
2025-11-20 17:51:35 +08:00

385 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {fileURLToPath} from 'url';
import {dirname} from 'path';
import {getParam, upload} from './params.js';
import {randomString, jsonToHtml, sanitizeHtml} from './utils.js';
import {FONT_STYLE} from './constants.js';
import {successResponse, errorResponse, validationErrorResponse, serverErrorResponse} from './response.js';
// 获取当前模块的目录名
const __filename = fileURLToPath(import.meta.url);
const basePath = dirname(dirname(__filename)); // 项目根目录
// 延迟获取 upload_path确保环境变量已被加载
let upload_path;
/**
* 健康检查接口
* @param {*} req
* @param {*} res
*/
function statusHandler(req, res) {
console.log("健康检查", new Date().getMilliseconds());
return res.status(200).json(successResponse({}, "Service is running", 200));
}
/**
* @swagger
* /api/v1/poster:
* post:
* summary: 生成海报
* description: 从网页URL或HTML内容生成海报图像
* tags: [Poster]
* security:
* - ApiKeyAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* webpage:
* type: string
* description: 要生成海报的网页URL
* example: "https://example.com"
* html:
* type: string
* description: 要生成海报的HTML内容可选优先级高于webpage
* example: "<html><body><h1>Hello World</h1></body></html>"
* device:
* type: number
* description: 设备缩放因子
* default: 1
* example: 1
* width:
* type: number
* description: 海报宽度
* default: 1920
* example: 1920
* height:
* type: number
* description: 海报高度
* default: 1080
* example: 1080
* type:
* type: string
* description: 输出图像类型
* default: "png"
* example: "png"
* encoding:
* type: string
* description: 编码类型
* default: "binary"
* example: "binary"
* responses:
* 200:
* description: 成功生成海报
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* description: 请求是否成功
* data:
* type: object
* properties:
* name:
* type: string
* example: "poster_abc123def456.png"
* description: 生成的海报文件名
* path:
* type: string
* example: "http://example.com/uploads/posters/2024/11/14/poster_abc123def456.png"
* description: 生成的海报文件访问路径
* message:
* type: string
* example: "Poster generated successfully"
* description: 响应消息
* code:
* type: number
* example: 200
* description: 响应代码
* 400:
* description: 请求参数错误
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* description: 请求是否成功
* data:
* type: object
* example: null
* description: 响应数据
* message:
* type: string
* example: "Missing required parameter: webpage or html"
* description: 错误消息
* code:
* type: number
* example: 3001
* description: 错误代码
* 401:
* description: 未授权访问
* 500:
* description: 服务器内部错误
*/
/**
* 海报生成接口
* @param {*} req
* @param {*} res
* @param {*} browserManager
* @param {*} storageManager
*/
async function posterHandler(
req,
res,
browserManager,
storageManager
) {
// 确保在使用时获取最新的环境变量值
upload_path = process.env.UPLOAD_PATH || 'uploads';
upload_path = upload_path + '/posters'
// 参数校验 - 现在支持webpage或html
if (!req.body || (!req.body.webpage && !req.body.html)) {
return res.status(400).json(validationErrorResponse('Missing required parameter: webpage or html', 3001));
}
const webpage = req.body.webpage;
let html = req.body.html;
const device = req.body.device || 1;
const width = req.body.width || 1920;
const height = req.body.height || 1080;
const type = req.body.type || 'png';
const encoding = req.body.encoding || 'binary';
const filename = 'poster_' + randomString(20) + '.' + type;
let param = getParam(req, filename, upload_path);
let base64;
const page = browserManager.getTargetPage();
try {
await page.setViewport({width: width, height: height, deviceScaleFactor: device, isMobile: true});
// 根据参数选择加载方式
if (html) {
// 检查html是否为JSON格式如果是则转换为HTML字符串
if (typeof html === 'object' && html !== null) {
html = jsonToHtml(html);
}
// 直接使用HTML内容
await page.setContent(html, {
timeout: 30000,
waitUntil: 'networkidle0'
});
} else {
// 导航到网页
await page.goto(webpage, {
timeout: 30000,
waitUntil: 'networkidle0'
});
}
await page.addStyleTag({content: FONT_STYLE});
await page.screenshot(param).then((data) => {
base64 = data;
});
// Update the upload function to return standardized response
const result = await upload(res, encoding, base64, type, filename, storageManager, basePath, upload_path);
return res.status(200).json(successResponse(result, "Poster generated successfully", 200));
} catch (err) {
console.error('Poster generation error:', err);
return res.status(500).json(serverErrorResponse('Failed to generate poster', 4001));
} finally {
// 确保只归还有效的页面
if (page && !page.isClosed()) {
browserManager.returnTargetPage(page);
}
}
}
/**
* @swagger
* /api/v1/pdf:
* post:
* summary: 生成PDF
* description: 从网页URL或HTML内容生成PDF文档
* tags: [PDF]
* security:
* - ApiKeyAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* webpage:
* type: string
* description: 要生成PDF的网页URL
* example: "https://example.com"
* html:
* type: string
* description: 要生成PDF的HTML内容可选优先级高于webpage
* example: "<html><body><h1>Hello World</h1></body></html>"
* device:
* type: number
* description: 设备缩放因子
* default: 1
* example: 1
* width:
* type: number
* description: PDF宽度
* default: 1920
* example: 1920
* height:
* type: number
* description: PDF高度
* default: 1080
* example: 1080
* responses:
* 200:
* description: 成功生成PDF
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* description: 请求是否成功
* data:
* type: object
* properties:
* name:
* type: string
* example: "pdf_abc123def456.pdf"
* description: 生成的PDF文件名
* path:
* type: string
* example: "http://example.com/uploads/pdfs/2024/11/14/pdf_abc123def456.pdf"
* description: 生成的PDF文件访问路径
* message:
* type: string
* example: "PDF generated successfully"
* description: 响应消息
* code:
* type: number
* example: 200
* description: 响应代码
* 400:
* description: 请求参数错误
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* description: 请求是否成功
* data:
* type: object
* example: null
* description: 响应数据
* message:
* type: string
* example: "Missing required parameter: webpage or html"
* description: 错误消息
* code:
* type: number
* example: 3001
* description: 错误代码
* 401:
* description: 未授权访问
* 500:
* description: 服务器内部错误
*/
/**
* PDF下载接口
* @param {*} req
* @param {*} res
* @param {*} browserManager
* @param {*} storageManager
*/
async function pdfHandler(
req,
res,
browserManager,
storageManager
) {
// 确保在使用时获取最新的环境变量值
upload_path = process.env.UPLOAD_PATH || 'uploads';
upload_path = upload_path + '/pdfs'
// 参数校验
if (!req.body || (!req.body.webpage && !req.body.html)) {
return res.status(400).json(validationErrorResponse('Missing required parameter: webpage or html', 3001));
}
const webpage = req.body.webpage;
let html = req.body.html;
const filename = 'pdf_' + randomString(20) + '.pdf';
let base64;
const page = browserManager.getTargetPage();
try {
// 根据参数选择加载方式
if (html) {
// 检查html是否为JSON格式如果是则转换为HTML字符串
if (typeof html === 'object' && html !== null) {
html = jsonToHtml(html);
}
// 直接使用HTML内容
await page.setContent(html, {
timeout: 30000,
});
} else {
// 导航到网页
await page.goto(webpage, {
timeout: 30000,
waitUntil: 'networkidle0'
});
}
await page.addStyleTag({content: FONT_STYLE});
// 生成PDF
await page.pdf({
format: 'A4',
printBackground: true,
path: `${upload_path}/${filename}`
});
// 复用上传函数处理PDF文件
const result = await upload(res, 'binary', base64, 'pdf', filename, storageManager, basePath, upload_path);
return res.status(200).json(successResponse(result, "PDF generated successfully", 200));
} catch (err) {
console.error('PDF generation error:', err);
return res.status(500).json(serverErrorResponse('Failed to generate PDF', 4002));
} finally {
// 确保只归还有效的页面
if (page && !page.isClosed()) {
browserManager.returnTargetPage(page);
}
}
}
export {
statusHandler,
posterHandler,
pdfHandler
};