diff --git a/backend/services/planting-service/src/infrastructure/pdf/pdf-generator.service.ts b/backend/services/planting-service/src/infrastructure/pdf/pdf-generator.service.ts index 54b6bb1c..b5b67951 100644 --- a/backend/services/planting-service/src/infrastructure/pdf/pdf-generator.service.ts +++ b/backend/services/planting-service/src/infrastructure/pdf/pdf-generator.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import * as fs from 'fs'; import * as path from 'path'; -import { PDFDocument, PDFFont, rgb } from 'pdf-lib'; +import { PDFDocument, PDFFont /* , rgb */ } from 'pdf-lib'; // eslint-disable-next-line @typescript-eslint/no-var-requires const fontkit = require('@pdf-lib/fontkit'); @@ -16,12 +16,104 @@ const FORM_FIELDS = { USER_ID_CARD: 'userIdCard', USER_PHONE: 'userPhone', TREE_COUNT: 'treeCount', + TOTAL_AMOUNT: 'totalAmount', // 认种金额(数字) + TOTAL_AMOUNT_CHINESE: 'totalAmountChinese', // 认种金额(大写) + GREEN_POINTS_AMOUNT: 'greenPointsAmount', // 绿积分金额 SIGN_YEAR: 'signYear', SIGN_MONTH: 'signMonth', SIGN_DAY: 'signDay', SIGNATURE: 'signature', // 签名图片按钮字段 } as const; +/** + * 数字转中文大写金额 + * @param num 数字金额 + * @returns 中文大写金额字符串 + */ +function numberToChineseAmount(num: number): string { + if (num === 0) return '零元整'; + + const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; + const units = ['', '拾', '佰', '仟']; + const bigUnits = ['', '万', '亿', '兆']; + + // 分离整数和小数部分 + const [integerPart, decimalPart] = num.toFixed(2).split('.'); + const intNum = parseInt(integerPart, 10); + const jiao = parseInt(decimalPart[0], 10); // 角 + const fen = parseInt(decimalPart[1], 10); // 分 + + let result = ''; + + // 处理整数部分 + if (intNum > 0) { + const intStr = intNum.toString(); + const len = intStr.length; + let zeroFlag = false; // 是否需要补零 + + for (let i = 0; i < len; i++) { + const digit = parseInt(intStr[i], 10); + const unitIndex = (len - i - 1) % 4; + const bigUnitIndex = Math.floor((len - i - 1) / 4); + + if (digit === 0) { + zeroFlag = true; + // 在万、亿位置需要加单位 + if (unitIndex === 0 && bigUnitIndex > 0) { + // 检查这个大单位区间是否全为零 + const start = Math.max(0, i - 3); + let allZero = true; + for (let j = start; j <= i; j++) { + if (parseInt(intStr[j], 10) !== 0) { + allZero = false; + break; + } + } + if (!allZero) { + result += bigUnits[bigUnitIndex]; + } + } + } else { + if (zeroFlag) { + result += digits[0]; // 补零 + zeroFlag = false; + } + result += digits[digit] + units[unitIndex]; + if (unitIndex === 0) { + result += bigUnits[bigUnitIndex]; + } + } + } + result += '元'; + } + + // 处理小数部分 + if (jiao === 0 && fen === 0) { + result += '整'; + } else { + if (jiao > 0) { + result += digits[jiao] + '角'; + } else if (intNum > 0) { + result += '零'; + } + if (fen > 0) { + result += digits[fen] + '分'; + } + } + + return result || '零元整'; +} + +/** + * 单棵榴莲树含税价格(人民币) + */ +export const PRICE_PER_TREE_CNY = 17414.1; + +/** + * 单棵榴莲树绿积分价格 + */ +export const PRICE_PER_TREE_GREEN_POINTS = 15831; + /** * 合同 PDF 生成数据 */ @@ -31,6 +123,8 @@ export interface ContractPdfData { userIdCard: string; userPhone: string; treeCount: number; + totalAmount?: number; // 认种金额(人民币),可选,默认根据 treeCount 计算 + greenPointsAmount?: number; // 绿积分金额,可选 signingDate: string; // YYYY-MM-DD 格式 } @@ -100,17 +194,15 @@ export class PdfGeneratorService { const fontBytes = fs.readFileSync(this.fontPath); const customFont = await pdfDoc.embedFont(fontBytes); - // 4. 选择填充方式:优先使用表单字段,否则使用坐标定位 - if (this.hasFormFields(pdfDoc)) { - this.logger.log('Using form fields mode'); - await this.fillFormFields(pdfDoc, data, customFont); - // 预览PDF时扁平化所有字段(包括签名字段) - const form = pdfDoc.getForm(); - form.flatten(); - } else { - this.logger.log('Using coordinate mode (no form fields found)'); - this.fillByCoordinates(pdfDoc, data, customFont); + // 4. 使用表单字段填充 + if (!this.hasFormFields(pdfDoc)) { + throw new Error('PDF模板缺少表单字段,请使用带表单字段的模板'); } + this.logger.log('Using form fields mode'); + await this.fillFormFields(pdfDoc, data, customFont); + // 预览PDF时扁平化所有字段(包括签名字段) + const form = pdfDoc.getForm(); + form.flatten(); // 5. 保存 PDF const pdfBytes = await pdfDoc.save(); @@ -138,6 +230,12 @@ export class PdfGeneratorService { const form = pdfDoc.getForm(); const [year, month, day] = data.signingDate.split('-'); + // 计算金额:优先使用传入的金额,否则根据棵数计算 + const totalAmount = data.totalAmount ?? data.treeCount * PRICE_PER_TREE_CNY; + const totalAmountChinese = numberToChineseAmount(totalAmount); + // 绿积分金额:优先使用传入的金额,否则根据棵数计算(使用绿积分单价) + const greenPointsAmount = data.greenPointsAmount ?? data.treeCount * PRICE_PER_TREE_GREEN_POINTS; + // 填充各个字段 const fieldMappings: Array<{ name: string; value: string }> = [ { name: FORM_FIELDS.CONTRACT_NO, value: data.contractNo }, @@ -145,6 +243,9 @@ export class PdfGeneratorService { { name: FORM_FIELDS.USER_ID_CARD, value: data.userIdCard || '未认证' }, { name: FORM_FIELDS.USER_PHONE, value: data.userPhone || '未认证' }, { name: FORM_FIELDS.TREE_COUNT, value: data.treeCount.toString() }, + { name: FORM_FIELDS.TOTAL_AMOUNT, value: totalAmount.toFixed(2) }, + { name: FORM_FIELDS.TOTAL_AMOUNT_CHINESE, value: totalAmountChinese }, + { name: FORM_FIELDS.GREEN_POINTS_AMOUNT, value: greenPointsAmount.toFixed(2) }, { name: FORM_FIELDS.SIGN_YEAR, value: year }, { name: FORM_FIELDS.SIGN_MONTH, value: month }, { name: FORM_FIELDS.SIGN_DAY, value: day }, @@ -156,7 +257,8 @@ export class PdfGeneratorService { field.setText(value); field.updateAppearances(font); } catch (error) { - this.logger.warn(`Form field '${name}' not found, skipping`); + // 所有字段都是必须的,缺少任何字段都应该报错 + throw new Error(`PDF模板缺少必需的表单字段: ${name}`); } } @@ -164,92 +266,6 @@ export class PdfGeneratorService { // 签名字段需要保留,待签名嵌入后再统一扁平化 } - /** - * 使用坐标定位填充 PDF(后备方式) - * 坐标需要根据实际 PDF 布局调整 - */ - private fillByCoordinates( - pdfDoc: PDFDocument, - data: ContractPdfData, - customFont: PDFFont, - ): void { - const pages = pdfDoc.getPages(); - const page1 = pages[0]; // 第1页:协议编号 - const page2 = pages[1]; // 第2页:乙方信息 - const page3 = pages[2]; // 第3页:种植期限、认种数量 - const page6 = pages[5]; // 第6页:签名区域 - - const fontSize = 12; - const textColor = rgb(0, 0, 0); - - // 填充第1页 - 协议编号(使用合同编号) - page1.drawText(data.contractNo, { - x: 390, - y: 95, - size: fontSize, - font: customFont, - color: textColor, - }); - - // 填充第2页 - 乙方信息 - page2.drawText(data.userRealName || '未认证', { - x: 155, - y: 583, - size: fontSize, - font: customFont, - color: textColor, - }); - - page2.drawText(data.userIdCard || '未认证', { - x: 130, - y: 557, - size: fontSize, - font: customFont, - color: textColor, - }); - - page2.drawText(data.userPhone || '未认证', { - x: 130, - y: 531, - size: fontSize, - font: customFont, - color: textColor, - }); - - // 填充第3页 - 认种数量 - page3.drawText(data.treeCount.toString(), { - x: 138, - y: 477, - size: fontSize, - font: customFont, - color: textColor, - }); - - // 填充第6页 - 签约日期 - const [year, month, day] = data.signingDate.split('-'); - page6.drawText(year, { - x: 290, - y: 103, - size: fontSize, - font: customFont, - color: textColor, - }); - page6.drawText(month, { - x: 345, - y: 103, - size: fontSize, - font: customFont, - color: textColor, - }); - page6.drawText(day, { - x: 385, - y: 103, - size: fontSize, - font: customFont, - color: textColor, - }); - } - /** * 在已生成的 PDF 上嵌入签名 * @param pdfBuffer 原始 PDF Buffer @@ -367,13 +383,11 @@ export class PdfGeneratorService { const customFont = await pdfDoc.embedFont(fontBytes); // 4. 填充表单字段(不扁平化) - if (this.hasFormFields(pdfDoc)) { - this.logger.log('Using form fields mode'); - await this.fillFormFields(pdfDoc, data, customFont); - } else { - this.logger.log('Using coordinate mode (no form fields found)'); - this.fillByCoordinates(pdfDoc, data, customFont); + if (!this.hasFormFields(pdfDoc)) { + throw new Error('PDF模板缺少表单字段,请使用带表单字段的模板'); } + this.logger.log('Using form fields mode'); + await this.fillFormFields(pdfDoc, data, customFont); // 5. 如果有签名,嵌入签名图片到 signature 按钮字段 if (signature) { diff --git a/backend/services/planting-service/templates/contract-template.pdf b/backend/services/planting-service/templates/contract-template.pdf index 44bba373..580f2d27 100644 Binary files a/backend/services/planting-service/templates/contract-template.pdf and b/backend/services/planting-service/templates/contract-template.pdf differ diff --git a/榴莲树认种权益协议_发布版_form.pdf b/榴莲树认种权益协议_发布版_form.pdf deleted file mode 100644 index 44bba373..00000000 Binary files a/榴莲树认种权益协议_发布版_form.pdf and /dev/null differ diff --git a/联合种植协议董事长_正式合同_带公章_form.pdf b/联合种植协议董事长_正式合同_带公章_form.pdf new file mode 100644 index 00000000..580f2d27 Binary files /dev/null and b/联合种植协议董事长_正式合同_带公章_form.pdf differ