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)}...`;
}
// 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

View File

@ -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<Buffer> {
// 先生成填充数据的 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;
}
}