SpringBoot+Vue合同管理系统:业务流驱动的契约执行中枢
2026/9/12 4:45:57 网站建设 项目流程

简介:本资源是一套基于Spring Boot与Vue技术栈开发的企业级合同管理系统完整源码包,面向Java全栈初学者、毕业设计学生及中小型团队开发者,旨在解决合同全生命周期管理中的起草、审批、签署、履行与归档等核心业务痛点。压缩包共728个文件,涵盖93个Java后端逻辑文件、46个Vue前端组件、156个JS交互脚本、162个SVG图标资源、49个CSS样式文件及1个SQL建库脚本,辅以bat部署脚本与yml配置文件,结构清晰、模块解耦,便于学习分层架构与前后端联调。包体大小18.85MB,已通过Tomcat7+与MySQL 5.7环境严格调试,开箱即用。目前已有2715人学习下载,资源包含可直接运行的项目源码、配套数据库脚本、功能说明文档及多套静态资源(Bootstrap/Element UI等),特别适合用于毕设实战、技术栈整合训练与企业轻量级合同管理原型搭建。

1. 合同管理系统不是电子文档归档工具,而是业务流驱动的契约执行中枢

很多团队拿到「SpringBoot + Vue 合同管理系统」这个标题,第一反应是:又一个 CRUD 表单项目?但实际落地时才发现,合同生命周期远比增删改查复杂——它要对接法务审核节点、触发用印审批流、关联供应商主数据、校验签约主体资质、生成带时间戳的 PDF 签章文件、同步归档至 OA 或档案系统,甚至要支持多版本比对和条款变更追溯。本项目正是围绕这些真实业务断点设计:后端用 SpringBoot 构建领域模型(Contract、Party、Clause、ApprovalRecord),前端用 Vue 实现动态表单渲染(支持条款模板拖拽配置)、PDF 预览与签署状态联动、以及基于路由守卫的权限粒度控制(如法务仅见审核中合同,财务仅见已付款条款)。适合正在从 Excel 管理转向数字化契约治理的中小型企业 IT 团队,或需要快速交付合同模块的 Java/Vue 全栈开发者——它不提供 SaaS 服务,但给出可嵌入现有系统的标准接口与可复用组件。

2. 搭建合同管理基础骨架:SpringBoot 后端核心结构与关键依赖选型

2.1 为什么选择 SpringBoot 2.7.x 而非 3.x?

当前主流企业级项目仍以 JDK 8/11 为主,而 SpringBoot 3.x 强制要求 JDK 17+ 且移除了 Servlet API 的部分兼容层。合同管理系统需对接老旧 OA 系统(如泛微 e-cology)的 WebService 接口,其 WSDL 文件依赖 JAX-WS 运行时,该组件在 SpringBoot 3.x 中已被弃用。因此,本项目采用 SpringBoot 2.7.18(2023 年 LTS 版本),既满足 Java 8 兼容性,又获得安全补丁支持。若你环境已升级至 JDK 17,可将spring-boot-starter-parent替换为3.2.5,但需同步替换spring-boot-starter-webspring-boot-starter-webflux并重写 Controller 层。

<!-- pom.xml 关键依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <!-- 合同核心:JPA 持久化 + Lombok 减少样板代码 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- PDF 生成:iText 7.2.5(支持数字签名与条款高亮) --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext7-core-pdfhtml</artifactId> <version>4.0.2</version> <type>pom</type> </dependency> <!-- 审批流:轻量级 Flowable 6.8.0(避免 Activiti 复杂配置) --> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter-process</artifactId> <version>6.8.0</version> </dependency> </dependencies>

提示:iText 7 商业授权较严格,若项目为内部系统且无对外分发需求,可使用 AGPLv3 版本;若需商用,请购买官方许可。替代方案是 Apache PDFBox,但其数字签名支持弱于 iText。

2.2 合同实体建模:从「一份文档」到「可执行契约」

合同不是静态 PDF,而是由多个聚合根组成的业务对象。本项目定义以下核心实体关系:

实体关键字段业务含义
ContractcontractNo,status(DRAFT/APPROVING/SIGNED/EXPIRED),signDate,expireDate主合同头信息,status驱动整个生命周期
PartypartyType(CLIENT/SUPPLIER),name,creditCode,legalRepresentative签约主体,支持同一主体多次出现(如甲方母公司+子公司)
ClausetemplateId,content,isMandatory,version条款内容,templateId关联预设模板库,version支持历史回溯
ApprovalRecordnodeId,approverId,status(PENDING/APPROVED/REJECTED),comment审批记录,与 Flowable 的ACT_HI_PROCINST表双向同步
// Contract.java - 使用 @EntityGraph 预加载关联数据,避免 N+1 查询 @Entity @Table(name = "t_contract") @NamedEntityGraph( name = "Contract.withPartiesAndClauses", attributeNodes = { @NamedAttributeNode("parties"), @NamedAttributeNode("clauses") } ) public class Contract { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "contract_no", unique = true, nullable = false) private String contractNo; // 格式:CT-2024-0001 @Enumerated(EnumType.STRING) @Column(name = "status") private ContractStatus status = ContractStatus.DRAFT; @OneToMany(mappedBy = "contract", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Party> parties = new ArrayList<>(); @OneToMany(mappedBy = "contract", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Clause> clauses = new ArrayList<>(); }
2.2.1 动态条款模板引擎:用 Thymeleaf 渲染合同正文

