diff --git a/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts b/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts index c7e2377c..47eec4e0 100644 --- a/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts +++ b/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts @@ -248,21 +248,19 @@ export class ContractSigningController { signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`; } - // 5. 生成带签名的 PDF + // 5. 生成带签名的 PDF(在同一个实例上完成填充和签名) const signingDate = new Date().toISOString().split('T')[0]; - let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({ - contractNo: task.contractNo, - userRealName: task.userRealName || '未认证', - userIdCard: task.userIdCardNumber || '', - userPhone: task.userPhoneNumber || '', - treeCount: task.treeCount, - signingDate, - }); - - // 6. 嵌入签名到 PDF - pdfBuffer = await this.pdfGeneratorService.embedSignature(pdfBuffer, { - signatureImagePng: signatureBuffer, - }); + const pdfBuffer = await this.pdfGeneratorService.generateSignedContractPdf( + { + contractNo: task.contractNo, + userRealName: task.userRealName || '未认证', + userIdCard: task.userIdCardNumber || '', + userPhone: task.userPhoneNumber || '', + treeCount: task.treeCount, + signingDate, + }, + { signatureImagePng: signatureBuffer }, + ); this.logger.log(`Signed PDF generated: ${pdfBuffer.length} bytes`); // 7. 上传签署后的 PDF 到 MinIO @@ -338,21 +336,19 @@ export class ContractSigningController { signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`; } - // 5. 生成带签名的 PDF + // 5. 生成带签名的 PDF(在同一个实例上完成填充和签名) const signingDate = new Date().toISOString().split('T')[0]; - let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({ - contractNo: task.contractNo, - userRealName: task.userRealName || '未认证', - userIdCard: task.userIdCardNumber || '', - userPhone: task.userPhoneNumber || '', - treeCount: task.treeCount, - signingDate, - }); - - // 6. 嵌入签名到 PDF - pdfBuffer = await this.pdfGeneratorService.embedSignature(pdfBuffer, { - signatureImagePng: signatureBuffer, - }); + const pdfBuffer = await this.pdfGeneratorService.generateSignedContractPdf( + { + contractNo: task.contractNo, + userRealName: task.userRealName || '未认证', + userIdCard: task.userIdCardNumber || '', + userPhone: task.userPhoneNumber || '', + treeCount: task.treeCount, + signingDate, + }, + { signatureImagePng: signatureBuffer }, + ); this.logger.log(`Signed PDF generated: ${pdfBuffer.length} bytes`); // 7. 上传签署后的 PDF 到 MinIO 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 ded71ae4..f452e1bd 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 @@ -104,6 +104,9 @@ export class PdfGeneratorService { 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); @@ -157,8 +160,8 @@ export class PdfGeneratorService { } } - // 扁平化表单(将字段转换为静态文本,不可再编辑) - form.flatten(); + // 注意:不在这里调用 form.flatten() + // 签名字段需要保留,待签名嵌入后再统一扁平化 } /** @@ -339,6 +342,7 @@ export class PdfGeneratorService { /** * 生成带签名的完整合同 PDF + * 在同一个 PDFDocument 实例上完成填充和签名,最后统一扁平化 * @param data 合同数据 * @param signature 签名数据(可选,如果没有签名则只生成填充数据的 PDF) * @returns PDF Buffer @@ -347,15 +351,63 @@ export class PdfGeneratorService { data: ContractPdfData, signature?: SignatureData, ): Promise { - // 先生成填充数据的 PDF - let pdfBuffer = await this.generateContractPdf(data); + this.logger.log(`Generating signed PDF for contract: ${data.contractNo}`); - // 如果有签名,嵌入签名 - if (signature) { - pdfBuffer = await this.embedSignature(pdfBuffer, signature); + try { + // 1. 加载 PDF 模板 + const templateBytes = fs.readFileSync(this.templatePath); + const pdfDoc = await PDFDocument.load(templateBytes); + + // 2. 注册 fontkit 以支持自定义字体 + pdfDoc.registerFontkit(fontkit); + + // 3. 加载中文字体 + 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); + } else { + this.logger.log('Using coordinate mode (no form fields found)'); + this.fillByCoordinates(pdfDoc, data, customFont); + } + + // 5. 如果有签名,嵌入签名图片到 signature 按钮字段 + if (signature) { + const signatureImage = await pdfDoc.embedPng(signature.signatureImagePng); + + try { + const form = pdfDoc.getForm(); + const signatureButton = form.getButton(FORM_FIELDS.SIGNATURE); + signatureButton.setImage(signatureImage); + this.logger.log('Signature embedded using form field'); + } catch { + // 如果没有签名按钮字段,回退到坐标方式 + this.logger.log('Signature button field not found, using coordinates'); + this.embedSignatureByCoordinates(pdfDoc, signatureImage); + } + } + + // 6. 最后统一扁平化所有表单字段 + if (this.hasFormFields(pdfDoc)) { + const form = pdfDoc.getForm(); + form.flatten(); + this.logger.log('Form fields flattened'); + } + + // 7. 保存 PDF + const pdfBytes = await pdfDoc.save(); + this.logger.log(`Signed PDF generated successfully for contract: ${data.contractNo}`); + return Buffer.from(pdfBytes); + } catch (error) { + this.logger.error( + `Failed to generate signed PDF for contract ${data.contractNo}:`, + error, + ); + throw new Error(`Signed PDF generation failed: ${error.message}`); } - - return pdfBuffer; } }