const COS = require('cos-nodejs-sdk-v5');
const cosConfig = require('../config/cos.js');
module.exports = class extends think.Service {
/**
* 生成签署合成图
* @param {Object} params
* @param {string} params.title - 协议标题
* @param {string} params.content - 协议富文本内容
* @param {string} params.signImageUrl - 签名图片URL
* @param {string} params.signerName - 签署人姓名
* @param {string} params.signerIdCard - 签署人身份证号
* @param {string} params.signTime - 签署时间
* @param {number} [params.amount] - 收入金额(仅income类型)
* @param {string} [params.guardianName] - 监护人姓名(仅privacy_jhr类型)
* @param {string} [params.guardianIdCard] - 监护人身份证号
* @param {string} [params.guardianRelation] - 与患者关系
* @returns {string} 合成图COS URL
*/
async generate({ title, content, signImageUrl, signerName, signerIdCard, signTime, amount, guardianName, guardianIdCard, guardianRelation }) {
const puppeteer = require('puppeteer');
const html = this._buildHtml({ title, content, signImageUrl, signerName, signerIdCard, signTime, amount, guardianName, guardianIdCard, guardianRelation });
let browser;
try {
browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--disable-dev-shm-usage',
'--font-render-hinting=none'
]
});
const page = await browser.newPage();
await page.setViewport({ width: 750, height: 1000, deviceScaleFactor: 2 });
await page.setContent(html, { waitUntil: 'networkidle0', timeout: 15000 });
// 等签名图片加载
await page.waitForSelector('.sign-img', { timeout: 5000 }).catch(() => {});
const screenshot = await page.screenshot({ fullPage: true, type: 'png' });
await browser.close();
browser = null;
// 上传到 COS
const url = await this._uploadToCos(screenshot);
return url;
} catch (error) {
if (browser) await browser.close().catch(() => {});
throw error;
}
}
/**
* 构建 HTML 模板
*/
_buildHtml({ title, content, signImageUrl, signerName, signerIdCard, signTime, amount, guardianName, guardianIdCard, guardianRelation }) {
const amountHtml = amount ? `
个人年可支配收入:
¥${amount}
` : '';
// 监护人类型:特殊签署区布局
const isGuardian = !!(guardianName && guardianIdCard);
const signAreaHtml = isGuardian ? `
监护人签字:
患者:${signerName}
患者身份证:${signerIdCard}
监护人姓名:${guardianName}
监护人身份证:${guardianIdCard}
与患者关系:${guardianRelation || ''}
签署时间:${signTime}
` : `
签名:
签署人:${signerName}
身份证:${signerIdCard}
签署时间:${signTime}
`;
return `
${title}
${content}
${amountHtml}
${signAreaHtml}
`;
}
/**
* 重新生成签署图(保留原图签名区域,替换上方内容)
* 原理:原图做背景 bottom 对齐,白色 overlay 覆盖上方内容区,
* placeholder 撑出签名区高度让背景图底部签名露出
* @param {Object} params
* @param {string} params.originalImageUrl - 原合成图URL
* @param {string} params.title - 新协议标题
* @param {string} params.content - 新协议富文本内容
* @returns {string} 新合成图COS URL
*/
async regenerate({ originalImageUrl, title, content }) {
const puppeteer = require('puppeteer');
const html = `
`;
let browser;
try {
browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--disable-dev-shm-usage',
'--font-render-hinting=none'
]
});
const page = await browser.newPage();
await page.setViewport({ width: 750, height: 1000, deviceScaleFactor: 2 });
await page.setContent(html, { waitUntil: 'networkidle0', timeout: 15000 });
const screenshot = await page.screenshot({ fullPage: true, type: 'png' });
await browser.close();
browser = null;
const url = await this._uploadToCos(screenshot);
return url;
} catch (error) {
if (browser) await browser.close().catch(() => {});
throw error;
}
}
/**
* 上传截图到 COS
*/
async _uploadToCos(buffer) {
const cos = new COS({ SecretId: cosConfig.secretId, SecretKey: cosConfig.secretKey });
const now = new Date();
const dateFolder = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`;
const fileName = `sign_${Date.now()}.png`;
const cosKey = `signs/${dateFolder}/${fileName}`;
const result = await new Promise((resolve, reject) => {
cos.putObject({
Bucket: cosConfig.bucket,
Region: cosConfig.region,
Key: cosKey,
Body: buffer,
ContentType: 'image/png'
}, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
if (result.statusCode === 200) {
const bucketUrl = `https://${result.Location}`;
return bucketUrl.replace(cosConfig.bucketUrl, cosConfig.cdnUrl);
}
throw new Error('截图上传COS失败');
}
};