Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 

281 rinda
8.4 KiB

  1. const COS = require('cos-nodejs-sdk-v5');
  2. const cosConfig = require('../config/cos.js');
  3. module.exports = class extends think.Service {
  4. /**
  5. * 生成签署合成图
  6. * @param {Object} params
  7. * @param {string} params.title - 协议标题
  8. * @param {string} params.content - 协议富文本内容
  9. * @param {string} params.signImageUrl - 签名图片URL
  10. * @param {string} params.signerName - 签署人姓名
  11. * @param {string} params.signerIdCard - 签署人身份证号
  12. * @param {string} params.signTime - 签署时间
  13. * @param {number} [params.amount] - 收入金额(仅income类型)
  14. * @param {string} [params.guardianName] - 监护人姓名(仅privacy_jhr类型)
  15. * @param {string} [params.guardianIdCard] - 监护人身份证号
  16. * @param {string} [params.guardianRelation] - 与患者关系
  17. * @returns {string} 合成图COS URL
  18. */
  19. async generate({ title, content, signImageUrl, signerName, signerIdCard, signTime, amount, guardianName, guardianIdCard, guardianRelation }) {
  20. const puppeteer = require('puppeteer');
  21. const html = this._buildHtml({ title, content, signImageUrl, signerName, signerIdCard, signTime, amount, guardianName, guardianIdCard, guardianRelation });
  22. let browser;
  23. try {
  24. browser = await puppeteer.launch({
  25. headless: 'new',
  26. args: [
  27. '--no-sandbox',
  28. '--disable-setuid-sandbox',
  29. '--disable-gpu',
  30. '--disable-dev-shm-usage',
  31. '--font-render-hinting=none'
  32. ]
  33. });
  34. const page = await browser.newPage();
  35. await page.setViewport({ width: 750, height: 1000, deviceScaleFactor: 2 });
  36. await page.setContent(html, { waitUntil: 'networkidle0', timeout: 15000 });
  37. // 等签名图片加载
  38. await page.waitForSelector('.sign-img', { timeout: 5000 }).catch(() => {});
  39. const screenshot = await page.screenshot({ fullPage: true, type: 'png' });
  40. await browser.close();
  41. browser = null;
  42. // 上传到 COS
  43. const url = await this._uploadToCos(screenshot);
  44. return url;
  45. } catch (error) {
  46. if (browser) await browser.close().catch(() => {});
  47. throw error;
  48. }
  49. }
  50. /**
  51. * 构建 HTML 模板
  52. */
  53. _buildHtml({ title, content, signImageUrl, signerName, signerIdCard, signTime, amount, guardianName, guardianIdCard, guardianRelation }) {
  54. const amountHtml = amount ? `
  55. <div style="margin: 30px 0; padding: 20px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
  56. <span style="font-size: 28px; color: #555;">个人年可支配收入:</span>
  57. <span style="font-size: 32px; color: #0e63e3; font-weight: 600;">¥${amount}</span>
  58. </div>` : '';
  59. // 监护人类型:特殊签署区布局
  60. const isGuardian = !!(guardianName && guardianIdCard);
  61. const signAreaHtml = isGuardian ? `
  62. <div class="sign-area">
  63. <div class="sign-row">
  64. <span class="sign-label">监护人签字:</span>
  65. <img class="sign-img" src="${signImageUrl}" />
  66. </div>
  67. <div class="sign-info">患者:${signerName}</div>
  68. <div class="sign-info">患者身份证:${signerIdCard}</div>
  69. <div class="sign-info">监护人姓名:${guardianName}</div>
  70. <div class="sign-info">监护人身份证:${guardianIdCard}</div>
  71. <div class="sign-info">与患者关系:${guardianRelation || ''}</div>
  72. <div class="sign-info">签署时间:${signTime}</div>
  73. </div>` : `
  74. <div class="sign-area">
  75. <div class="sign-row">
  76. <span class="sign-label">签名:</span>
  77. <img class="sign-img" src="${signImageUrl}" />
  78. </div>
  79. <div class="sign-info">签署人:${signerName}</div>
  80. <div class="sign-info">身份证:${signerIdCard}</div>
  81. <div class="sign-info">签署时间:${signTime}</div>
  82. </div>`;
  83. return `<!DOCTYPE html>
  84. <html>
  85. <head>
  86. <meta charset="utf-8">
  87. <style>
  88. * { margin: 0; padding: 0; box-sizing: border-box; }
  89. body {
  90. width: 750px;
  91. padding: 60px;
  92. font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
  93. background: #fff;
  94. color: #333;
  95. }
  96. .title {
  97. text-align: center;
  98. font-size: 36px;
  99. font-weight: bold;
  100. margin-bottom: 40px;
  101. color: #222;
  102. }
  103. .content {
  104. font-size: 28px;
  105. line-height: 2;
  106. color: #444;
  107. margin-bottom: 30px;
  108. }
  109. .content p {
  110. margin-bottom: 16px;
  111. text-indent: 2em;
  112. }
  113. .sign-area {
  114. margin-top: 50px;
  115. border-top: 2px solid #eee;
  116. padding-top: 30px;
  117. }
  118. .sign-row {
  119. display: flex;
  120. align-items: center;
  121. margin-bottom: 20px;
  122. }
  123. .sign-label {
  124. font-size: 26px;
  125. color: #666;
  126. width: 120px;
  127. flex-shrink: 0;
  128. }
  129. .sign-img {
  130. height: 100px;
  131. }
  132. .sign-info {
  133. font-size: 24px;
  134. color: #888;
  135. margin-bottom: 10px;
  136. }
  137. </style>
  138. </head>
  139. <body>
  140. <div class="title">${title}</div>
  141. <div class="content">${content}</div>
  142. ${amountHtml}
  143. ${signAreaHtml}
  144. </body>
  145. </html>`;
  146. }
  147. /**
  148. * 重新生成签署图(保留原图签名区域,替换上方内容)
  149. * 原理:原图做背景 bottom 对齐,白色 overlay 覆盖上方内容区,
  150. * placeholder 撑出签名区高度让背景图底部签名露出
  151. * @param {Object} params
  152. * @param {string} params.originalImageUrl - 原合成图URL
  153. * @param {string} params.title - 新协议标题
  154. * @param {string} params.content - 新协议富文本内容
  155. * @returns {string} 新合成图COS URL
  156. */
  157. async regenerate({ originalImageUrl, title, content }) {
  158. const puppeteer = require('puppeteer');
  159. const html = `<!DOCTYPE html>
  160. <html>
  161. <head>
  162. <meta charset="utf-8">
  163. <style>
  164. * { margin: 0; padding: 0; box-sizing: border-box; }
  165. body {
  166. width: 750px;
  167. overflow: hidden;
  168. font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
  169. background-image: url('${originalImageUrl}');
  170. background-size: 100% auto;
  171. background-repeat: no-repeat;
  172. background-position: bottom center;
  173. position: relative;
  174. }
  175. .overlay {
  176. width: 100%;
  177. background: #fff;
  178. padding: 60px;
  179. }
  180. .title {
  181. text-align: center;
  182. font-size: 36px;
  183. font-weight: bold;
  184. margin-bottom: 40px;
  185. color: #222;
  186. }
  187. .content {
  188. font-size: 28px;
  189. line-height: 2;
  190. color: #444;
  191. }
  192. .content p {
  193. margin-bottom: 16px;
  194. text-indent: 2em;
  195. }
  196. .sign-area-placeholder {
  197. height: 360px;
  198. }
  199. </style>
  200. </head>
  201. <body>
  202. <div class="overlay">
  203. <div class="title">${title}</div>
  204. <div class="content">${content}</div>
  205. </div>
  206. <div class="sign-area-placeholder"></div>
  207. </body>
  208. </html>`;
  209. let browser;
  210. try {
  211. browser = await puppeteer.launch({
  212. headless: 'new',
  213. args: [
  214. '--no-sandbox',
  215. '--disable-setuid-sandbox',
  216. '--disable-gpu',
  217. '--disable-dev-shm-usage',
  218. '--font-render-hinting=none'
  219. ]
  220. });
  221. const page = await browser.newPage();
  222. await page.setViewport({ width: 750, height: 1000, deviceScaleFactor: 2 });
  223. await page.setContent(html, { waitUntil: 'networkidle0', timeout: 15000 });
  224. const screenshot = await page.screenshot({ fullPage: true, type: 'png' });
  225. await browser.close();
  226. browser = null;
  227. const url = await this._uploadToCos(screenshot);
  228. return url;
  229. } catch (error) {
  230. if (browser) await browser.close().catch(() => {});
  231. throw error;
  232. }
  233. }
  234. /**
  235. * 上传截图到 COS
  236. */
  237. async _uploadToCos(buffer) {
  238. const cos = new COS({ SecretId: cosConfig.secretId, SecretKey: cosConfig.secretKey });
  239. const now = new Date();
  240. const dateFolder = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`;
  241. const fileName = `sign_${Date.now()}.png`;
  242. const cosKey = `signs/${dateFolder}/${fileName}`;
  243. const result = await new Promise((resolve, reject) => {
  244. cos.putObject({
  245. Bucket: cosConfig.bucket,
  246. Region: cosConfig.region,
  247. Key: cosKey,
  248. Body: buffer,
  249. ContentType: 'image/png'
  250. }, (err, data) => {
  251. if (err) reject(err);
  252. else resolve(data);
  253. });
  254. });
  255. if (result.statusCode === 200) {
  256. const bucketUrl = `https://${result.Location}`;
  257. return bucketUrl.replace(cosConfig.bucketUrl, cosConfig.cdnUrl);
  258. }
  259. throw new Error('截图上传COS失败');
  260. }
  261. };