JavaWeb合同管理系统实战:从环境搭建到审批流与部署避坑

发布时间:2026/10/8 4:53:24
JavaWeb合同管理系统实战:从环境搭建到审批流与部署避坑
简介这是一套基于Java Web技术栈开发的合同管理系统源码包面向需要学习企业级Web应用开发的学生、初级开发者及小型企业信息化选型人员帮助理解合同全生命周期管理的实现思路涵盖合同创建、审批、变更、到期提醒与审计追踪等核心业务场景。压缩包共941个文件约16.18MB以html、css、png、js等前端资源为主配合jsp页面、java源码、class字节码、jar依赖包及xml、json配置文件构成完整的前后端工程结构另有少量db数据文件与工程元数据文件便于直接导入IDE运行调试。目前已有1010人学习下载。资源包含登录鉴权、合同操作、用户信息管理、导出Excel、验证码生成等模块的实现代码读者可据此掌握Servlet、JSP与MVC分层架构的落地方式并参考DAO数据访问与工具类的封装技巧适合作为课程设计或二次开发的参考底本。1. 合同管理系统新版拆包一套 JavaWeb 项目能解决哪些实际业务很多团队在合同管理上还在用共享文件夹加 Excel 台账法务改一版、销售改一版最后谁手里的是终稿都说不清。这套 JavaWeb 合同管理系统免费版核心解决的就是合同从起草、审批、签署到归档的全流程线上化问题。它基于 Spring Boot MyBatis 技术栈构建前端走 JSP 或模板引擎渲染数据库用 MySQL属于典型的 Java 工程师能快速上手二次开发的 Web 项目。适合谁一是中小型企业想低成本搭一套内部合同流转工具二是 Java 学习者需要一个业务逻辑完整、模块划分清晰的实战项目来练手三是接私活的开发者拿它当基础框架改造成客户交付版。它不是什么高精尖的微服务架构但胜在业务闭环完整、代码结构规整能让你把精力放在合同业务本身而不是环境折腾上。2. 环境搭建与数据库初始化从零把项目跑起来2.1 技术栈确认与依赖版本选择拿到一个 JavaWeb 项目包第一件事不是急着导入 IDE而是先看pom.xml或build.gradle确认技术栈版本。这套合同管理系统常见配置是 Spring Boot 2.x 搭配 MyBatis-PlusJDK 要求 1.8 或 11MySQL 5.7 或 8.0。为什么强调版本因为 Spring Boot 2.7 和 3.x 在 Jakarta 包名上不兼容MyBatis-Plus 3.5 前后 API 也有差异。我一般会先扫一眼依赖树确认没有引入奇怪的商业授权库再决定用哪个 JDK 跑。常见做法是本地装两个 JDK一个 8 用于老项目维护一个 17 用于新项目。这套系统如果pom.xml里写的是java.version1.8/java.version那就老老实实用 JDK 8别硬上 17否则编译期就可能报Unsupported class file major version。MySQL 驱动注意mysql-connector-java和mysql-connector-j的区别8.0 以上用后者URL 里要带serverTimezoneAsia/Shanghai不然时间字段存进去会差 8 小时。2.2 数据库建表与初始数据导入项目包里通常有个sql目录里面是建表脚本和初始数据。导入之前先建库字符集用utf8mb4排序规则utf8mb4_general_ci。合同管理系统涉及合同主体、审批流、附件、操作日志等表字段里会有TEXT和DATETIME字符集不对会导致中文乱码或表情符号插入失败。-- 创建数据库字符集必须 utf8mb4 CREATE DATABASE contract_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 导入建表脚本在命令行执行 -- mysql -u root -p contract_db contract_schema.sql -- mysql -u root -p contract_db contract_data.sql建表脚本里如果有外键约束导入顺序要注意先导主表再导从表。初始数据里一般会有一个 admin 账号密码可能是 MD5 或 BCrypt 加密的登录不上就去看sys_user表里的password字段用对应加密方式重新生成一个。别直接改明文登录逻辑里会做加密比对。2.3 application.yml 关键配置项配置文件是项目能不能跑起来的命门。数据库连接、端口、文件上传路径、日志级别都在这里。下面是一份典型配置我按参数含义逐项说明。server: port: 8080 # 如果被占用改成 8081别和本地其他服务冲突 spring: datasource: url: jdbc:mysql://localhost:3306/contract_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password # 改成你自己的 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 50MB # 合同附件可能较大默认 1MB 不够用 max-request-size: 100MB mybatis-plus: mapper-locations: classpath:mapper/*.xml # XML 映射文件位置 configuration: map-underscore-to-camel-case: true # 数据库下划线转 Java 驼峰 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期打印 SQLmap-underscore-to-camel-case这个参数特别关键。数据库字段叫contract_nameJava 实体里叫contractName不开这个映射查出来就是 null。开发期把 SQL 日志打开能省很多排查时间上线再关掉。文件上传大小限制也要调合同 PDF 加附件很容易超过默认值不调的话上传直接抛MaxUploadSizeExceededException。2.4 启动类与常见启动失败排查启动类上一般有SpringBootApplication和MapperScan(com.xxx.mapper)。如果忘了加MapperScanMyBatis 找不到 Mapper 接口启动时报Consider defining a bean of type xxxMapper。端口冲突报Port 8080 was already in use改端口或杀掉占用进程。数据库连不上先看 URL 里的库名、时区、账号密码再看 MySQL 服务有没有起。启动成功后访问http://localhost:8080能看到登录页就说明前后端通了。如果页面 404检查application.yml里有没有配spring.mvc.view.prefix和suffixJSP 项目还要确认pom.xml里引入了tomcat-embed-jasper依赖否则 JSP 不会被解析。3. 合同核心业务模块拆解从实体设计到审批流实现3.1 合同实体与 MyBatis-Plus 映射合同表是整个系统的核心字段设计直接决定后续功能好不好扩展。典型字段包括合同编号、名称、甲方乙方、金额、签订日期、生效日期、到期日期、状态、创建人、审批人。用 MyBatis-Plus 的话实体类加TableName和TableId注解主键策略选ASSIGN_ID或AUTO。Data TableName(contract_info) public class ContractInfo { TableId(type IdType.AUTO) private Long id; private String contractNo; // 合同编号业务唯一 private String contractName; private String partyA; private String partyB; private BigDecimal amount; // 金额用 BigDecimal别用 double private Date signDate; private Date expireDate; private Integer status; // 0 草稿 1 审批中 2 已生效 3 已归档 private Long createBy; private Date createTime; }金额字段用BigDecimal是血泪经验double做加减会出现0.1 0.2 0.30000000000000004这种精度问题合同金额算错是要出事的。状态字段用整数枚举别用字符串查询和索引效率更高。合同编号建议加唯一索引防止并发插入重复。3.2 合同增删改查接口与分页MyBatis-Plus 的BaseMapper已经提供了单表 CRUD分页需要配一个PaginationInnerInterceptor拦截器。下面是一个合同列表查询的 Service 方法带条件筛选和分页。public PageContractInfo pageQuery(int pageNum, int pageSize, String keyword, Integer status) { LambdaQueryWrapperContractInfo wrapper new LambdaQueryWrapper(); // 关键字模糊匹配合同名称或编号 if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(ContractInfo::getContractName, keyword) .or().like(ContractInfo::getContractNo, keyword)); } if (status ! null) { wrapper.eq(ContractInfo::getStatus, status); } wrapper.orderByDesc(ContractInfo::getCreateTime); return contractMapper.selectPage(new Page(pageNum, pageSize), wrapper); }LambdaQueryWrapper的好处是字段名用方法引用编译期就能发现拼写错误比手写字符串强。分页拦截器要在配置类里注册否则selectPage返回的是全量数据页码参数不生效。关键字查询用and包一层避免or条件和其他eq条件优先级混乱导致查错数据。3.3 审批流的状态机实现合同审批是这套系统的业务重心。简单实现是一个状态机草稿提交后变审批中审批通过变已生效驳回退回草稿。每次状态变更要记录操作日志谁在什么时间把状态从 A 改成了 B。Transactional public void approve(Long contractId, Long approverId, boolean pass, String remark) { ContractInfo contract contractMapper.selectById(contractId); if (contract.getStatus() ! 1) { throw new BizException(当前合同不在审批中无法操作); } contract.setStatus(pass ? 2 : 0); contractMapper.updateById(contract); // 写审批日志 ApprovalLog log new ApprovalLog(); log.setContractId(contractId); log.setApproverId(approverId); log.setResult(pass ? 通过 : 驳回); log.setRemark(remark); log.setCreateTime(new Date()); approvalLogMapper.insert(log); }Transactional保证状态更新和日志写入要么都成功要么都回滚。状态判断前置防止重复审批。审批日志单独建表不要塞进合同主表否则一条合同多次审批会撑爆字段。如果业务复杂到多级审批常见做法是引入approval_flow和approval_node两张表按节点顺序流转这套免费版一般只做了单级审批二次开发时可以扩展。3.4 文件上传与合同附件管理合同附件上传要处理文件存储路径、文件名冲突、类型校验。别把文件存到项目目录里打成 jar 包后路径会变。我一般配一个绝对路径比如/data/contract/upload/在application.yml里可配。PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { String originalName file.getOriginalFilename(); String suffix originalName.substring(originalName.lastIndexOf(.)); // 只允许 pdf、doc、docx if (!Arrays.asList(.pdf, .doc, .docx).contains(suffix.toLowerCase())) { return Result.fail(不支持的文件类型); } String newName UUID.randomUUID().toString() suffix; File dest new File(uploadPath newName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); return Result.ok(newName); }文件名用 UUID 重命名避免中文名和特殊字符导致存储失败。类型校验用后缀白名单别信前端传来的Content-Type那个可以伪造。上传目录要提前建好并给写权限Linux 下注意运行用户的权限否则报Permission denied。4. 避坑与常见问题排查那些让你加班到凌晨的坑4.1 中文乱码从数据库到页面全链路排查现象合同名称存进去是问号或者页面显示乱码。原因通常有三层数据库字符集不是utf8mb4、连接 URL 没带characterEncodingutf8、Tomcat 的URIEncoding没配。解决顺序是从底往上查先SHOW CREATE DATABASE contract_db看字符集再检查 JDBC URL最后看server.xml里 Connector 有没有加URIEncodingUTF-8。三层都对了乱码基本消失。4.2 文件上传大小超限与临时目录问题现象上传大合同报MaxUploadSizeExceededException或The temporary upload location is not valid。原因是 Spring Boot 默认上传限制 1MB且临时目录在系统重启后可能被清理。解决在application.yml里调大max-file-size和max-request-size同时显式配置spring.servlet.multipart.location指向一个存在的目录。Linux 下/tmp可能被定时清理建议指到应用自己的数据目录。4.3 MyBatis 字段映射为 null 的三种情况现象查询出来某些字段是 null但数据库里明明有值。原因一map-underscore-to-camel-case没开contract_name映射不到contractName。原因二实体类字段名和数据库列名完全对不上比如数据库叫c_name实体叫contractName需要TableField(c_name)显式指定。原因三写了自定义 XML 查询但resultMap没配全漏了字段。排查时先开 SQL 日志看查出来的列名再对照实体字段。4.4 事务不生效的典型场景现象审批方法加了Transactional但状态更新了日志没写进去。原因一方法不是public的Spring AOP 代理不生效。原因二同类内部方法直接调用绕过了代理对象。原因三异常被 catch 了没往外抛事务管理器认为没出错。解决确保方法是public、通过注入的代理对象调用、异常要么不 catch 要么 catch 后throw new RuntimeException(e)。这个坑我踩过不止一次后来养成习惯事务方法里 catch 块必须手动回滚或重新抛出。4.5 合同编号并发生成重复现象两个用户同时创建合同生成了相同的合同编号。原因是编号生成逻辑用了「查最大值加一」这种非原子操作。解决用数据库序列或 Redis 原子自增或者给合同编号字段加唯一索引插入冲突时捕获异常重试。最省事的做法是唯一索引兜底业务层做一次重试别指望应用层锁能完全防住。5. 二次开发与部署上线的几个关键动作5.1 从免费版到可交付版本的改造清单免费版能跑通流程但离交付还有距离。我一般按这个顺序改先加操作日志切面记录每个接口的调用人和参数再把硬编码的审批人改成从角色表动态查然后补数据权限销售只能看自己的合同法务能看全部最后加导出 Excel 功能领导最喜欢这个。改造时别动核心表结构用扩展表或新增字段的方式做方便后续升级。5.2 打包部署与生产环境配置差异开发用mvn spring-boot:run生产要打成可执行 jar。mvn clean package -DskipTests之后target目录下会有个几十 MB 的 jarjava -jar xxx.jar --spring.profiles.activeprod启动。生产配置和开发配置分文件application-prod.yml里数据库密码用环境变量注入别写死在文件里。日志级别从DEBUG调到INFOSQL 日志关掉不然日志文件几天就撑满磁盘。# 生产启动示例堆内存按服务器配置调整 java -Xms512m -Xmx1024m -jar contract-system.jar --spring.profiles.activeprod app.log 21 -Xms和-Xmx设成一样大避免运行期频繁扩容。 app.log 21 把标准输出和错误都重定向到日志文件并后台运行。别忘了配logback-spring.xml做日志滚动按天切割保留 30 天。5.3 验证部署是否成功的检查项部署完别只看进程在不在。先curl http://localhost:8080/login看返回码是不是 200再登录进去创建一条合同走完审批流最后检查上传附件能不能下载。数据库连接池监控如果有配 Druid看活跃连接数是否正常。这套流程走一遍基本能确认系统可用。从那以后我每次部署完都强制走一遍「登录、建合同、审批、上传、下载」五步验证少一步都可能漏掉问题。希望帮到你。本文还有配套的精品资源点击获取