ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SSM+JSP 小工程预算系统开发:从数据建模到部署实战

2026/9/16 15:55:44 拓冰建站 浏览量
SSM+JSP 小工程预算系统开发:从数据建模到部署实战 简介这套基于SSMJSP的小工程预算Java毕业设计资料包面向Java方向毕业生与课程设计学习者覆盖用户管理、装修材料管理、订单信息管理、工程信息管理、工程订单管理、材料分类管理等核心业务模块能帮助快速完成一个完整的企业级管理系统项目。资源共1272个文件涵盖Java源码、JSP页面、JavaScript脚本、CSS样式、图片与动图素材、XML配置以及MySQL数据库脚本等其中JS和CSS用于前端交互与界面美化SQL脚本可直接初始化数据库压缩包整体约32.88MB目录结构清晰便于按模块查阅与二次开发。目前已有98人学习下载适合作为毕业设计参考或SSM框架整合实战练习。除完整源码外还附带演示录像与说明文档演示录像完整展示系统操作流程说明文档梳理了环境搭建与配置要点可帮助读者快速理解项目架构、数据库设计及前后端交互逻辑有效缩短开发与调试周期。1. 基于 SSMJSP 的小工程预算先把技术分工想清楚基于 SSMJSP 做小工程预算毕业设计很多人一开始就把劲使错了方向以为难点在预算公式、造价定额、计价规则实际上做下来会发现这类系统本质就是一套标准的三层 CRUD 项目难点全在 Spring、SpringMVC、MyBatis、JSP 这四层之间的职责切分。小工程预算的业务面很小预算单、分项清单、工程量、综合单价、合价、总金额、审核状态加起来就是一个带金额计算的多表数据流转。正确的顺序是先把预算的表结构和状态机设计好再用 MyBatis 做数据访问在 Service 层做金额汇总和状态校验最后用 JSP 把预算清单渲染成可操作的表格。这篇文章围绕这个顺序把各个环境下的参数、核心代码和常见坑位都讲一遍适合正在做 SSMJSP 毕业设计、课程设计的 Java 学习者也适合想快速回忆 SSM 常规写法的在职工程师。2. 工程预算数据建模与 MyBatis 映射预算清单怎样落成表小工程预算系统在实际开发中比较容易出现两个极端要么只建一张宽表把所有内容塞进预算表要么把数据库设计做成树形科目、多级分类过度设计。多数场景下一张预算单带若干清单项再挂一些基础参考数据就够用关键是把“预算单”和“清单项”的主从关系先定下来。2.1 预算实体拆成预算单、清单项、材料参考三组表我一般会把数据模型拆成三张核心表预算单头、预算清单项、材料参考主数据。数据域表名关键字段在预算中的角色预算单头budgetid、budget_no、project_name、owner、total_amount、status、remark一份完整的预算文件记录抬头与汇总金额预算清单项budget_itemid、budget_id、seq、item_code、item_name、unit、quantity、price、amount预算文件里的每一行对应一个分项工程材料参考budget_materialid、material_name、spec、unit、price人工、材料单价参考编辑时下拉选择拆成三组而不是一张宽表理由有几点。第一清单项需要独立插入、删除、批量维护与预算头混在一张表里会导致每改一行都要更新整条大记录并发和日志都难处理第二审核预算单时只需要锁 budget 和 budget_item 两组数据不需要影响基础参考数据第三budget 主表字段非常少沉淀出一张“预算头表”也更符合工程预算书“先有封面、后有明细”的直观结构。字段类型上quantity、price、amount 建议统一使用decimal(12,2)而不是 float 或 double。工程预算对金额精度要求不高但要稳定二进制浮点类型在累计求和时容易出现 0.10.2 这类误差后续做金额校验会很头疼。item_code 最好固定长度比如“01010101”这种层级编码至少在数据库里保持唯一索引这是我在做预算类系统时保留的习惯。2.2 MyBatis Mapper 层把预算项写入和查询封装起来实体类可以直接对应 budget_item 表注意金额字段全用 BigDecimalpublic class BudgetItem { private Long id; private Long budgetId; // 所属预算单 private String itemCode; // 清单编码 private String itemName; // 分项工程名称 private String unit; // 计量单位m³、m²、t private BigDecimal quantity; // 工程量 private BigDecimal price; // 综合单价 private BigDecimal amount; // 合价 quantity * price }对应的 MyBatis Mapper XML 里最常用的是插入和按预算单查询列表insert idinsertItem parameterTypecom.example.budget.entity.BudgetItem INSERT INTO budget_item (budget_id, item_code, item_name, unit, quantity, price, amount) VALUES (#{budgetId}, #{itemCode}, #{itemName}, #{unit}, #{quantity}, #{price}, #{amount}) /insert select idlistByBudgetId resultTypecom.example.budget.entity.BudgetItem SELECT id, budget_id AS budgetId, item_code AS itemCode, item_name AS itemName, unit, quantity, price, amount FROM budget_item WHERE budget_id #{budgetId} ORDER BY seq, id /select#{budgetId}这类写法是 JDBC 的 PreparedStatement 占位符由 MyBatis 转成?后传参能避开 SQL 注入问题。查询语句里用AS budgetId这种方式给列起别名是为了把下划线列名映射成 Java 驼峰属性也可以在 mybatis-config.xml 中开启map-underscore-to-camel-case: true由框架自动完成映射两者选一种即可混用反而会让代码难读。如果清单项需要分页常见做法是引入 PageHelper 插件分页时在 Mapper 方法执行前调用PageHelper.startPage(pageNum, pageSize)注意这一行必须紧接着放在查询语句之前中间不能插入其他数据库操作否则分页范围就错了。SSM 整合时 Mapper 接口的扫描也要确认。Spring 配置里通常用MapperScan(com.example.budget.mapper)或MapperScannerConfigurer把 mapper 接口注册成 Bean漏掉这一步启动时会出现NoSuchBeanDefinitionException这是初学 SSM 时最常踩的坑。2.3 金额汇总用 SQL 聚合避免在 Java 里循环累计预算清单保存后总金额要回写到 budget 主表的 total_amount。常见做法是先把当前预算下的清单项金额汇总出来再执行一次 update。汇总可以直接交给 SQLselect idsumAmountByBudgetId resultTypejava.math.BigDecimal SELECT COALESCE(SUM(amount), 0) FROM budget_item WHERE budget_id #{budgetId} /select这里的COALESCE(SUM(amount), 0)处理的是“预算单下一条清单都没有”的情况即使 SUM 结果为 NULL也会返回 0Service 层就不用再对返回结果做空值判断。用 SQL 聚合而不是在 Java 里 for 循环累加还有个额外好处将来如果做了批量删除、批量导入只要一行 SQL 就能拿到最新汇总不必把整张清单拉到应用层再遍历。还有一个细节有些旧数据可能因为人工改库导致列表行里的 amount 与 quantity * price 不一致最终汇总也就错了。所以我在 Service 层保存清单项时会强制用quantity.multiply(price)重算 amount而不是信任前端传过来的值这条规则可以写进项目的开发规范里。3. JSP 渲染层设计预算清单页面怎么写才能配合 SSMSSM 项目里的 JSP 常被说“老”但在毕业设计这种交付场景下它反而是最稳的方案。不需要额外搭建前端工程没有跨域问题Tomcat 启动后直接访问上下文路径就能看到页面演示也顺手。关键在于 JSP 只负责展示和收集参数不写业务逻辑。3.1 为什么这个场景用 JSPJSTL 而不是前后端分离前后端分离要把页面构建、接口联调、部署产物纳入流程对 SSM 毕业设计来说多出了很多不确定性前端构建产物要放到哪里Tomcat 要不要配静态资源映射跨域要不要开 CORS。与其花时间去解决这些问题不如用 SpringMVC 的InternalResourceViewResolver直接指向 JSP 目录Controller 返回逻辑视图名就行bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean配置后Controller 里写return budget/listSpringMVC 就会去渲染/WEB-INF/views/budget/list.jsp。JSP 放在 WEB-INF 下还有一个好处客户端无法通过 URL 直接访问到 JSP 源文件所有入口都必须通过 Controller这对预算这种内部系统来说是很合适的访问控制。3.2 用 EL 表达式和 JSTL 渲染预算清单表格预算清单页面的核心是一张可读性很强的表格包含序号、清单编码、分项名称、单位、工程量、单价、合价和操作列。Controller 把预算头对象和清单列表放进 Model 后JSP 里这样渲染% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:set varctx value${pageContext.request.contextPath} / table classtable table-bordered thead tr th序号/thth清单编码/thth分项名称/thth单位/th th工程量/thth综合单价/thth合价/thth操作/th /tr /thead tbody c:forEach items${itemList} varitem varStatusst tr td${st.count}/td td${item.itemCode}/td td${item.itemName}/td td${item.unit}/td td classnumfmt:formatNumber value${item.quantity} pattern#,##0.00//td td classnumfmt:formatNumber value${item.price} pattern#,##0.00//td td classnumfmt:formatNumber value${item.amount} pattern#,##0.00//td td a href${ctx}/budget/item/${item.id}/edit编辑/a a hrefjavascript:void(0) onclickdeleteItem(${item.id})删除/a /td /tr /c:forEach /tbody /table div classtotal-box 预算总金额 strongfmt:formatNumber value${budget.totalAmount} pattern#,##0.00//strong 元 /divc:forEach的varStatusst提供st.count用于显示行号从 1 开始比直接在数据库里查序号更省事。fmt:formatNumber强制输出两位小数比如工程量 10 会显示为“10.00”这种格式对预算表来说是基本要求。${ctx}在页面顶部用c:set保存了应用上下文路径所有链接都通过它拼接避免应用部署在 Tomcat 的 ROOT 之外的路径时出现 404。前端不要承担金额计算职责最多通过一小段 jQuery 监听工程量和单价的输入事件实时把“本行金额”显示出来给用户看。这个计算只是交互反馈真正落库的 amount 仍然以后端 Service 为准。3.3 编辑预算项的 JSP 表单与提交路径预算项编辑页的表单比列表页简单但有两个容易出错的地方隐藏字段和提交路径。form action${ctx}/budget/item/save methodpost input typehidden nameid value${item.id}/ input typehidden namebudgetId value${budget.id}/ div label清单编码/label input typetext nameitemCode value${item.itemCode}/ /div div label工程量/label input typetext namequantity value${item.quantity}/ /div div label综合单价/label input typetext nameprice value${item.price}/ /div button typesubmit保存/button /form隐藏的 budgetId 必须带上因为保存时要靠它确认这条清单属于哪份预算。Controller 接收表单后把字段组装成 BudgetItem 对象再调用 Service 处理。保存成功后建议用redirect:/budget/view?id重定向到预算查看页这样浏览器刷新时不会因为重复提交表单而生成重复数据。3.4 JSP 改了不生效时的编译缓存排查JSP 会先被容器翻译成 Java Servlet 源文件再编译成 class 文件后执行。Tomcat 8/9 下编译缓存的位置是{CATALINA_HOME}/work/Catalina/localhost/{应用上下文}/org/apache/jsp/IDEA 内置 Tomcat 时这个目录会落在 IDE 的输出目录下。运行中修改 JSP 后页面没变化通常不是代码问题而是编译缓存修改后的 JSP 没有被同步到 Tomcat 实际读取的目录Tomcat 的 work 目录残留了旧编译结果应用开启了 JSP 预编译容器直接加载了旧的 class 文件排查时先把 Tomcat 的 work 目录清理掉再重启rm -rf ${CATALINA_HOME}/work/Catalina/localhost/${APP_CONTEXT}如果项目通过 Maven 构建可以再执行一次mvn clean把 target 目录清掉。这个操作做完JSP 重新翻译、重新编译绝大多数“改了不生效”的问题都能解决。4. SSM 业务层与事务预算提交、状态流转和金额计算数据层和页面确定后业务层是决定这个预算系统“像不像一个正式系统”的关键。所有写操作经过 Service数据库访问通过 MapperController 只做参数接收和页面跳转。4.1 Spring 声明式事务预算头和预算明细必须同生共死保存预算时通常要执行两步先写预算单头再批量写清单项。如果第二步失败而第一步已经提交就会出现只有预算头、没有明细行的脏数据。解决方式是在 Service 方法上加事务注解Override Transactional(rollbackFor Exception.class) public void saveBudgetWithItems(Budget budget, ListBudgetItem items) { // 预算头保存insert 或 update budgetMapper.insertOrUpdate(budget); // 编辑场景先清空旧明细再重新插入 budgetItemMapper.deleteByBudgetId(budget.getId()); BigDecimal total BigDecimal.ZERO; for (BudgetItem item : items) { item.setBudgetId(budget.getId()); if (item.getQuantity() ! null item.getPrice() ! null) { item.setAmount(item.getQuantity().multiply(item.getPrice())); } else { item.setAmount(BigDecimal.ZERO); } budgetItemMapper.insertItem(item); total total.add(item.getAmount()); } budgetMapper.updateTotalAmount(budget.getId(), total); }这段逻辑是先更新预算头再删除旧清单然后逐行插入新清单。小工程预算的清单通常只有几十行逐行 diff 的复杂度高于整体重插所以“删除全部再重插”在这个场景下反而是省心的方案。rollbackFor Exception.class表示受检异常也回滚Spring 默认只对 RuntimeException 回滚如果不显式声明业务方法里抛出的非运行时异常不会触发事务回滚。这里还能延伸出一个 Java 八股文里常见的考点同类内部调用this.saveBudgetWithItems()不会经过 Spring 代理事务注解会失效。因此事务方法要由外部调用方注入 Service 接口后再调用。4.2 Service 层状态机预算单的状态流转怎么写预算单从草稿到提交再到审核需要明确的状态流转。状态字段用字符串表示会比数字更直观状态含义允许流转到DRAFT草稿可编辑SUBMITTEDSUBMITTED已提交待审核APPROVED / REJECTEDAPPROVED审核通过不可变更REJECTED已退回DRAFT状态校验放在 Service 方法入口提交操作可以这样写public void submitBudget(Long budgetId) { Budget budget budgetMapper.selectById(budgetId); if (budget null) { throw new BusinessException(预算单不存在); } if (!DRAFT.equals(budget.getStatus())) { throw new BusinessException(仅草稿状态的预算可提交当前状态 budget.getStatus()); } BigDecimal total budgetItemMapper.sumAmountByBudgetId(budgetId); budget.setTotalAmount(total); budget.setStatus(SUBMITTED); budgetMapper.updateById(budget); }先查库、再校验状态、然后重算金额、最后更新状态这四步的顺序不能乱。每次提交都基于数据库里的最新状态做判断可以防止两个用户同时对一个预算单操作时出现状态覆盖。审核操作类似把SUBMITTED校验和status的更新换成 AUDIT 逻辑即可。4.3 Controller 只做编排参数转换与业务异常的边界SSM 项目的 Controller 最常见的坏味道是把类型转换、金额判断、数据写入全堆进一个方法。我一般会把 Controller 写得很薄Controller RequestMapping(/budget) public class BudgetController { private final BudgetService budgetService; public BudgetController(BudgetService budgetService) { this.budgetService budgetService; } PostMapping(/item/save) public String saveItem(ModelAttribute BudgetItemForm form, RedirectAttributes ra) { try { budgetService.saveBudgetWithItems( buildBudget(form.getBudgetId()), Collections.singletonList(buildItem(form))); ra.addFlashAttribute(msg, 保存成功); } catch (BusinessException e) { ra.addFlashAttribute(errMsg, e.getMessage()); } return redirect:/budget/edit?id form.getBudgetId(); } }Controller 负责把表单对象转换成 Service 需要的实体捕获 BusinessException把提示信息通过 FlashAttribute 带到重定向后的 JSP 页面。不要在 Controller 里做金额乘法也不要直接打印异常堆栈后就返回成功这样会让调用方完全不知道预算保存到底有没有成功。金额转换时我建议对前端传过来的 price、quantity 使用new BigDecimal(form.getPrice().trim())不要用Double.parseDouble再转 BigDecimal前者从字符串直接构造能保证精度不丢失。5. 验证与交付手算数据校验预算金额干净环境复现 JSP 页面这一章不讲框架原理讲一套能直接执行的验证路径。5.1 人肉验算一组预算数字作为功能验收的标尺建议在交付前构造一组固定测试数据并提前手算出期望结果。举例清单项工程量综合单价合价土方开挖1085.50855.00混凝土浇筑20600.0012000.00合计--12855.00操作顺序新建预算单头逐行录入清单项进入列表页核对金额格式提交审核。然后比对页面上的“预算总金额”是否等于 12855.00。这一步能同时验证三个逻辑单价小数位是否保留、总金额是否由 SQL 正确汇总、状态从 DRAFT 流转到 SUBMITTED 后 total_amount 有没有被重复计算覆盖。如果要对账可以用 SQL 直接验证库里的冗余字段是否一致SELECT b.budget_no, i.item_name, i.quantity, i.price, i.amount, b.total_amount FROM budget b LEFT JOIN budget_item i ON b.id i.budget_id WHERE b.budget_no YS-2025-0001;重点看 i.amount 是否等于 quantity 乘以 price再看 b.total_amount 是否等于所有 i.amount 的和。这一条 SQL 基本能找出“改了单价但没重算总金额”这类问题。5.2 部署 SOP清干净 work 目录跑通脚本再录演示交付前建议在干净环境上重新部署一遍。Java 方面确认 JDK 版本和JAVA_HOME环境变量Tomcat 端口没有被占用数据库脚本重新导入后检查jdbc.properties里的连接账号。若本机用 IDEA 启动把 Tomcat 的 work 目录删除后重启一次确认 JSP 不受编译缓存影响再用mvn clean package打成 war 包直接丢进 Tomcat 的 webapps 目录访问这一步如果通过说明从源码到运行的链路是完整的。压缩包里的“演示录像”建议录两遍第一遍走正常流程第二遍故意制造一个数据库连接失败的场景展示日志定位和恢复过程。这比只录一遍顺畅操作更能体现系统的可维护性最后回到页面刷新预算总金额证明数据完整这个验证流程才算收口。本文还有配套的精品资源点击获取