合同正文不是纯文本,而是含变量占位符的模板(如${client.name}${totalAmount})。系统在生成 PDF 前,先用 Thymeleaf 解析模板并注入实时数据:

@Service public class ContractTemplateService { private final TemplateEngine templateEngine; public ContractTemplateService(TemplateEngine templateEngine) { this.templateEngine = templateEngine; } public String renderHtml(Contract contract) { Context context = new Context(); context.setVariable("contract", contract); context.setVariable("parties", contract.getParties()); context.setVariable("clauses", contract.getClauses()); // 注入当前日期、审批人等上下文变量 context.setVariable("now", LocalDateTime.now()); return templateEngine.process("contract-template", context); } }

注意:contract-template.html存放于src/main/resources/templates/,需启用 Thymeleaf 自动配置(spring-boot-starter-thymeleaf已包含)。此设计使法务人员可直接编辑 HTML 模板,无需修改 Java 代码。

2.3 REST API 设计:面向 Vue 前端的资源契约

API 不按传统 CRUD 命名,而是按业务动作设计,降低前端理解成本:

URLMethod说明响应示例
POST /api/contracts/draft创建草稿合同返回Contract对象及draftId{ "id": 101, "contractNo": "CT-2024-0001", "status": "DRAFT" }
PUT /api/contracts/{id}/submit提交审核触发 Flowable 流程实例{ "processInstanceId": "a1b2c3d4" }
GET /api/contracts/{id}/pdf生成 PDF返回application/pdf二进制 PDF 内容
GET /api/contracts/{id}/diff?fromVersion=2&toVersion=3版本比对返回 HTML 格式差异报告<ins>新增条款</ins><del>删除条款</del>
@RestController @RequestMapping("/api/contracts") public class ContractController { @PostMapping("/draft") public ResponseEntity<Contract> createDraft(@RequestBody ContractDraftRequest request) { Contract contract = contractService.createDraft(request); return ResponseEntity.status(HttpStatus.CREATED).body(contract); } @PutMapping("/{id}/submit") public ResponseEntity<Map<String, String>> submitForApproval( @PathVariable Long id, @RequestBody ApprovalSubmitRequest request) { String processId = approvalService.startProcess(id, request.getApproverIds()); return ResponseEntity.ok(Map.of("processInstanceId", processId)); } }

3. Vue 前端实现:动态表单、PDF 预览与审批状态驱动 UI

3.1 项目初始化与依赖安装:Vue 3 + Composition API + Element Plus

本项目采用 Vue 3.4(Composition API)而非 Options API,因其更契合合同管理的复杂状态管理需求。使用create-vue脚手架初始化,并安装以下核心依赖:

# 创建项目(选择 TypeScript、Router、Pinia、ESLint) npm create vue@latest # 安装 UI 组件库与工具 npm install element-plus@2.3.12 npm install axios@1.6.7 npm install pdf-lib@3.1.0 # 用于前端 PDF 编辑(如添加水印) npm install @vueuse/core@10.7.2 # 提供 useStorage、useAsyncState 等组合式函数

提示:pdf-lib仅用于前端轻量操作(如添加公司水印),正式签署必须调用后端 iText 生成带数字签名的 PDF,因浏览器环境无法安全存储私钥。

3.2 合同创建页:拖拽式条款模板配置

合同创建页不是固定表单,而是基于 JSON Schema 的动态渲染器。法务管理员预先配置条款模板库(存于数据库t_clause_template表),前端通过v-for渲染可拖拽区块:

<!-- ContractCreate.vue --> <template> <el-container> <el-aside width="300px"> <h3>条款模板库</h3> <draggable :list="templates" item-key="id" ghost-class="ghost" @end="onDragEnd" > <template #item="{ element }"> <div class="template-item" @click="addClause(element)"> {{ element.name }} ({{ element.category }}) </div> </template> </draggable> </el-aside> <el-main> <clause-editor v-model:clauses="currentClauses" :readonly="false" /> <el-button @click="saveDraft">保存草稿</el-button> </el-main> </el-container> </template> <script setup> import { ref, onMounted } from 'vue' import draggable from 'vuedraggable' const templates = ref([]) const currentClauses = ref([]) // 加载模板库(调用 /api/clause-templates) onMounted(async () => { const res = await axios.get('/api/clause-templates') templates.value = res.data }) const addClause = (template) => { currentClauses.value.push({ templateId: template.id, content: template.defaultContent, isMandatory: template.isMandatory, version: 1 }) } </script>
3.2.1 条款编辑器组件:支持富文本与变量插入

<clause-editor>组件封装了 Quill 富文本编辑器,并内置变量插入按钮(如「客户名称」「签约日期」),点击后自动插入${client.name}占位符,后端渲染时替换为真实值:

// components/ClauseEditor.vue import { onMounted, ref } from 'vue' import Quill from 'quill' import 'quill/dist/quill.snow.css' export default { props: ['modelValue', 'readonly'], emits: ['update:modelValue'], setup(props, { emit }) { const editor = ref(null) onMounted(() => { const quill = new Quill(editor.value, { theme: 'snow', readOnly: props.readonly, modules: { toolbar: [ [{ 'header': [1, 2, 3, false] }], ['bold', 'italic', 'underline'], [{ 'color': [] }, { 'background': [] }], [{ 'variable': ['${client.name}', '${totalAmount}', '${now}'] }] // 自定义变量按钮 ] } }) quill.on('text-change', () => { emit('update:modelValue', quill.root.innerHTML) }) }) return { editor } } }

3.3 PDF 预览与签署状态同步:WebSocket 实时更新

合同 PDF 不直接返回二进制流,而是生成临时访问链接(如/pdf/preview/101?token=abc),前端用<iframe>嵌入。但关键在于:当法务在后台完成审批,需实时通知前端更新 UI。本项目采用 SpringBoot 的spring-boot-starter-websocket+ Vue 的@stomp/stompjs实现:

// WebSocketConfig.java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); // 订阅主题 registry.setApplicationDestinationPrefixes("/app"); // 发送前缀 } }
// utils/websocket.js import { Client } from '@stomp/stompjs' const stompClient = new Client({ brokerURL: 'ws://localhost:8080/ws', onConnect: () => { stompClient.subscribe('/topic/contract-status', (message) => { const data = JSON.parse(message.body) if (data.contractId === currentContractId.value) { // 更新当前合同状态徽标 contractStatus.value = data.status } }) } }) export default stompClient

注意:WebSocket 连接需在用户进入合同详情页时建立,离开时断开,避免长连接泄漏。/topic/contract-status主题由后端在审批完成时广播:simpMessagingTemplate.convertAndSend("/topic/contract-status", statusUpdate);

4. 合同 PDF 生成与数字签名:iText 7 的安全实践

4.1 生成带时间戳与条款高亮的 PDF

合同 PDF 不是简单导出 HTML,而是用 iText 7 将 Thymeleaf 渲染的 HTML 转为 PDF,并添加业务必需元素:

