做计算机毕业设计,最怕的不是题目难,而是题目老、资料少、找不到参考,最后憋到答辩前一周才开始赶工。超市进销存管理系统算是一个非常经典的选题了,它好就好在业务逻辑清楚,模块划分自然,工作量适中,而且用 SSM 框架去做正好能体现你对 Java Web 技术栈的掌握程度。我当初做这个系统的时候,也是从网上扒了一大堆资料,最后发现真正能一口气跑起来的源码少得可怜,很多帖子要么缺数据库脚本,要么配置文件写了一半,折腾两天还卡在启动报错上。这篇文章我就把自己从选题、设计、编码到部署的思路完整捋一遍,后面附带的源码也是我本地实测跑通的版本,希望能帮你少走点弯路。
这篇文章适合两类人看:一类是正在准备计算机毕业设计、考虑用 SSM 做仓库或进销存方向的同学;另一类是刚学完 SSM 框架、想找一个完整项目练手的初学者。我不会只贴代码,还会把每一步为什么这么做的逻辑讲清楚,这样你答辩的时候被老师追问设计思路,也能从容应对。
1. 为什么选“超市进销存”这个题目
1.1 选题背后的需求逻辑
很多同学选毕设题目时有个误区,觉得题目越新、技术越高大上越好。实际上,毕业设计和商业项目不同,核心目标是展示你掌握了哪些能力,以及能不能独立完成一个完整的项目。超市进销存管理系统恰恰能满足这几个要求。
从业务角度看,超市的日常运营绕不开三件事:进什么货、货放哪里、货卖了多少。进货对应采购管理,货放哪里对应库存管理,货卖了多少对应销售管理和统计报表。这三个核心环节天然形成了业务闭环,你可以围绕它们做模块划分,也能扩展出供应商管理、会员管理、预警提醒等功能。模块多了,工作量就上去了,模块少了,内容又不够写论文,这个度刚好可以通过进销存系统来灵活控制。
从技术角度看,进销存系统涉及增删改查、关联查询、事务处理、分页搜索、数据统计报表等常见开发场景,能把 SSM 框架的各个层面都用上,包括 Spring 的依赖注入和事务管理、SpringMVC 的请求流转、MyBatis 的 SQL 映射和动态 SQL。相比做那种花里胡哨的前端页面,这种项目更能让答辩老师看到你的后端功底。
这里还要多说一句,进销存系统的业务逻辑比单纯的“XX管理系统”要复杂一点,因为有库存加减、价格计算、单据和明细表关联这些操作,所以在项目里能体现业务级的事务处理能力,这在答辩时是很加分的点。
1.2 SSM 技术栈选型的真实原因
现在很多同学可能会纠结,是不是该用 Spring Boot 或者 Spring Cloud?我的建议是,如果你学校教学用的是 SSM 框架,或者你还没有完全掌握微服务那套东西,就老老实实选 SSM。
原因很简单:SSM 是传统 Java Web 开发的基础组合,Spring Boot 本质上是在 Spring 和 SpringMVC 基础上的自动配置整合。你在 SSM 项目里要手动配置数据源、配置事务管理器、配置 MyBatis 的 SqlSessionFactory,这些东西折腾一遍,你对框架内部怎么运作的理解会比直接套用 Spring Boot 深得多。答辩的时候,老师常常会问“你配这个事务是怎么生效的”,如果你亲手配过<tx:advice>或者@Transactional,你答起来就有底气了。
选 SSM 还有另一个现实原因:网上的参考资料多,前人踩过的坑基本都有解决方案。我的项目用的是经典 SSM 组合,也就是 Spring 5 + SpringMVC + MyBatis 3,配合 MySQL 5.7 和 Maven 做依赖管理。这套组合稳定、兼容性好、官方文档和社区资料都很全,毕设做它性价比最高。
2. 项目整体设计与核心模块拆解
2.1 功能模块怎么划分才清晰
超市进销存管理系统的功能划分,我建议遵循“一个角色一条线”的思路,把普通超市的日常操作流程映射到系统模块里。我的项目里分了这几个核心模块:
- 商品管理:负责商品分类和商品信息的维护,包括名称、条码、规格、单位、进价、售价、库存上下限等。
- 供应商管理:维护供应商的基本资料,方便采购时关联供应商信息。
- 采购入库管理:填写采购单,选择供应商和商品,填写数量和进价,确认入库后自动增加库存。
- 销售出库管理:填写销售单,选择商品填写销售数量,保存后自动扣减库存并计算销售金额。
- 库存管理:查看当前所有商品的库存数量,支持库存预警,低库存商品高亮提示,还可以做库存盘点,调整差异数量。
- 报表统计:按时间段统计进货金额、销售金额、毛利,用列表展示每日或每月的销售趋势。
你可能已经注意到了,在进销存这样的系统里,“单据”和“明细”是两个非常关键的概念。以采购单为例,一张采购单要同时记录单头信息(单号、供应商、采购日期、操作员、总金额)和单体信息(本次采购了哪些商品、每个商品多少数量、什么进价),所以数据库里至少是两张表,而且要在事务里一次性保存。这是我项目里特别重视的一部分,也是后面写代码时容易出错的地方。
2.2 数据库设计是重中之重
数据库设计好不好,直接影响写代码的难度。我第一次做这个系统的时候,就是图省事每张表都单独设计,结果做销售单的时候字段对不上,改了一晚上表结构。后来我重新梳理了表的关系,才把数据模型稳定下来。
我的表设计大体如下:
user:用户表,字段包括 id、用户名、密码、真实姓名、角色,角色可以简单分管理员和收银员。category:商品分类表,字段包括 id、分类名称、备注。product:商品表,字段包括 id、分类 id、条码、名称、规格、单位、进价、售价、库存数量、库存下限、库存上限。supplier:供应商表,字段包括 id、供应商名称、联系人、联系电话、地址、备注。purchase:采购单表,字段包括 id、采购单号、供应商 id、采购日期、总金额、操作人、备注。purchase_item:采购单明细表,字段包括 id、采购单 id、商品 id、采购数量、采购单价、小计金额。sale:销售单表,字段与采购单类似。sale_item:销售单明细表,字段与采购单明细表类似。
关键的设计点是,商品表里的库存数量是一个“冗余字段”,每次采购入库或销售出库时都会更新它。这里要强调一下,商品库存数量并非只能通过查流水表汇总得到,如果每次都去 sum 所有入库记录再减去所有出库记录,数据量大了以后性能会很差。直接在 product 表里维护一个实时库存字段,虽然有一定的冗余,但对一个小型超市进销存系统来说,是最实用的做法。
另外,采购单号和销售单号我推荐用“日期+自增编号”的方式生成,比如PO20250601001,这样人眼识别方便,数据库里做唯一索引也好做,还方便按日期搜索。
2.3 项目目录结构与分层思路
SSM 项目最忌讳的是把代码全部塞到 Controller 里,那样改起来会非常痛苦。我用了经典的三层架构:表现层(Controller)、业务层(Service)、持久层(Mapper)。
项目的 Maven 目录结构如下:
ssm-supermarket/ ├── pom.xml ├── src/main/java/com/example/supermarket/ │ ├── controller/ # 控制器,接收前端请求 │ ├── service/ # 业务接口 │ ├── service/impl/ # 业务实现类 │ ├── mapper/ # MyBatis Mapper接口 │ ├── entity/ # 实体类 │ ├── dto/ # 数据传输对象,用于接收页面参数 │ └── common/ # 公共类,比如分页结果、统一返回结果 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML映射文件 │ ├── spring/ # Spring配置文件 │ ├── mybatis-config.xml │ └── jdbc.properties └── src/main/webapp/ ├── static/ # 静态资源:JS、CSS、图片 └── WEB-INF/ ├── views/ # JSP页面 └── web.xml很多同学喜欢把业务逻辑写在 Service 里,这一点要明确:Service 层是业务的核心,比如库存加减、金额计算这种逻辑,一定放在 Service 里,Controller 只负责接收参数、调用 Service 方法、返回视图或 JSON 数据。Mapper 层只做 SQL 查询和持久化操作,不写业务判断。
这里有一个我踩过的坑要特别提醒:实体类字段和数据库字段的命名要尽量保持一致,或者用 MyBatis 的mapUnderscoreToCamelCase配置来自动映射。比如数据库字段是create_time,Java 属性是createTime,如果不开驼峰映射就要在每个查询里自己用resultMap映射,那会很啰嗦。我项目里直接在mybatis-config.xml里开了这个配置:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>这个配置虽然不起眼,但能省下大量重复的 resultMap 代码。
3. 核心功能实现与关键代码解析
3.1 Spring、SpringMVC、MyBatis 的配置串联
搭建 SSM 项目,第一步是把三大框架整合起来。网上有很多零散配置,但如果你不理解配置文件的加载顺序,很容易出问题。我来理一下思路。
web.xml 是整个项目的入口,它负责启动 Spring 容器和 SpringMVC 容器。一个常见的坑是,Spring 的容器是父容器,SpringMVC 的容器是子容器。子容器可以拿到父容器的 Bean,但父容器拿不到子容器的 Bean。所以在配置扫描包时,Spring 的扫描要扫除 Controller 以外的包,SpringMVC 只扫 Controller。
我的 web.xml 核心配置如下:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/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/springmvc.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>这里再解释一下为什么用<url-pattern>/</url-pattern>而不是*.do。其实两种都能用,但/表示所有请求都经过 SpringMVC,配合注解驱动和静态资源映射,代码风格会更 RESTful 一点。如果你用了*.do,链接里就要带 .do 后缀,我个人不太喜欢这种风格。
Spring 的配置里,最核心的是数据源、事务管理器、以及 MyBatis 的 SqlSessionFactory。我用的是 Druid 连接池,配置如下:
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.supermarket.mapper"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>这里有一个细节必须注意:mapperLocations配置的是 MyBatis XML 文件路径,如果你在src/main/resources/mapper目录下放的是ProductMapper.xml,那ProductMapper.java接口里的方法名就要和 XML 里的statement id完全一致。如果出现“Invalid bound statement (not found)”这个报错,第一反应就是去检查 XML 文件的namespace是不是写成了全限定接口名,以及方法名是否一致。
3.2 商品的增删改查与分页搜索
商品管理是所有模块的基础,其他模块都要引用商品表。它的核心操作就是分页搜索和增删改查。分页我用了 PageHelper,这是一个 MyBatis 的分页插件,功能很强大,配置也简单。
在 Spring 配置里加这一段:
<bean id="sqlSessionFactory" ...> ... <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <value> helperDialect=mysql reasonable=true </value> </property> </bean> </array> </property> </bean>然后在 Service 层里,只需要在查询前调用PageHelper.startPage(pageNum, pageSize),后面紧跟的 Mapper 查询就会自动带 limit。返回的时候把查询结果包装成 PageInfo,就能拿到总条数、总页数这些分页数据。
举一个商品查询的例子,ServiceImpl 里大概是这样的逻辑:
@Override public PageInfo<Product> findProductList(String keyword, Integer pageNum, Integer pageSize) { PageHelper.startPage(pageNum, pageSize); List<Product> list = productMapper.searchProducts(keyword); return new PageInfo<>(list); }对应的 Mapper XML 使用了动态 SQL,这样搜索框不输入关键字时也能正常查询:
<select id="searchProducts" resultType="com.example.supermarket.entity.Product"> SELECT * FROM product <where> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR barcode LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY id DESC </select>写分页搜索的时候,有几个容易踩的小坑。第一个是PageHelper.startPage必须紧跟在你要分页的 Mapper 查询之前,中间不能夹着其他查询,否则分页插件会把不是目标查询的 SQL 也处理了,结果就是分页失效甚至报错。第二个是如果你的查询里还有子查询或者嵌套结果集,PageHelper 有时会统计不对,遇到这种情况可以手动写一个 count 查询来修正。
3.3 采购入库与事务处理的核心逻辑
采购入库模块是整个系统里最有业务含量的部分。它的流程是:用户在前端填写一张采购单,选择供应商,然后往明细里添加若干商品,每个商品还要填数量和进价。提交的时候,后端要同时完成以下操作:
- 生成采购单主记录,包含单号、供应商、日期、总金额。
- 逐条插入采购单明细。
- 更新商品库存:
product.stock = product.stock + 采购数量。 - 如果有库存下限逻辑,还要检查是否需要触发补货预警。
因为这四个操作要保证“要么全成功,要么全失败”,所以必须加上事务控制。我推荐在 Service 方法上加@Transactional注解,简单直接,而且 Spring 声明式事务能帮你处理回滚。
核心代码类似这样:
@Transactional(rollbackFor = Exception.class) @Override public void createPurchaseOrder(PurchaseDTO dto) { Purchase purchase = new Purchase(); purchase.setPurchaseNo(generatePurchaseNo()); purchase.setSupplierId(dto.getSupplierId()); purchase.setPurchaseDate(new Date()); purchase.setOperator(dto.getOperator()); double totalAmount = 0; // 先插入主单,获取主单ID purchaseMapper.insert(purchase); for (PurchaseItemDTO item : dto.getItemList()) { PurchaseItem detail = new PurchaseItem(); detail.setPurchaseId(purchase.getId()); detail.setProductId(item.getProductId()); detail.setQuantity(item.getQuantity()); detail.setPrice(item.getPrice()); detail.setSubtotal(item.getQuantity() * item.getPrice()); totalAmount += detail.getSubtotal(); purchaseItemMapper.insert(detail); // 更新库存 productMapper.increaseStock(item.getProductId(), item.getQuantity()); } // 更新主单总金额 purchase.setTotalAmount(totalAmount); purchaseMapper.updateTotalAmount(purchase); }这里有几个容易想不明白的地方,我展开解释一下。
第一,为什么先插主单再插明细?因为明细表里需要一个purchase_id作为外键关联主单。如果我们先插明细,就没有主单 ID 可用。MyBatis 里要拿到自增主键,可以在 insert 语句里配置useGeneratedKeys="true" keyProperty="id",这样插入后实体的 id 字段就会被自动填上。
<insert id="insert" parameterType="com.example.supermarket.entity.Purchase" useGeneratedKeys="true" keyProperty="id"> INSERT INTO purchase (purchase_no, supplier_id, purchase_date, operator, remark) VALUES (#{purchaseNo}, #{supplierId}, #{purchaseDate}, #{operator}, #{remark}) </insert>第二,库存更新用productMapper.increaseStock(productId, quantity),而不是先 select 再 set,这是为了减少一次查询,也避免并发下出现库存覆盖的问题。SQL 类似:
<update id="increaseStock"> UPDATE product SET stock = stock + #{quantity} WHERE id = #{id} </update>这里用stock = stock + #{quantity}而不是stock = #{stock} + #{quantity},关键区别是前者直接在数据库层面做原子更新,不需要先读出当前库存,安全性更高。
第三,事务只对 RuntimeException 回滚,对受检异常默认不回滚,所以我在注解里写rollbackFor = Exception.class,确保任何异常都触发回滚。
3.4 销售出库、库存扣减与负库存处理
销售出库的逻辑和采购入库是镜像的,但有个额外的顾虑:不能卖超了。也就是说,销售数量不能大于当前库存,否则会出现负库存,这在超市场景里是不允许的。
我实现的方法是在扣减库存前先检查库存是否充足,然后在更新库存的 SQL 里再加一个条件,双保险:
int rows = productMapper.decreaseStock(productId, quantity); if (rows == 0) { throw new RuntimeException("商品" + productId + "库存不足"); }对应的 SQL:
<update id="decreaseStock"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity} </update>这个方法很巧妙,MySQL 的 UPDATE 会检查 WHERE 条件,如果stock >= quantity不成立,就影响 0 行。我们就可以根据影响行数来判断是否扣减成功。这么做的好处是,在并发场景下也能防止超卖,因为数据库行锁会保证更新是串行的。对于毕设项目来说,这一处细节足够让你在答辩时讲半分钟,而且能体现你对并发问题的敏感性。
销售单保存后,前端通常还需要显示当前商品的库存余量。我建议在页面提交前,用 AJAX 实时查询商品库存并做前端校验,这样就避免了用户填了 99 个结果提交后才发现库存不足的情况。
3.5 登录认证与拦截器实现细节
既然是管理系统,登录和权限控制肯定是绕不开的。我的系统用了最简单的 Session 登录方案:用户登录成功后,把用户对象放到 Session 里,然后写一个拦截器拦截所有页面请求,只有登录过的人才能访问。
登录 Controller 的核心逻辑:
@PostMapping("/login") public String login(String username, String password, HttpSession session) { User user = userService.login(username, password); if (user != null) { session.setAttribute("loginUser", user); return "redirect:/index"; } return "redirect:/login?error=1"; }密码存储我用了 MD5 加盐处理,虽然现在推荐用 BCrypt,但毕设项目里 MD5 加盐已经是及格线了,答辩老师一般不会在这个点上刁难你。如果你有余力,可以引入 spring-security-crypto 里的 BCryptPasswordEncoder,这个在论文里能写成“使用 BCrypt 加密算法保证密码安全”,会显得专业很多。
拦截器配置在 springmvc.xml 里:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.example.supermarket.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>注意这里要把静态资源路径排除掉,否则 CSS、JS 文件也被拦截,页面样式会全部丢失。
3.6 报表统计模块的思路
报表统计模块是进销存系统里的加分项,也是很多同学觉得“不知道从哪下手”的地方。其实核心就是几个带 GROUP BY 的聚合查询。
比如按天统计销售金额:
<select id="countDailySale" resultType="map"> SELECT DATE_FORMAT(sale_date, '%Y-%m-%d') AS day, SUM(total_amount) AS amount FROM sale GROUP BY day ORDER BY day DESC LIMIT 30 </select>这里的返回类型用 map,是为了避免为聚合结果单独创建实体类。前端拿到后可以用 JSTL 或者 JS 图表库渲染。如果你想让论文显得有亮点,可以引入 ECharts,把每天的销售趋势画成折线图、把商品销售占比画成饼图,视觉效果会好很多。
毛利统计也很有意思。利润的核心计算方式是“售价金额 - 进价成本”,但在设计表结构时,我们可以直接在 sale_item 里冗余保存商品的进价字段,这样统计毛利就不需要再去关联商品表取进价了。有些同学可能会问:进价不是应该以商品表里的最新进价为准吗?但实际情况是,商品进价是波动的,采购时是什么价格就应该按那个价格算成本,所以在销售明细里存一个 snapshot 进价字段是最稳妥的,也方便以后做成本核算。
4. 部署运行的完整指南与避坑心得
4.1 环境准备与项目导入
要把项目跑起来,环境配置是关键。我用的是下面的组合,已经实测过兼容性没问题:
- JDK 1.8
- Maven 3.6 以上
- Tomcat 8.5 或 9
- MySQL 5.7
- IDEA 2021 或更新版本
拿到源码后,部署的步骤大致是:
- 先用 IDEA 打开项目,等待 Maven 自动下载依赖。如果下载很慢,建议在
settings.xml里配置阿里云镜像,这里不展开,但这是很多新手卡住的第一关。 - 在 MySQL 里新建数据库,我项目里叫
db_supermarket,然后把sql目录里的初始化脚本执行一遍,它会自动建表并插入测试数据。 - 修改
jdbc.properties里的数据库地址、用户名、密码,三个参数一定要改对。 - 配置 Tomcat,把项目部署到 Tomcat 中,启动。
- 浏览器访问
http://localhost:8080/ssm-supermarket/,如果看到登录页面,说明项目启动成功。
登录页面的初始账号密码在数据库脚本里给定,一般是admin/123456,但是为了安全,你部署后应该立刻更改。
4.2 启动过程中常见的报错排查
项目跑不起来,对新手来说非常折磨,因为报错信息五花八门。我把这几年看到学生遇到的最多的错误整理成了一个速查表,你在部署时可以直接对照:
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
Invalid bound statement (not found): ...ProductMapper.findById | Mapper XML 没被扫描到,或 namespace 写错 | 检查 Spring 配置里的mapperLocations是否为classpath:mapper/*.xml,检查 XML 的 namespace 是全限定接口名 |
java.sql.SQLException: Access denied for user 'root'@'localhost' | 数据库用户名或密码错误 | 检查jdbc.properties,注意不要有多余空格 |
Table 'db_supermarket.product' doesn't exist | 数据库脚本没执行成功,或连错数据库 | 确认脚本执行完成,在 MySQL 里用show tables查看 |
| 启动时端口被占用 | Tomcat 8080 被其他程序占用 | 修改 Tomcat 端口,或先把占用程序停掉 |
| 404,访问不到页面 | 项目没有正确部署到 webapps,或 context path 不对 | 检查 IDEA 里 Artifacts 配置,确认 deployment 里的 Application context |
ClassNotFoundException: com.mysql.cj.jdbc.Driver | MySQL 驱动版本和连接串不匹配 | 把驱动改成com.mysql.cj.jdbc.Driver,URL 里加serverTimezone=Asia/Shanghai |
Failed to configure a DataSource | Spring 配置里没有加载到 jdbc.properties | 检查<context:property-placeholder>路径是否正确 |
这些报错里最经典的就是Invalid bound statement,几乎每个 SSM 新手都会遇到。它的本质原因是 Mapper 接口和 XML 映射文件没有关联上,要么是 XML 没在mapperLocations指定的目录里,要么是 namespace 写错。排查思路也很简单:先编译项目,确认 XML 是否被复制到了 target/classes 目录下。如果 XML 不在,说明 Maven 没有把它当资源文件,需要在 pom.xml 里配置 resources 节点。
4.3 论文配图和演示时的注意点
做毕设答辩,演示环节其实比代码本身更重要。你应该提前准备几条演示路径,不要现场随意点击。
我的建议是:登录后先展示主页,介绍系统概览和库存预警信息;然后演示商品管理,指出分页搜索功能,现场搜索一个关键字;再演示采购入库,录一张带 3 个商品的采购单,展示库存变化;再演示销售出库,录入一个销售单,展示库存扣减和毛利计算;最后打开报表统计,展示销售趋势图。这一套流程走下来,基本能把系统的核心功能全部覆盖。
实操时有个小技巧:提前准备一批商品测试数据,数量别太少,至少几十条,这样分页效果才明显。演示销售单的时候,别选库存只有 1 的商品,万一被别人测试过已经卖完了,你现场提交会直接报错,那就尴尬了。
5. 答辩常见问题与自查清单
5.1 五个高频答辩问题怎么答
老师答辩时很少会问特别刁钻的问题,基本都是围绕项目本身发问。我梳理了几个高频问题,每个问题背后都是要你展示对项目原理的理解。
问题一:为什么使用 SSM 框架,它的优点是什么?
这是一个送分题,但很多同学答得太浅。你可以这样组织回答:SSM 是 Spring + SpringMVC + MyBatis 的组合,Spring 负责解耦对象管理,通过 IoC 和 AOP 管理 Bean;SpringMVC 负责请求分发,通过 DispatcherServlet 把请求路由到 Controller;MyBatis 负责持久层操作,封装了 JDBC,支持灵活的 SQL 编写。三者各司其职,让系统层次清晰、便于维护。
问题二:采购入库时如何保证数据一致性?
这个问题直接考察事务概念。你回答时要说清楚:采购入库涉及插入主单、插入明细、更新库存三个操作,任何一个失败都必须回滚。我用 Spring 的声明式事务,在 Service 方法上添加@Transactional注解,交给 Spring 管理事务的提交和回滚。
问题三:库存是怎么更新的?如果多个人同时下单怎么办?
库存更新我直接在 SQL 里用UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}这条原子语句,数据库行锁能保证同一商品只能串行扣减,所以不会出现超卖。
问题四:如何进行权限控制?
我的系统通过 Session 保存登录用户,实现了一个拦截器,将除登录请求和静态资源外的所有请求都拦截下来,判断 Session 中是否有用户对象。后续拓展可以做基于角色表的动态权限控制。
问题五:项目里你遇到的最大难点是什么?
回答这个问题要有故事感,不要泛泛而谈。你可以说:最开始的难点是 MyBatis 多表关联时,结果集映射经常出问题,后来我通过自定义 DTO 类来承接多表查询结果,绕开了 resultMap 的复杂配置,也让代码更直观。这里“自定义 DTO”是个很好的回答点,因为它是实际开发的常用技巧,老师听了会觉得你真做过项目。
5.2 提交源码前的自查清单
交源码之前,建议按照下面这个清单逐项检查,避免被答辩老师现场指出低级问题:
- 数据库脚本是否能一键执行,执行后是否有测试数据。
- 数据库连接配置是否和环境匹配,是否误提交了真实密码。
- 项目在干净环境下能否一次启动成功。
- 所有页面是否有统一风格,错误提示是否友好。
- 论文里的表结构、截图是否和实际代码一致。
- README 文件是否写了如何部署和默认账号。
- 代码里是否有无用的
System.out.println或调试代码。 - 删除本地缓存文件(如 IDEA 的 .idea、target 目录),压缩源码时保证干净。
特别是第一条。很多同学源码里数据库脚本是有的,但一执行就各种报错,或者脚本里没有 insert 测试数据,登录后所有列表都是空的,演示效果就会大打折扣。我给你的源码里已经准备了一部分商品和供应商测试数据,你应该再根据自己的需求扩充一些,比如多建几个分类、多录几个供应商。
5.3 从毕设项目延伸到面试项目的空间
如果你不是只想应付毕设,还想把这套内容放到简历上,那么我建议你在现有基础上做三个小升级,成本低但简历含金量高。
第一,把 SSM 升级为 Spring Boot + MyBatis 版本,核心业务代码可以复用,只改配置层。Spring Boot 的自动配置能让你省掉大量 XML 配置,简历上写“熟悉 Spring Boot 项目开发”更有说服力。
第二,给登录模块引入 JWT 或者 Spring Security,能体现你对认证授权的理解。
第三,把报表模块的统计逻辑改为定时任务,比如每天凌晨自动汇总昨天的销售数据到一张统计表里,用 Spring 的@Scheduled实现,这可以让项目更有“工程感”。
当然,如果你现在时间紧张,不建议大改,先保证核心流程能跑通。毕设的核心是“完整”,不是“复杂”,你要在论文里写清楚模块设计、数据库设计、关键代码分析,再把项目演示顺畅,就是一份能拿到不错成绩的成品。
6. 写在最后:一点过来人的建议
做完整个超市进销存系统,我有一个很深的体会:毕业设计其实是一场“项目管理”训练,而不是单纯考验编程能力。你需要在不同模块之间取舍,在有限时间内确定优先级,还要让最后的交付物(源码、论文、演示)都能稳定运行。很多同学喜欢在前期纠结用什么前端框架、要不要加 Redis 缓存,结果连基础的进销存流程都没写完。我的建议永远是:先跑通核心链路,再考虑锦上添花。
在做这个项目的过程中,我自己也反复踩了不少坑,比如 MyBatis 的驼峰映射配置没开,导致大量字段映射失败;比如采购单和明细表的事务忘了加rollbackFor,出错了也不会自动回滚;再比如没有提前准备测试数据,演示时页面空空如也。这些坑都不算难,但非常典型,这篇文章里我尽量都讲到了。你如果照着做,即使遇到问题,也能顺着排查思路找到原因。
最后再送一个小技巧:如果你用了 JSP 做页面,建议在web.xml里把项目默认首页指向登录页,或者做一个index.jsp做重定向。这样部署后访问根路径就能跳转到登录,体验会好很多。祝你的毕设顺利过关。