Spring Boot +微信小程序实现电子合同功能(电子签、手签)

一、需求介绍

项目需要使用电子合同进行客户签约入驻管理,使用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)。

详细解释
  1. 用户单位(User Units)

    • PDF中的默认用户单位是1/72英寸,即1点(point)。
    • 1英寸 = 72点。
    • 1厘米 ≈ 28.346点。
  2. 坐标系

    • 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();
        }
    }
}

至此,已将文件做好了处理,再将处理好的文件上传到服务器,返回给前端地址即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值