|
- 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 ? `
- <div style="margin: 30px 0; padding: 20px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
- <span style="font-size: 28px; color: #555;">个人年可支配收入:</span>
- <span style="font-size: 32px; color: #0e63e3; font-weight: 600;">¥${amount}</span>
- </div>` : '';
-
- // 监护人类型:特殊签署区布局
- const isGuardian = !!(guardianName && guardianIdCard);
- const signAreaHtml = isGuardian ? `
- <div class="sign-area">
- <div class="sign-row">
- <span class="sign-label">监护人签字:</span>
- <img class="sign-img" src="${signImageUrl}" />
- </div>
- <div class="sign-info">患者:${signerName}</div>
- <div class="sign-info">患者身份证:${signerIdCard}</div>
- <div class="sign-info">监护人姓名:${guardianName}</div>
- <div class="sign-info">监护人身份证:${guardianIdCard}</div>
- <div class="sign-info">与患者关系:${guardianRelation || ''}</div>
- <div class="sign-info">签署时间:${signTime}</div>
- </div>` : `
- <div class="sign-area">
- <div class="sign-row">
- <span class="sign-label">签名:</span>
- <img class="sign-img" src="${signImageUrl}" />
- </div>
- <div class="sign-info">签署人:${signerName}</div>
- <div class="sign-info">身份证:${signerIdCard}</div>
- <div class="sign-info">签署时间:${signTime}</div>
- </div>`;
-
- return `<!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- width: 750px;
- padding: 60px;
- font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
- background: #fff;
- color: #333;
- }
- .title {
- text-align: center;
- font-size: 36px;
- font-weight: bold;
- margin-bottom: 40px;
- color: #222;
- }
- .content {
- font-size: 28px;
- line-height: 2;
- color: #444;
- margin-bottom: 30px;
- }
- .content p {
- margin-bottom: 16px;
- text-indent: 2em;
- }
- .sign-area {
- margin-top: 50px;
- border-top: 2px solid #eee;
- padding-top: 30px;
- }
- .sign-row {
- display: flex;
- align-items: center;
- margin-bottom: 20px;
- }
- .sign-label {
- font-size: 26px;
- color: #666;
- width: 120px;
- flex-shrink: 0;
- }
- .sign-img {
- height: 100px;
- }
- .sign-info {
- font-size: 24px;
- color: #888;
- margin-bottom: 10px;
- }
- </style>
- </head>
- <body>
- <div class="title">${title}</div>
- <div class="content">${content}</div>
- ${amountHtml}
- ${signAreaHtml}
- </body>
- </html>`;
- }
-
- /**
- * 重新生成签署图(保留原图签名区域,替换上方内容)
- * 原理:原图做背景 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 = `<!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- width: 750px;
- overflow: hidden;
- font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
- background-image: url('${originalImageUrl}');
- background-size: 100% auto;
- background-repeat: no-repeat;
- background-position: bottom center;
- position: relative;
- }
- .overlay {
- width: 100%;
- background: #fff;
- padding: 60px;
- }
- .title {
- text-align: center;
- font-size: 36px;
- font-weight: bold;
- margin-bottom: 40px;
- color: #222;
- }
- .content {
- font-size: 28px;
- line-height: 2;
- color: #444;
- }
- .content p {
- margin-bottom: 16px;
- text-indent: 2em;
- }
- .sign-area-placeholder {
- height: 360px;
- }
- </style>
- </head>
- <body>
- <div class="overlay">
- <div class="title">${title}</div>
- <div class="content">${content}</div>
- </div>
- <div class="sign-area-placeholder"></div>
- </body>
- </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失败');
- }
- };
|