fix(planting-service): 修复合同签名无法放到指定位置的问题

- 修改 generateSignedContractPdf 在同一个 PDFDocument 实例上完成填充和签名
- 移除 fillFormFields 中的 form.flatten(),保留签名字段供后续使用
- 最后统一扁平化所有表单字段,确保签名放到正确位置
- 控制器改用 generateSignedContractPdf 替代分两步调用

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-26 05:23:53 -08:00
parent 09cc696efa
commit bdeff3b372
2 changed files with 85 additions and 37 deletions

View File

@ -248,21 +248,19 @@ export class ContractSigningController {
signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`; signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`;
} }
// 5. 生成带签名的 PDF // 5. 生成带签名的 PDF(在同一个实例上完成填充和签名)
const signingDate = new Date().toISOString().split('T')[0]; const signingDate = new Date().toISOString().split('T')[0];
let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({ const pdfBuffer = await this.pdfGeneratorService.generateSignedContractPdf(
contractNo: task.contractNo, {
userRealName: task.userRealName || '未认证', contractNo: task.contractNo,
userIdCard: task.userIdCardNumber || '', userRealName: task.userRealName || '未认证',
userPhone: task.userPhoneNumber || '', userIdCard: task.userIdCardNumber || '',
treeCount: task.treeCount, userPhone: task.userPhoneNumber || '',
signingDate, treeCount: task.treeCount,
}); signingDate,
},
// 6. 嵌入签名到 PDF { signatureImagePng: signatureBuffer },
pdfBuffer = await this.pdfGeneratorService.embedSignature(pdfBuffer, { );
signatureImagePng: signatureBuffer,
});
this.logger.log(`Signed PDF generated: ${pdfBuffer.length} bytes`); this.logger.log(`Signed PDF generated: ${pdfBuffer.length} bytes`);
// 7. 上传签署后的 PDF 到 MinIO // 7. 上传签署后的 PDF 到 MinIO
@ -338,21 +336,19 @@ export class ContractSigningController {
signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`; signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`;
} }
// 5. 生成带签名的 PDF // 5. 生成带签名的 PDF(在同一个实例上完成填充和签名)
const signingDate = new Date().toISOString().split('T')[0]; const signingDate = new Date().toISOString().split('T')[0];
let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({ const pdfBuffer = await this.pdfGeneratorService.generateSignedContractPdf(
contractNo: task.contractNo, {
userRealName: task.userRealName || '未认证', contractNo: task.contractNo,
userIdCard: task.userIdCardNumber || '', userRealName: task.userRealName || '未认证',
userPhone: task.userPhoneNumber || '', userIdCard: task.userIdCardNumber || '',
treeCount: task.treeCount, userPhone: task.userPhoneNumber || '',
signingDate, treeCount: task.treeCount,
}); signingDate,
},
// 6. 嵌入签名到 PDF { signatureImagePng: signatureBuffer },
pdfBuffer = await this.pdfGeneratorService.embedSignature(pdfBuffer, { );
signatureImagePng: signatureBuffer,
});
this.logger.log(`Signed PDF generated: ${pdfBuffer.length} bytes`); this.logger.log(`Signed PDF generated: ${pdfBuffer.length} bytes`);
// 7. 上传签署后的 PDF 到 MinIO // 7. 上传签署后的 PDF 到 MinIO

View File

@ -104,6 +104,9 @@ export class PdfGeneratorService {
if (this.hasFormFields(pdfDoc)) { if (this.hasFormFields(pdfDoc)) {
this.logger.log('Using form fields mode'); this.logger.log('Using form fields mode');
await this.fillFormFields(pdfDoc, data, customFont); await this.fillFormFields(pdfDoc, data, customFont);
// 预览PDF时扁平化所有字段包括签名字段
const form = pdfDoc.getForm();
form.flatten();
} else { } else {
this.logger.log('Using coordinate mode (no form fields found)'); this.logger.log('Using coordinate mode (no form fields found)');
this.fillByCoordinates(pdfDoc, data, customFont); this.fillByCoordinates(pdfDoc, data, customFont);
@ -157,8 +160,8 @@ export class PdfGeneratorService {
} }
} }
// 扁平化表单(将字段转换为静态文本,不可再编辑) // 注意:不在这里调用 form.flatten()
form.flatten(); // 签名字段需要保留,待签名嵌入后再统一扁平化
} }
/** /**
@ -339,6 +342,7 @@ export class PdfGeneratorService {
/** /**
* PDF * PDF
* PDFDocument
* @param data * @param data
* @param signature PDF * @param signature PDF
* @returns PDF Buffer * @returns PDF Buffer
@ -347,15 +351,63 @@ export class PdfGeneratorService {
data: ContractPdfData, data: ContractPdfData,
signature?: SignatureData, signature?: SignatureData,
): Promise<Buffer> { ): Promise<Buffer> {
// 先生成填充数据的 PDF this.logger.log(`Generating signed PDF for contract: ${data.contractNo}`);
let pdfBuffer = await this.generateContractPdf(data);
// 如果有签名,嵌入签名 try {
if (signature) { // 1. 加载 PDF 模板
pdfBuffer = await this.embedSignature(pdfBuffer, signature); 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;
} }
} }