简介:这份资源是面向Java方向毕业设计与课程设计学生的SSM+JSP小工程预算系统完整项目包,适合需要完成预算管理类课题、缺乏完整项目经验的中高级学习者参考。项目采用B/S架构,以Java为开发语言,结合SSM框架与JSP页面,底层使用MySQL数据库,通过Eclipse工具开发,实现了用户管理、装修材料管理、订单信息管理、工程信息管理、工程订单管理及材料分类管理等核心模块,覆盖预算类系统的主要业务场景。压缩包共1272个文件,包含120个Java源码、109个JSP页面、364个JavaScript脚本、146个CSS样式及172张PNG图片等,另有SQL脚本与配置文件,整体约32.88MB,目录结构清晰,便于按模块查阅与二次开发。资源同时附带演示录像与说明文档,已有100人学习下载,可帮助读者快速理解系统架构、梳理功能实现思路并完成课题搭建。
1. 小工程预算系统为什么还在用 SSM+JSP:一个被低估的毕设选题
每年到选题季,计算机毕业设计里最容易被嫌弃的就是"XX管理系统",尤其是基于 SSM+JSP 的小工程预算系统,很多人一看技术栈就觉得老土。但如果你真去翻招聘网站和实际外包项目,会发现大量中小型企业内部系统、区县级工程队的预算台账、装修公司的报价工具,跑的还是 Spring + SpringMVC + MyBatis 加 JSP 这套组合。原因很直接:部署简单、依赖少、一台低配服务器就能跑,改起来不用重新学一套前端工程化链路。
这个选题真正要解决的问题是:把工程项目从立项到结算过程中的材料费、人工费、机械费、间接费按分项工程录入、汇总、对比预算与实际支出,并输出可打印的预算表。适合谁做?适合 Java 基础刚过、想稳扎稳打拿下一个能演示、能答辩、能讲清楚数据流转的毕业设计的同学,也适合需要一套可二次开发脚手架的小团队。它不炫技,但每一个环节都能讲出为什么这么设计,这恰恰是答辩老师最想听到的东西。
2. 把 SSM+JSP 跑起来:环境、依赖与最小可运行骨架
2.1 技术选型为什么是 SSM 而不是 SpringBoot
先把这个纠结讲清楚。SpringBoot 集成 JSP 在官方文档里一直属于"不推荐但支持"的状态,打包成可执行 jar 后 JSP 解析经常出问题,必须打成 war 包丢进外部 Tomcat 才稳。而传统 SSM 项目本身就是 war 结构,Tomcat 直接部署,没有内嵌容器的路径玄学。对于毕业设计这种"要交演示录像、老师可能在自己电脑上跑一遍"的场景,war 包 + 外部 Tomcat 的确定性远高于 SpringBoot 内嵌方案。
另一个现实原因是资料密度。SSM 的配置虽然啰嗦,但每一段 XML 都能找到对应解释,出问题时有大量可检索的报错记录。SpringBoot 把配置藏进自动装配,一旦版本对不上,新手连从哪查都不知道。所以我的建议是:毕设阶段用 SSM 打基础,把 IoC、AOP、MVC 请求流转、MyBatis 映射这几件事吃透,比直接上 SpringBoot 更有收获。
依赖版本上,JDK 用 8 或 11 都行,Tomcat 用 8.5 或 9,Spring 用 5.x 系列,MyBatis 用 3.5.x,MySQL 用 5.7 或 8.0。注意 MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,连接串要带时区和 SSL 参数,这是新手第一个翻车点。
2.2 Maven 依赖与目录结构
一个能跑的最小 SSM 项目,pom.xml 里核心依赖如下:
<!-- Spring 核心与 Web MVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.30</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.30</version> </dependency> <!-- MyBatis 与 Spring 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <!-- 数据库驱动,MySQL 8 用 cj 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- JSP 与 JSTL,缺一不可 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>这里servlet-api的 scope 必须是provided,因为 Tomcat 自带这份 jar,如果打进 war 包会和容器冲突,启动时报ClassCastException或NoSuchMethodError。JSTL 则必须打进包,否则 JSP 里的<c:forEach>标签直接不解析,页面显示原始标签文本。
目录结构按 Maven 标准走:src/main/java放 controller、service、mapper 接口和实体,src/main/resources放applicationContext.xml、spring-mvc.xml、mybatis-config.xml、jdbc.properties和 mapper 的 XML 文件,src/main/webapp/WEB-INF下放web.xml和jsp目录。JSP 放在 WEB-INF 下是刻意的,这样浏览器不能直接访问,必须经过 Controller 转发,权限控制才有意义。
2.3 web.xml 与两个 Spring 容器的分工
SSM 最容易讲不清的就是"为什么有两个 Spring 容器"。ContextLoaderListener加载的父容器管 Service、DAO、数据源、事务;DispatcherServlet加载的子容器管 Controller、视图解析器、拦截器。子容器能拿到父容器的 Bean,反过来不行。所以 Service 里注入 Controller 会失败,Controller 里注入 Service 正常。
<!-- web.xml 关键片段 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 解决 POST 中文乱码 --> <filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>url-pattern写/而不是/*,区别在于/只拦截静态资源之外的请求,/*会把 JSP 也拦下来导致死循环。这个细节答辩时被问到能加分。字符编码过滤器要放在所有过滤器最前面,否则参数已经被按默认编码解析过了,再设也没用。
3. 预算业务建模:从分项工程到费用汇总的数据表设计
3.1 核心表结构与字段含义
小工程预算的领域模型不复杂,但字段设计要经得起推敲。核心是四张表:项目表、分项工程表、费用明细表、用户表。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| project | id, name, location, total_budget, status, create_time | 一个工程项目一条记录,status 区分立项/施工/结算 |
| sub_item | id, project_id, item_name, unit, quantity, unit_price | 分项工程,如"土方开挖",quantity 与 unit_price 相乘得合价 |
| cost_detail | id, sub_item_id, cost_type, amount, remark | 费用明细,cost_type 区分材料/人工/机械/间接 |
| sys_user | id, username, password, role | role 区分管理员和普通录入员 |
sub_item里不直接存合价,而是存数量和单价,合价在查询时用 SQL 计算或 Java 端计算。这样做的好处是修改单价后合价自动更新,避免数据不一致。如果为了查询快把合价冗余存下来,就必须在每次更新时同步维护,这是典型的空间换一致性权衡,毕设阶段建议不冗余。
cost_detail的cost_type用枚举值存储,比如 1 材料、2 人工、3 机械、4 间接,不要直接存中文,否则后期改文案要动数据。前端展示时用 JSTL 的<c:choose>或后端字典转换。
3.2 MyBatis 映射与动态 SQL 汇总
预算汇总的核心是一条带GROUP BY的查询,按分项工程汇总各类费用:
<!-- CostDetailMapper.xml --> <select id="sumByProject" resultType="map"> SELECT s.item_name AS itemName, s.quantity * s.unit_price AS budgetAmount, SUM(CASE WHEN c.cost_type = 1 THEN c.amount ELSE 0 END) AS materialCost, SUM(CASE WHEN c.cost_type = 2 THEN c.amount ELSE 0 END) AS laborCost, SUM(CASE WHEN c.cost_type = 3 THEN c.amount ELSE 0 END) AS machineCost FROM sub_item s LEFT JOIN cost_detail c ON c.sub_item_id = s.id WHERE s.project_id = #{projectId} GROUP BY s.id, s.item_name, s.quantity, s.unit_price </select>用LEFT JOIN而不是INNER JOIN,是因为某个分项工程可能还没录入任何费用明细,但预算表里仍要显示它的预算金额。如果用内连接,这些行会消失,汇总结果就少了。CASE WHEN做行转列,把不同费用类型摊到不同列,比在 Java 里循环累加更高效,也更好维护。
参数#{projectId}用#{}而不是${},前者是预编译占位符能防 SQL 注入,后者是字符串拼接。这条规则在任何涉及用户输入的查询里都不能破例。
3.3 Service 层事务与预算校验
录入费用明细时,要同时更新分项工程的已用金额并校验是否超预算,这两步必须在同一个事务里:
@Service public class CostServiceImpl implements CostService { @Autowired private CostDetailMapper costDetailMapper; @Autowired private SubItemMapper subItemMapper; @Override @Transactional(rollbackFor = Exception.class) public void addCost(CostDetail detail) { // 先插入明细 costDetailMapper.insert(detail); // 再汇总该项目已用金额 BigDecimal used = costDetailMapper.sumUsedBySubItem(detail.getSubItemId()); SubItem item = subItemMapper.selectById(detail.getSubItemId()); BigDecimal budget = item.getQuantity().multiply(item.getUnitPrice()); // 超预算只警告不阻断,实际工程中允许调整 if (used.compareTo(budget) > 0) { item.setOverBudget(true); subItemMapper.updateById(item); } } }@Transactional的rollbackFor = Exception.class很关键。Spring 默认只对运行时异常回滚,受检异常不回滚。如果这里抛的是受检异常而没配这个属性,插入成功了但后续校验失败,数据就脏了。金额一律用BigDecimal,不要用double,浮点误差在预算场景里是致命的,0.1+0.2 不等于 0.3 这种事在财务报表上会被人追着问。
4. JSP 页面与交互:把预算表做成能演示的样子
4.1 列表页与 JSTL 循环渲染
JSP 页面现在被很多人当成过时技术,但在毕设演示里它有个优势:服务端渲染,打开就是完整页面,不依赖前端构建工具,老师电脑上不用装 node。列表页用 JSTL 渲染分项工程:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <table class="table"> <tr> <th>分项名称</th><th>单位</th><th>数量</th> <th>单价</th><th>预算合价</th><th>操作</th> </tr> <c:forEach items="${subItems}" var="item"> <tr> <td>${item.itemName}</td> <td>${item.unit}</td> <td>${item.quantity}</td> <td><fmt:formatNumber value="${item.unitPrice}" pattern="#,##0.00"/></td> <td><fmt:formatNumber value="${item.quantity * item.unitPrice}" pattern="#,##0.00"/></td> <td><a href="cost?subItemId=${item.id}">录入费用</a></td> </tr> </c:forEach> </table>fmt:formatNumber做千分位和两位小数格式化,比在 Java 端拼字符串干净。${item.quantity * item.unitPrice}在 EL 表达式里直接算,注意这里如果 quantity 是整数、unitPrice 是 BigDecimal,EL 会自动做类型转换,但精度可能受影响,严谨做法是在后端算好放进 VO。演示场景下 EL 直接算够用,但答辩被问到要能说清这个边界。
4.2 表单提交与后端接收
新增分项工程的表单提交到 Controller,用@RequestMapping接收:
@Controller @RequestMapping("/subItem") public class SubItemController { @Autowired private SubItemService subItemService; @RequestMapping("/add") public String add(SubItem subItem, Model model) { // 参数校验,数量单价必须为正 if (subItem.getQuantity() == null || subItem.getQuantity().compareTo(BigDecimal.ZERO) <= 0) { model.addAttribute("msg", "数量必须大于0"); return "subItem/add"; } subItemService.save(subItem); return "redirect:/subItem/list?projectId=" + subItem.getProjectId(); } }返回redirect:前缀会触发重定向,避免用户刷新页面重复提交。这是 POST-Redirect-GET 模式,表单提交后跳转到列表页,刷新不会重复插入。如果直接返回视图名,刷新时浏览器会重发 POST,数据就重复了,这是新手最常见的坑之一。
4.3 拦截器做登录校验
未登录用户不能直接访问预算页面,用 SpringMVC 拦截器统一处理:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } return true; } }拦截器在spring-mvc.xml里注册,<mvc:exclude-mapping>排除登录页和静态资源路径。注意拦截器只拦 Controller 请求,如果 JSP 直接放在 webapp 根目录下能被 URL 直接访问,拦截器管不到。所以前面强调 JSP 要放 WEB-INF 下,这是配套的。
5. 避坑与排查:SSM+JSP 项目最容易翻车的五个地方
5.1 启动报 NoClassDefFoundError 或 ClassNotFoundException
现象:Tomcat 启动时报找不到某个类,或者运行到某个功能才报。原因通常是依赖没打进 war 包,或者 scope 设成了 provided 但容器里没有。解决:先看WEB-INF/lib下有没有对应 jar,没有就检查 pom 的 scope;有还报错就检查是不是版本冲突,用mvn dependency:tree看依赖树,排除重复引入的低版本。
5.2 JSP 页面显示 404 或直接下载源码
现象:访问某个 JSP 返回 404,或者浏览器把 JSP 当文件下载。原因一是 JSP 放在 WEB-INF 下但 Controller 没转发,二是url-pattern配成了/*把 JSP 请求也拦了,三是 Tomcat 的conf/web.xml里 JSP Servlet 映射被改过。解决:确认 Controller 返回的视图名和视图解析器前缀后缀拼出来的路径真实存在;把 DispatcherServlet 的 url-pattern 改回/。
5.3 中文乱码,POST 和 GET 表现还不一样
现象:表单提交的中文存进数据库变成问号,或者页面显示乱码。原因:POST 乱码靠 CharacterEncodingFilter 解决,GET 乱码要靠 Tomcat 的server.xml里 Connector 加URIEncoding="UTF-8",两者机制不同。解决:过滤器配 UTF-8 且放最前;数据库连接串加useUnicode=true&characterEncoding=utf8;Tomcat 8 以上默认 URI 编码已是 UTF-8,但 7 及以下要手动加。
5.4 MyBatis 查询返回 null 或字段全空
现象:SQL 在数据库客户端能查出数据,程序里返回的对象字段全是 null。原因:实体类属性名和表字段名没对上,且没开驼峰映射。比如item_name对应itemName,MyBatis 默认不会自动转换。解决:在mybatis-config.xml里加<setting name="mapUnderscoreToCamelCase" value="true"/>,或者在 SQL 里用AS起别名。前者一劳永逸,推荐。
5.5 事务不生效,插入成功但回滚失败
现象:Service 方法抛异常了,但前面插入的数据还在。原因:Spring 事务基于代理,同类内部方法调用不走代理,@Transactional失效;或者方法不是 public;或者异常类型不在回滚范围内。解决:确保事务方法由外部调用,配rollbackFor = Exception.class,Service 类不要自己 new 自己。这个坑在答辩演示时如果被问到事务,答不上来很尴尬。
6. 让预算系统更耐问:导出、校验与二次开发方向
把基础功能跑通只是及格线,想让这个毕设真正拿得出手,得在几个细节上做深。第一个是预算表导出。答辩老师很可能会问"这个表能不能打印",用 Apache POI 生成 Excel 是常见做法,但要注意 POI 生成图表的能力有限,柱状图饼图这类需要 XSSFChart,配置繁琐。更稳的方案是导出 Excel 数据,图表交给前端 ECharts 在页面展示,各司其职。
// 用 POI 导出预算汇总表的核心逻辑 public void exportBudget(HttpServletResponse response, Long projectId) throws IOException { Workbook wb = new XSSFWorkbook(); Sheet sheet = wb.createSheet("预算汇总"); // 表头 Row header = sheet.createRow(0); String[] cols = {"分项名称", "预算合价", "材料费", "人工费", "机械费"}; for (int i = 0; i < cols.length; i++) { header.createCell(i).setCellValue(cols[i]); } // 数据行由 service 查询后填充 List<Map<String, Object>> data = costService.sumByProject(projectId); int rowIdx = 1; for (Map<String, Object> row : data) { Row r = sheet.createRow(rowIdx++); r.createCell(0).setCellValue(String.valueOf(row.get("itemName"))); r.createCell(1).setCellValue(toDouble(row.get("budgetAmount"))); // 其余列同理 } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=budget.xlsx"); wb.write(response.getOutputStream()); wb.close(); }Content-Disposition里的文件名如果含中文,要做 URL 编码,否则不同浏览器表现不一致,有的乱码有的直接截断。toDouble是自定义的 null 安全转换,因为SUM查询在无数据时返回 null,直接强转会空指针。
第二个是数据校验的层次。前端 JSP 用 JavaScript 做即时提示,后端 Controller 做参数合法性校验,数据库层用非空约束和唯一索引兜底。三层缺一不可,只靠前端校验等于没校验,绕过浏览器直接发请求就破了。答辩时能讲清这三层,说明你对"不可信输入"有概念。
第三个是二次开发方向。这套骨架换成"设备维修预算""活动经费预算""装修报价"都只是改表名和字段的事,Controller 和 Service 的分层结构不用动。如果想升级,可以把 JSP 换成 Thymeleaf 或前后端分离,但那是另一个工作量级,毕设阶段不建议动,把现有链路讲透比堆技术栈更有说服力。
我自己做这类项目最大的教训是:别一上来就想着把功能做全,先把"登录→建项目→录分项→录费用→看汇总→导出"这条主链路跑通,能演示、能讲清数据怎么流,剩下的边角功能都是加分项。主链路不通,功能再多也是散的。希望帮到你。
本文还有配套的精品资源,点击获取