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.waitForFunction(() => { const img = document.querySelector('.sign-img'); return img && img.complete && img.naturalWidth > 0 && img.naturalHeight > 0; }, { timeout: 10000 }); 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 = `
${title}
${content}
`; 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失败'); } };