  • 时间戳:调用国家授时中心 API(http://www.ntp.org.cn/)获取权威时间,写入 PDF 元数据
  • 条款高亮:对Clause实体中标记为isMandatory=true的段落,用黄色背景突出显示
  • 页眉页脚:每页显示合同编号、页码、生成时间(如「CT-2024-0001 · 第 1 页 · 2024-06-15 14:22:33」)
@Service public class PdfGenerationService { public byte[] generatePdf(Long contractId) throws IOException { Contract contract = contractRepository.findById(contractId).orElseThrow(); String html = templateService.renderHtml(contract); // 使用 XMLWorker 解析 HTML(iText 7.2+ 推荐使用 HtmlConverter) ConverterProperties properties = new ConverterProperties(); properties.setBaseUri("https://example.com/static/"); Document document = HtmlConverter.convertToDocument(html, properties); // 添加时间戳元数据 document.getDocumentInfo().setCreationDate(Calendar.getInstance()); document.getDocumentInfo().setAuthor("Contract System v1.0"); // 高亮强制条款(遍历 Paragraph,匹配 ${clause.id} 占位符) for (IBlockElement element : document.getChildren()) { if (element instanceof Paragraph) { Paragraph p = (Paragraph) element; if (p.getText().contains("${clause.mandatory}")) { p.setBackgroundColor(ColorConstants.YELLOW); } } } ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfWriter writer = new PdfWriter(baos); PdfDocument pdfDoc = new PdfDocument(writer); document.copyTo(pdfDoc); document.close(); pdfDoc.close(); return baos.toByteArray(); } }

4.2 数字签名集成:对接 CFCA 或上海 CA 的国密 SM2 签名

国内合同签署必须符合《电子签名法》,要求使用国家认可的 CA 机构颁发的数字证书。本项目预留 SM2 签名接口,实际部署时需替换为合作 CA 的 SDK:

// SignatureService.java - 抽象签名策略 public interface SignatureService { byte[] sign(byte[] pdfBytes, String certPath, String password) throws Exception; } // CFCASignatureService.java - CFCA 实现(需引入 cfca-sdk.jar) @Component public class CFCASignatureService implements SignatureService { @Override public byte[] sign(byte[] pdfBytes, String certPath, String password) { // 调用 CFCA SDK 的 Signer.sign() 方法 // 此处省略具体 SDK 调用代码,实际需配置 CFCA 服务地址、证书路径 return cfcaSigner.sign(pdfBytes, certPath, password); } }

提示:CFCA SDK 需向中国金融认证中心申请,获取cfca-sdk.jar及测试证书。生产环境必须使用硬件 USBKey 存储私钥,禁止明文存储密码。SM2 签名后,PDF 的AcroForm字段会嵌入数字签名,可用 Adobe Reader 验证「签名有效」与「证书可信」。

5. 部署与生产调优:Nginx 反向代理、PDF 生成性能与敏感信息防护

5.1 Nginx 配置:分离静态资源与 API 请求

Vue 打包后的dist/目录由 Nginx 直接托管,API 请求反向代理至 SpringBoot(默认 8080):

# /etc/nginx/conf.d/contract.conf server { listen 80; server_name contract.example.com; # 前端静态资源 location / { root /var/www/contract-frontend; try_files $uri $uri/ /index.html; } # API 接口代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # PDF 预览临时链接(需设置过期时间) location /pdf/preview/ { proxy_pass http://127.0.0.1:8080/; proxy_cache_valid 200 302 10m; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; } }

注意:/pdf/preview/路径需在 SpringBoot 中配置@GetMapping("/pdf/preview/{id}"),并校验token参数有效性,防止未授权访问。建议使用 JWT 签名 token,有效期设为 5 分钟。

5.2 PDF 生成性能优化:异步队列与缓存策略

PDF 生成是 CPU 密集型操作,直接同步处理会导致接口超时。本项目采用@Async+ Redis 缓存:

@Service public class AsyncPdfService { @Async("taskExecutor") // 配置独立线程池,避免阻塞 Web 线程 public void generateAndCachePdf(Long contractId) { try { byte[] pdfBytes = pdfGenerationService.generatePdf(contractId); // 存入 Redis,Key: pdf:contract:{id}, TTL: 24h redisTemplate.opsForValue() .set("pdf:contract:" + contractId, pdfBytes, Duration.ofHours(24)); } catch (Exception e) { log.error("PDF generation failed for contract {}", contractId, e); } } }
// Controller 中改为异步触发 @GetMapping("/pdf/preview/{id}") public ResponseEntity<Resource> previewPdf(@PathVariable Long id) { // 先查 Redis 缓存 byte[] cachedPdf = (byte[]) redisTemplate.opsForValue().get("pdf:contract:" + id); if (cachedPdf != null) { return ResponseEntity.ok() .contentType(MediaType.APPLICATION_PDF) .body(new ByteArrayResource(cachedPdf)); } // 缓存未命中,触发异步生成并返回 loading 页面 asyncPdfService.generateAndCachePdf(id); return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) .body(new ByteArrayResource("<h3>PDF 正在生成,请稍候...</h3>".getBytes())); }

5.3 敏感信息防护:HeapDump 与日志脱敏

SpringBoot 默认开启 HeapDump 功能(/actuator/heapdump),可能泄露合同中的客户名称、金额等敏感数据。必须禁用或加权:

# application-prod.yml management: endpoint: heapdump: show-details: NEVER # 禁止暴露堆转储 endpoints: web: exposure: include: health,info,metrics,prometheus # 仅暴露必要端点 endpoint: health: show-details: WHEN_AUTHORIZED # 健康检查详情需认证

同时,所有日志输出需脱敏:

// Logback 配置 logback-spring.xml <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%ex{10}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 自定义脱敏转换器 --> <conversionRule conversionWord="sensitive" converterClass="com.example.contract.util.SensitiveLogConverter"/>
// SensitiveLogConverter.java public class SensitiveLogConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { String message = event.getFormattedMessage(); // 脱敏手机号、身份证号、银行账号 message = message.replaceAll("(?<!\\d)\\d{3}(?=\\d{4})", "****"); message = message.replaceAll("\\d{17}[\\dXx]", "*****************"); return message; } }

提示:SensitiveLogConverter仅处理日志消息体,数据库 SQL 日志需在application.yml中关闭logging.level.org.hibernate.SQL=OFF,或使用p6spy代理 JDBC 进行字段级脱敏。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询