news 2026/11/2 10:31:53

SSM+JSP小工程预算系统毕设实战:从环境搭建到预算汇总全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM+JSP小工程预算系统毕设实战:从环境搭建到预算汇总全流程

简介:这份资源是面向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 核心表结构与字段含义

小工程预算的领域模型不复杂,但字段设计要经得起推敲。核心是四张表:项目表、分项工程表、费用明细表、用户表。

表名关键字段说明
projectid, name, location, total_budget, status, create_time一个工程项目一条记录,status 区分立项/施工/结算
sub_itemid, project_id, item_name, unit, quantity, unit_price分项工程,如"土方开挖",quantity 与 unit_price 相乘得合价
cost_detailid, sub_item_id, cost_type, amount, remark费用明细,cost_type 区分材料/人工/机械/间接
sys_userid, username, password, rolerole 区分管理员和普通录入员

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 或前后端分离,但那是另一个工作量级,毕设阶段不建议动,把现有链路讲透比堆技术栈更有说服力。

我自己做这类项目最大的教训是:别一上来就想着把功能做全,先把"登录→建项目→录分项→录费用→看汇总→导出"这条主链路跑通,能演示、能讲清数据怎么流,剩下的边角功能都是加分项。主链路不通,功能再多也是散的。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 10:31:34

Python抓取小说章节与正文:从页面分析到断点续爬的完整实践

网络小说阅读这件事&#xff0c;很多人都有过“追更追到一半&#xff0c;想把自己喜欢的章节存下来慢慢看”的念头。我最早动这个心思&#xff0c;是因为追的一本书更新太慢&#xff0c;想先把已发布的章节抓下来做成离线文档&#xff0c;通勤路上没网也能翻。后来发现身边不少…

作者头像 李华
网站建设 2026/11/2 10:29:18

鸢尾花数据集上的四大分类算法比较与实践解析

简介&#xff1a;一份面向机器学习初学者的期末作业参考实现&#xff0c;围绕鸢尾花数据集完成逻辑回归、随机森林、K近邻、决策树四种经典分类算法的横向比较&#xff0c;可直接用于课程设计或期末大作业。压缩包共七个文件&#xff0c;其中包含四个源代码脚本&#xff0c;分别…

作者头像 李华
网站建设 2026/11/2 10:29:11

JSP期末大作业社团管理系统源码解析:Servlet与Ajax审批流实战

简介&#xff1a;这套基于JSP的期末大作业社团管理系统源代码&#xff0c;适用于大学阶段网页设计课程的项目实战&#xff0c;适合需要快速搭建社团管理后台的学生参考。系统具备成员信息维护、请假申请审批、退团请求处理以及匿名信息反馈等模块&#xff0c;前端使用异步方式提…

作者头像 李华
网站建设 2026/11/2 10:29:06

WinForm上位机MQTT客户端实例:从通信基座到调试避坑

简介&#xff1a;C# WinForm MQTT客户端实例附完整工程源码&#xff0c;面向需要在.NET平台实现物联网通信的C#桌面开发者&#xff0c;适用于快速搭建支持发布订阅、遗嘱消息、TLS/SSL加密的MQTT客户端&#xff0c;也可作为测试与监控工具的基础。压缩包内共121个文件&#xff…

作者头像 李华
网站建设 2026/11/2 10:28:57

Unity连接MySQL显示表格:Simple TableUI实现与避坑指南

简介&#xff1a;面向 Unity 开发者的 MySQL 数据展示整合包&#xff0c;适用于在 Unity 2020.3.45f1 客户端直连数据库、读取表格数据&#xff0c;并借助 Simple TableUI 完成界面渲染的技术场景&#xff0c;尤其适合做后台管理面板、数据监控看板或本地工具类应用的中级开发者…

作者头像 李华
网站建设 2026/11/2 10:28:19

OpenClaw记忆插件memory-lancedb-pro解析与应用

1. OpenClaw记忆插件memory-lancedb-pro深度解析 在AI助手领域&#xff0c;长期记忆能力一直是提升用户体验的关键。OpenClaw作为新一代智能助手平台&#xff0c;其memory-lancedb-pro插件通过本地向量数据库实现了高效的记忆存储与检索。这个官方插件将记忆数据存储在LanceDB中…

作者头像 李华