一、需求介绍
项目需要使用电子合同进行客户签约入驻管理,使用Spring Boot框架实现了一套电子合同管理系统,主要涵盖电子签章功能和合同管理两大核心功能。
功能项:电子签章功能 - 合同管理
功能1:合同模板管理
功能描述
- 模板创建与编辑
- 功能概述:使用户能够创建新的合同模板或编辑现有的模板,以及签章位置信息字段的管理。
- 实现方式:利用
contract_templates表来存储模板信息,包括模板名称、类型、内容等。 - 用户界面:设计一个表单页面,让用户可以输入模板名称、类型,以及上传或编辑模板内容,并能保存这些信息。
- 模板查看
- 功能概述:允许用户查看系统中所有合同模板的信息。
- 实现方式:通过查询
contract_templates表来获取模板列表。 - 用户界面:在模板管理界面提供一个视图,列出所有模板的基本信息,例如名称、类型、创建时间等。
- 模板删除
- 功能概述:给用户提供删除不再使用的模板的功能。
- 实现方式:从
contract_templates表中移除对应的模板记录。 - 用户界面:在模板列表中加入删除按钮,用户确认后即可执行删除操作。
功能2:合同管理
功能描述
- 合同状态查看
- 功能概述:提供查看所有合同当前状态的能力。
- 实现方式:通过查询
contract_contracts表中的status字段来获取合同的状态信息。 - 用户界面:在合同管理界面展示所有合同的状态,状态可包括草稿、待签署、部分签署、已签署等,采用不同颜色或图标区分状态。
- 处理签署请求
- 功能概述:允许用户对处于待签署状态的合同进行签署操作。
- 实现方式:用户可以在
contract_contracts表中找到待签署的合同,并通过contract_signatures表更新签署状态。 - 用户界面:在合同详情页面设置签署按钮,用户点击后,系统将更新
contract_signatures表中的签署状态,并同步修改contract_contracts表中的状态。
- 签署历史记录
- 功能概述:系统自动追踪并记录每份合同的签署历程。
- 实现方式:在数据库中记录每个合同的签署记录,包括签署者ID、签署时间、签署状态等。
- 用户界面:在合同详情页面提供签署历史区域,展示详细的签署记录。
- 筛选与查询
- 功能概述:支持基于合同名称、签署者、签署时间等条件的筛选和查询功能。
- 实现方式:在管理界面集成过滤器或搜索框,便于用户根据关键词或时间范围查找特定的合同记录。
- 用户界面:提供过滤器组件,如文本输入框、下拉菜单等,以满足用户的筛选需求。
功能3:发起签署
功能描述
- 创建签署请求
- 功能概述:使用户能够创建新的签署请求。
- 实现方式:用户需填写合同的基本信息,如名称、描述、有效期等,并上传或选择合同模板。
- 用户界面:构建一个表单页面,供用户输入合同相关信息,并选择或上传合同模板。
- 指定接收方
- 功能概述:允许用户选定合同的接收对象。
- 实现方式:用户可从已有的联系人列表中挑选接收方,或手动输入接收方信息(如账户名、电话号码等)。
- 用户界面:提供联系人选框或输入框,方便用户选择或输入接收方资料。
- 附加文件
- 功能概述:允许用户上传额外的相关文件。
- 实现方式:用户能够上传附件、解释性文档等材料。
- 用户界面:在表单页面中加入附件上传控件。
- 预览与确认
- 功能概述:让用户在正式提交签署请求前预览合同文档及附件。
- 实现方式:用户可以浏览所有上传的文件,确认无误后提交签署请求。
- 用户界面:设立预览页面,显示所有文档的预览效果,用户确认后提交签署请求。
- 签署提醒与通知
- 功能概述:系统会自动向接收方发送签署提醒,并在签署流程中更新签署状态。
- 实现方式:系统通过内部消息或邮件通知接收方签署合同,同时记录签署状态的变化。
- 用户界面:虽然此功能不需要直接的用户界面交互,但必须确保通知机制的有效性和可靠性。
功能4:合同任务
功能描述
- 系统定时任务
- 功能概述:系统定期检查新注册的客户或未签署协议的客户,自动生成签订任务。
- 实现方式:编写定时任务脚本或使用调度工具定期访问
customers表,对于新客户或未签署合同的情况生成相应的任务。 - 用户界面:此功能无需直接用户界面交互,但在后台运行时应记录任务执行的日志,以便后续审计和跟踪。
二、合同管理功能实现逻辑
1. 合同创建与模板选择
-
用户创建模板:
- 当用户创建新的合同模板时,系统会在
contract_templates表中生成一条新记录,记录模板的名称、类型、内容等信息。 - 实现代码:
@PostMapping("/templates") public ResponseEntity<String> createTemplate(@RequestBody ContractTemplate template) { contractTemplateRepository.save(template); return ResponseEntity.ok("Template created successfully"); }
- 当用户创建新的合同模板时,系统会在
-
用户选择模板:
- 用户在创建合同时可以选择已有的模板,系统将根据选中的模板生成一条新的记录到
contract_contracts表中。 - 实现代码:
@PostMapping("/contracts") public ResponseEntity<String> createContract(@RequestBody Contract contract) { // 根据模板ID加载模板信息 ContractTemplate template = contractTemplateRepository.findById(contract.getTemplateId()).orElseThrow(() -> new ResourceNotFoundException("Template not found")); // 填写合同详细内容 contract.setTemplateName(template.getName()); contract.setStatus(0); // 初始状态为草稿 if (template.requiresSignature()) { // 如果模板要求签订,则生成客户签署任务 for (String clientId : contract.getClientIds()) { Signature signature = new Signature(); signature.setContractId(contract.getId()); signature.setClientId(clientId); signature.setStatus(0); // 初始状态为待签署 signatureRepository.save(signature); } } contractRepository.save(contract); return ResponseEntity.ok("Contract created successfully"); }
- 用户在创建合同时可以选择已有的模板,系统将根据选中的模板生成一条新的记录到
2. 合同签署
- 用户签署合同:
- 用户签署合同后,系统将更新
contract_signatures表中的签署状态,并根据签署状态更新contract_contracts表中的状态。 - 实现代码:
@PostMapping("/signatures/{signatureId}") public ResponseEntity<String> signContract(@PathVariable Long signatureId) { Signature signature = signatureRepository.findById(signatureId).orElseThrow(() -> new ResourceNotFoundException("Signature not found")); // 更新签署状态 signature.setStatus(1); // 已签署 signature.setSignedAt(new Date()); signatureRepository.save(signature); // 更新合同状态 updateContractStatus(signature.getContractId()); return ResponseEntity.ok("Contract signed successfully"); } private void updateContractStatus(Long contractId) { List<Signature> signatures = signatureRepository.findByContractId(contractId); int totalSignatures = signatures.size(); int signedCount = (int) signatures.stream().filter(s -> s.getStatus() == 1).count(); Contract contract = contractRepository.findById(contractId).orElseThrow(() -> new ResourceNotFoundException("Contract not found")); if (signedCount == totalSignatures) { contract.setStatus(3); // 已签署 } else if (signedCount > 0) { contract.setStatus(2); // 部分签署 } else { contract.setStatus(1); // 待签署 } contractRepository.save(contract); }
- 用户签署合同后,系统将更新
3. 合同状态更新
- 系统定时任务:
- 系统定期检查
contract_signatures表中的签署状态,并根据所有签署方的签署状态更新contract_contracts表中的状态。 - 实现代码:
@Scheduled(fixedRate = 60000) // 每分钟执行一次 public void updateContractStatusTask() { List<Contract> contracts = contractRepository.findAllByStatusNot(3); // 只处理未完成的合同 for (Contract contract : contracts) { updateContractStatus(contract.getId()); } }
- 系统定期检查
4. 合同查询与管理
-
用户访问管理界面:
- 用户访问管理界面时,系统查询
contract_contracts表中的所有合同记录。 - 实现代码:
@GetMapping("/contracts") public ResponseEntity<List<Contract>> getAllContracts() { List<Contract> contracts = contractRepository.findAll(); return ResponseEntity.ok(contracts); }
- 用户访问管理界面时,系统查询
-
用户查询合同详情:
- 用户查询特定合同的详细信息时,系统查询
contract_contracts表和contract_signatures表中的相关记录。 - 实现代码:
@GetMapping("/contracts/{contractId}") public ResponseEntity<ContractDetail> getContractDetails(@PathVariable Long contractId) { Contract contract = contractRepository.findById(contractId).orElseThrow(() -> new ResourceNotFoundException("Contract not found")); List<Signature> signatures = signatureRepository.findByContractId(contractId); ContractDetail detail = new ContractDetail(); detail.setContract(contract); detail.setSignatures(signatures); return ResponseEntity.ok(detail); }
- 用户查询特定合同的详细信息时,系统查询
-
管理员编辑或删除合同:
- 管理员可以编辑或删除合同记录,系统将相应地更新或删除
contract_contracts表中的记录。 - 实现代码:
@PutMapping("/contracts/{contractId}") public ResponseEntity<String> updateContract(@PathVariable Long contractId, @RequestBody Contract updatedContract) { Contract contract = contractRepository.findById(contractId).orElseThrow(() -> new ResourceNotFoundException("Contract not found")); contract.setName(updatedContract.getName()); contract.setDescription(updatedContract.getDescription()); contract.setEffectiveDate(updatedContract.getEffectiveDate()); contract.setExpirationDate(updatedContract.getExpirationDate()); contractRepository.save(contract); return ResponseEntity.ok("Contract updated successfully"); } @DeleteMapping("/contracts/{contractId}") public ResponseEntity<String> deleteContract(@PathVariable Long contractId) { Contract contract = contractRepository.findById(contractId).orElseThrow(() -> new ResourceNotFoundException("Contract not found")); contractRepository.delete(contract); return ResponseEntity.ok("Contract deleted successfully"); }
- 管理员可以编辑或删除合同记录,系统将相应地更新或删除
三、合同电子文件签章处理
由于前端使用微信小程序,预览和处理pdf文件的能力偏弱,采用后端进行文件的处理方案。
在pom.xml文件中添加处理PDF的相关依赖。这里推荐使用iText库,因为它功能强大且易于使用。
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.1.15</version>
</dependency>
前端发送签名和公章
确保前端能够正确地将签名(通常是图片形式)和公章发送给后端。这些图像可以是Base64编码的字符串,也可以是文件流。如果使用Base64编码,可以在HTTP请求体中直接传递;如果是文件流,则可能需要使用multipart/form-data格式。
后端接收并处理
在Spring Boot应用中创建一个Controller来接收前端的数据,并处理PDF文件。
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import com.itextpdf.io.image.ImageDataFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@RestController
public class PdfSignatureController {
@PostMapping("/add-signature")
public String addSignature(@RequestParam("file") MultipartFile file,
@RequestParam("signature") MultipartFile signatureFile,
@RequestParam("stamp") MultipartFile stampFile,
@RequestParam("x") int x,
@RequestParam("y") int y) throws IOException {
// 将MultipartFile转换为File
File pdfFile = convertMultiPartToFile(file);
File signature = convertMultiPartToFile(signatureFile);
File stamp = convertMultiPartToFile(stampFile);
// 使用iText读取PDF文档
PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfFile), new PdfWriter("output.pdf"));
Document doc = new Document(pdfDoc);
// 添加签名
Image imgSignature = new Image(ImageDataFactory.create(signature));
imgSignature.setFixedPosition(x, y);
// 添加公章
Image imgStamp = new Image(ImageDataFactory.create(stamp));
imgStamp.setFixedPosition(x + 100, y - 50); // 假设公章位于签名右侧下方
// 将图像添加到PDF中
doc.add(imgSignature);
doc.add(imgStamp);
// 关闭文档
doc.close();
return "签名和公章已成功添加到PDF文件";
}
private File convertMultiPartToFile(MultipartFile multiPart) throws IOException {
File convFile = new File(multiPart.getOriginalFilename());
FileOutputStream fos = new FileOutputStream(convFile);
fos.write(multiPart.getBytes());
fos.close();
return convFile;
}
}
这里需要注意先获取签章位置,在pdf里获取到需要放置的签章位置,也可以在pdf里插入文本,这个后续会介绍。
1. 获取PDF页面尺寸
首先,需要知道PDF页面的具体尺寸,以便确定坐标的范围。iText提供了获取页面尺寸的方法。
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.geom.PageSize;
// 打开PDF文档
PdfDocument pdfDoc = new PdfDocument(new PdfReader("input.pdf"), new PdfWriter("output.pdf"));
// 获取第一页
PdfPage page = pdfDoc.getFirstPage();
// 获取页面尺寸
PageSize pageSize = page.getPageSize();
float width = pageSize.getWidth();
float height = pageSize.getHeight();
System.out.println("Page Width: " + width);
System.out.println("Page Height: " + height);
pdfDoc.close();
2. 计算坐标
根据页面尺寸,可以计算出需要放置签名或公章的坐标。例如,如果想将签名放在页面底部中央,可以这样计算坐标
float x = (width - signatureWidth) / 2; // 中心对齐
float y = 50; // 距离底部50个单位
3. 使用固定坐标
如果已经知道了具体的坐标,可以直接使用这些坐标。假设希望将签名放在 (100, 100) 位置,公章放在 (200, 100) 位置:
Image imgSignature = new Image(ImageDataFactory.create(signaturePath));
imgSignature.setFixedPosition(100, 100);
Image imgStamp = new Image(ImageDataFactory.create(stampPath));
imgStamp.setFixedPosition(200, 100);
4. 动态计算坐标
如果需要动态计算坐标,可以根据页面内容或其他元素的位置来调整。假设有一个标题栏,高度为100个单位,希望签名放在标题栏下方的中央位置:
float titleBarHeight = 100;
float x = (width - signatureWidth) / 2;
float y = height - titleBarHeight - 50; // 标题栏下方50个单位
Image imgSignature = new Image(ImageDataFactory.create(signaturePath));
imgSignature.setFixedPosition(x, y);
5. 测试和调整
在实际应用中,可能需要多次测试和调整坐标,以确保签名和公章的位置符合预期。可以使用一些调试工具或打印出中间结果来帮助调试。
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import com.itextpdf.io.image.ImageDataFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class PdfSignatureUtil {
public static void main(String[] args) throws IOException {
// 输入和输出文件路径
String inputPdfPath = "input.pdf";
String outputPdfPath = "output.pdf";
String signaturePath = "signature.png";
String stampPath = "stamp.png";
// 打开PDF文档
PdfDocument pdfDoc = new PdfDocument(new PdfReader(inputPdfPath), new PdfWriter(outputPdfPath));
// 获取第一页
PdfPage page = pdfDoc.getFirstPage();
// 获取页面尺寸
Rectangle pageSize = page.getPageSize();
float width = pageSize.getWidth();
float height = pageSize.getHeight();
// 计算签名和公章的坐标
float signatureWidth = 100; // 假设签名宽度为100
float signatureHeight = 50; // 假设签名为50
float stampWidth = 100; // 假设公章宽度为100
float stampHeight = 100; // 假设公章高度为100
float xSignature = (width - signatureWidth) / 2; // 签名居中
float ySignature = 50; // 距离底部50个单位
float xStamp = (width - stampWidth) / 2; // 公章居中
float yStamp = ySignature - stampHeight - 10; // 公章在签名下方10个单位
// 创建Document对象
Document doc = new Document(pdfDoc);
// 添加签名
Image imgSignature = new Image(ImageDataFactory.create(signaturePath));
imgSignature.setFixedPosition(xSignature, ySignature);
// 添加公章
Image imgStamp = new Image(ImageDataFactory.create(stampPath));
imgStamp.setFixedPosition(xStamp, yStamp);
// 将图像添加到PDF中
doc.add(imgSignature);
doc.add(imgStamp);
// 关闭文档
doc.close();
pdfDoc.close();
System.out.println("签名和公章已成功添加到PDF文件");
}
}
6.坐标解释
在PDF文件中,坐标单位默认是“用户单位”(User Units)。根据PDF规范,1个用户单位默认等于1/72英寸,这大约相当于1个点(point)。因此,PDF中的坐标单位通常是点(points)。
详细解释
-
用户单位(User Units):
- PDF中的默认用户单位是1/72英寸,即1点(point)。
- 1英寸 = 72点。
- 1厘米 ≈ 28.346点。
-
坐标系:
- PDF页面的坐标系原点(0, 0)位于页面的左下角。
- x轴向右增加,y轴向上增加。
1、示例
假设有一个A4纸大小的PDF页面,A4纸的尺寸是210mm × 297mm。
- 将毫米转换为点:
- 宽度:210mm * 28.346 ≈ 595.28点
- 高度:297mm * 28.346 ≈ 841.89点
因此,A4纸的PDF页面尺寸大约是595.28点 × 841.89点。
2、坐标示例
假设希望将公章放在页面的中心位置:
- 页面宽度:595.28点
- 页面高度:841.89点
- 公章宽度:100点
- 公章高度:100点
计算公章的中心坐标:
- x坐标:(595.28 - 100) / 2 = 247.64点
- y坐标:(841.89 - 100) / 2 = 370.945点
7.多页pdf处理
处理多页PDF文件时,可能需要在特定的页面上放置签名或公章。iText库提供了多种方法来访问和操作PDF文件中的不同页面。
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import com.itextpdf.io.image.ImageDataFactory;
import java.io.IOException;
public class PdfSignatureUtil {
public static void main(String[] args) throws IOException {
// 输入和输出文件路径
String inputPdfPath = "input.pdf";
String outputPdfPath = "output.pdf";
String signaturePath = "signature.png";
String stampPath = "stamp.png";
// 打开PDF文档
PdfDocument pdfDoc = new PdfDocument(new PdfReader(inputPdfPath), new PdfWriter(outputPdfPath));
// 获取第2页
int pageNumber = 2;
PdfPage page = pdfDoc.getPage(pageNumber);
// 获取页面尺寸
Rectangle pageSize = page.getPageSize();
float width = pageSize.getWidth();
float height = pageSize.getHeight();
// 假设公章的位置在 (100, 100),宽度为 100,高度为 100
float xSignature = 100;
float ySignature = 100;
float signatureWidth = 100;
float signatureHeight = 50;
float xStamp = 200;
float yStamp = 100;
float stampWidth = 100;
float stampHeight = 100;
// 创建Document对象
Document doc = new Document(pdfDoc);
// 添加签名
Image imgSignature = new Image(ImageDataFactory.create(signaturePath));
imgSignature.setFixedPosition(pageNumber, xSignature, ySignature);
// 添加公章
Image imgStamp = new Image(ImageDataFactory.create(stampPath));
imgStamp.setFixedPosition(pageNumber, xStamp, yStamp);
// 将图像添加到PDF中
doc.add(imgSignature);
doc.add(imgStamp);
// 关闭文档
doc.close();
pdfDoc.close();
System.out.println("签名和公章已成功添加到PDF文件的第 " + pageNumber + " 页");
}
}
8.pdf转图片处理
小程序的预览pdf文件,需要web-view的方式,但开通业务域名时,又受到了域名端口的限制,将pdf转成图片,进行小程序端的预览。
在 pom.xml 中添加以下依赖
<dependencies>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.27</version>
</dependency>
</dependencies>
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class PdfToSingleImageConverter {
public static void main(String[] args) {
String inputPdfPath = "input.pdf";
String outputImagePath = "output.png"; // 确保文件名包含扩展名
int dpi = 300; // DPI (dots per inch)
int pageSpacing = 20; // 每页之间的间距
try (PDDocument document = PDDocument.load(new File(inputPdfPath))) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
int numberOfPages = document.getNumberOfPages();
int totalHeight = 0;
int maxWidth = 0;
// 计算总高度和最大宽度
for (int page = 0; page < numberOfPages; ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, dpi, ImageType.RGB);
totalHeight += bim.getHeight();
maxWidth = Math.max(maxWidth, bim.getWidth());
}
// 加上每页之间的间距
totalHeight += (numberOfPages - 1) * pageSpacing;
// 创建最终的合并图像
BufferedImage combinedImage = new BufferedImage(maxWidth, totalHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = combinedImage.createGraphics();
// 将每一页的图像绘制到合并图像中
int yPosition = 0;
for (int page = 0; page < numberOfPages; ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, dpi, ImageType.RGB);
g2d.drawImage(bim, 0, yPosition, null);
yPosition += bim.getHeight();
if (page < numberOfPages - 1) {
yPosition += pageSpacing; // 添加每页之间的间距
}
}
g2d.dispose();
// 保存合并后的图像
File outputFile = new File(outputImagePath);
ImageIO.write(combinedImage, "PNG", outputFile);
System.out.println("PDF pages combined into a single image and saved as: " + outputFile.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}
}
}
至此,已将文件做好了处理,再将处理好的文件上传到服务器,返回给前端地址即可。
4764

被折叠的 条评论
为什么被折叠?



