diff --git a/backend/package-lock.json b/backend/package-lock.json index 38c944b..f92e1c4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,8 @@ "koa": "^2.15.3", "koa-body": "^6.0.1", "koa-route": "^4.0.1", + "marked": "^15.0.6", + "nodemailer": "^6.9.16", "npm": "^11.0.0", "puppeteer": "^24.0.0" }, @@ -28,6 +30,7 @@ "@types/koa": "^2.15.0", "@types/koa-route": "^3.2.8", "@types/node": "^14.0.0", + "@types/nodemailer": "^6.4.17", "nodemon": "^3.1.9", "prisma": "^6.2.1", "ts-node": "^10.0.0", @@ -423,6 +426,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", @@ -1902,6 +1915,18 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/marked": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz", + "integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2004,6 +2029,15 @@ "node": ">= 0.4.0" } }, + "node_modules/nodemailer": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", diff --git a/backend/package.json b/backend/package.json index 475fc59..c9d3615 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "@types/koa": "^2.15.0", "@types/koa-route": "^3.2.8", "@types/node": "^14.0.0", + "@types/nodemailer": "^6.4.17", "nodemon": "^3.1.9", "prisma": "^6.2.1", "ts-node": "^10.0.0", @@ -34,6 +35,8 @@ "koa": "^2.15.3", "koa-body": "^6.0.1", "koa-route": "^4.0.1", + "marked": "^15.0.6", + "nodemailer": "^6.9.16", "npm": "^11.0.0", "puppeteer": "^24.0.0" } diff --git a/backend/src/func/issueInvoice.ts b/backend/src/func/issueInvoice.ts index c41f55d..e9c9f65 100644 --- a/backend/src/func/issueInvoice.ts +++ b/backend/src/func/issueInvoice.ts @@ -7,6 +7,8 @@ import { readFileSync } from 'fs' import dayjs from 'dayjs' import handlebars from 'handlebars' import puppeteer from 'puppeteer' +import nodemailer from 'nodemailer' +import marked from 'marked' console.log = Debug('invoiceIssuer:func/issueInvoice.ts') @@ -102,8 +104,7 @@ export default async (prisma: PrismaClient, payerId: number, period: Date[], ite const browser = await puppeteer.launch() const page = await browser.newPage() await page.setContent(html) - await page.pdf({ - path: `./dist/${invoiceNumber}.pdf`, + const invoiceFile = await page.pdf({ format: 'A4', printBackground: true, margin: { @@ -113,5 +114,43 @@ export default async (prisma: PrismaClient, payerId: number, period: Date[], ite left: '1cm' } }) + const invoiceBuffer = Buffer.from(invoiceFile) await browser.close() + + // 发送邮件 + const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT || '465'), + secure: true, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + } + }) + + const mailMarkdownTemplate = readFileSync('./src/resources/mail.md', 'utf-8') + const mailData = { + payerName: payerInfo.name, + periodStart: dayjs(period[0]).format('D, MMMM YYYY'), + periodEnd: dayjs(period[1]).format('D, MMMM YYYY'), + invoiceNumber: invoiceNumber, + invoiceAmount: `\$${newInvoice.getTotal().toFixed(2)}`, + payeeName: data.payee.name, + } + const mailRender = handlebars.compile(mailMarkdownTemplate) + const mailText = mailRender(mailData) + + const mailOptions = { + from: process.env.SMTP_USER, + to: payerInfo.email, + subject: `Invoice ${invoiceNumber}`, + html: await marked.parse(mailText), + attachments: [ + { + filename: `${invoiceNumber}.pdf`, + content: invoiceBuffer + } + ] + } + await transporter.sendMail(mailOptions) } \ No newline at end of file diff --git a/backend/src/resources/mail.md b/backend/src/resources/mail.md new file mode 100644 index 0000000..eb9f57e --- /dev/null +++ b/backend/src/resources/mail.md @@ -0,0 +1,12 @@ +Dear {{payerName}}, + +I hope this email finds you well. Per our agreement, the invoice for {{periodStart}} ~ {{periodEnd}} is attached. Kindly review it at your earliest convenience and let me know if any questions or clarifications are required. + +Details of the Invoice: + +- Invoice Number: #{{invoiceNumber}} +- Amount: {{invoiceAmount}} + +Thank you for your prompt attention to this matter. I appreciate your support and look forward to your confirmation. + +Best regards,
{{payeeName}}