news 2026/9/1 10:10:52

Java超市商品管理系统实战:从环境配置到核心功能开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java超市商品管理系统实战:从环境配置到核心功能开发

简介:本资源是一个面向Java初学者与课程设计实践者的超市商品管理系统,基于JavaFX实现图形化界面,采用MVC架构组织代码,覆盖面向对象编程、GUI开发、文件持久化等核心教学知识点,适用于高校Java程序设计、软件工程实训或毕业设计参考。压缩包共46个文件,含8个Java源码文件(封装商品、库存等业务逻辑)、6个FXML界面文件(定义布局结构)、13个编译后class文件及14张PNG资源图(包括界面样式截图与商品图标),辅以Eclipse项目配置文件和build.fxbuild构建脚本,整体大小847KB,结构规范,开箱即用。已有1407人学习下载,提供完整可运行项目工程,包含清晰的src源码目录、Resources资源管理路径及bin输出结构,便于理解MVC分层思想、JavaFX事件驱动机制与文本/JSON类文件存储方案,是掌握Java桌面应用开发全流程的优质实践案例。 很多Java初学者在学完语法和面向对象之后,都会面临同一个问题:理论都会,代码也看得懂,但一到自己动手写项目就不知道从哪里开始。超市商品管理系统就是这类场景里最经典的练手题目之一,它不像电商系统那么复杂,却又把增删改查、权限控制、库存逻辑、报表统计这些日常开发离不开的基础能力全部覆盖了。这篇文章就结合我自己的开发经验,从项目搭建到核心代码实现,再到容易踩的坑,完整拆解一个基于Java的超市商品管理系统到底应该怎么做。

这个项目适合谁?刚学完Java基础、准备做课程设计的学生,想积累项目经验找工作的转行者,以及想快速上手一个完整业务闭环的开发者。超市商品管理系统能解决的问题也很清晰:门店里商品信息混乱、库存盘点靠手工、收银结算容易出错、进货出货没有记录。用系统把商品、库存、收银、供应商串起来,整个店铺的日常运营就能在软件里跑通。这篇东西不打算讲多高深的理论,就是把我实际写代码、调BUG、改需求的过程放大给你看。

1. 项目设计与技术选型思路

1.1 为什么超市管理系统是练手神作

超市商品管理系统在Java课程设计和面试项目里出现频率极高,不是没有原因的。它代表了一类非常典型的业务系统:围绕核心实体(商品)展开,延伸出多个关联功能模块。你在里面能看到单表CRUD、多表联查、逻辑删除、库存扣减、权限判断、数据统计,这些全是实际工作中每天都要碰的东西。

更关键的是,这个项目的业务规则非常明确,不需要你凭空想需求。比如商品库存不能为负数、收银结账要同时扣库存和生成账单、进货要更新库存并记录进货单,这些规则稍微想一想就能懂,但实现起来却又涉及事务、并发、数据一致性这些进阶话题。做完一个系统,你对Java Web开发的整体认知会有一个跳跃式的提升。

我当年做这个项目的时候,最大的感受是:写第一个版本只需要两个星期,但不断优化、重构、加功能,能让你整整琢磨两个月。这也是为什么我强烈建议不要满足于“能跑”,而是要把系统的每一步逻辑都弄明白。

1.2 技术选型:从控制台到Web的进阶路线

很多教程上来就给Swing界面方案,也有直接上Spring Boot + Vue的,但我觉得技术选型应该分阶段来看。

第一阶段,刚学完Java SE,还没接触Web框架。这时候建议用纯Java + MySQL,界面用控制台或者Swing。技术栈就是JDBC + 原生SQL + 面向对象设计,重点是把Java基础打牢。把Connection、PreparedStatement、ResultSet这套JDBC原生API用明白了,后面学MyBatis、Hibernate这些框架会轻松得多,因为你已经知道框架底层在解决什么问题了。

第二阶段,已经学过Java Web,那就直接上Spring Boot。Spring Boot + MyBatis + MySQL + Thymeleaf(前端模板)或者Vue分离,这匹配大家做毕设或者找工作项目中比较通用的配置。Spring Boot自带Tomcat,引入依赖就能跑,不用像以前SSH那样搞一堆XML配置,对新手友好很多。

第三阶段,如果只是想快速验证业务逻辑,甚至可以用H2内存数据库 + JDBC,这样连MySQL都不用装,代码跑起来更容易。我在开发过程中就经常用这种快速原型的方式验证业务逻辑。

我个人的建议是:如果你是为了学习,第一遍用纯JDBC写核心逻辑,然后迁移到Spring Boot重写一遍。这个迁移过程能让你真正理解框架存在的意义——不是炫技,而是帮你把那些重复的样板代码省掉。

1.3 项目结构规划:没有规划的项目一定会烂

动手写代码之前,先把包结构规划清楚,这个习惯能省掉后期大量重构的时间。

比如一个基于Maven的Spring Boot项目,可以这样规划:

com.supermarket ├── controller // 控制器层,接收请求,返回响应 ├── service // 业务逻辑层,处理核心业务规则 ├── dao/mapper // 数据访问层,操作数据库 ├── entity/model // 实体类,对应数据库表 ├── dto // 数据传输对象,用于接口参数接收 ├── vo // 视图对象,用于页面展示 ├── config // 配置类 ├── common // 公共类,统一返回结果、异常等 ├── util // 工具类 └── resources ├── mapper // MyBatis映射文件 ├── static // 静态资源 └── templates // 模板页面

很多新手喜欢把所有代码塞到两三个类里,一个类几百上千行,确实跑得起来,但后面稍微改点需求就要全局搜索,非常痛苦。分层是Java开发最基本的代码组织方式,它的好处是:每一层的职责单一,controller只做参数接收和响应封装,service只写业务逻辑,dao只做数据库交互。出了问题,直接定位到对应层,排查速度快得多。

我自己一开始也是把所有东西堆在一起,后来加一个“会员折扣”功能差点把整个系统改崩,才老老实实按三层架构重构。这不是说分层能给你带来什么神奇的能力,而是它能让你的代码在复杂度上升之后仍然可维护。

2. 核心功能模块设计与实现要点

2.1 基础环境准备:从JDK安装到数据库连接

这里先说说最基础的环境配置问题。热词里出现了“java环境变量配置”,其实很多人在第一步就卡住了。JDK安装完之后,需要配置JAVA_HOME、PATH、CLASSPATH三个环境变量。JAVA_HOME指向JDK安装目录,比如C:\Program Files\Java\jdk-17,PATH里加上%JAVA_HOME%\bin,这样在命令行里就能直接运行javac和java命令了。

CLASSPATH在新版JDK里其实已经不需要手动配置了,JDK 9之后默认就是当前目录加JDK内部的模块路径,但很多老教程还在让你配。你只要记住:环境变量的作用是让操作系统能“找到”Java编译器和运行时,配置成功与否直接在命令行输入java -version来验证即可。如果报“不是内部或外部命令”,多半是PATH没配好;如果配好了还是不行,检查一下是不是配置后忘了重新打开命令行窗口。

数据库部分,我推荐MySQL 8.x,安装时注意选UTF-8字符集。JDBC连接串这样写:

jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的密码

那个serverTimezone=Asia/Shanghai不能省,不然MySQL 8连接时会报时区错误。useSSL=false是为了避免本地开发时SSL握手警告。这些参数有些教程没有,但对新手来说能省很多麻烦。

2.2 登录与权限控制:不是简单的用户名密码比对

超市管理系统一般有两类角色:管理员和收银员。管理员能管理商品、查看报表、管理账号;收银员只能使用收银功能、查看商品信息和简单库存。这是典型的基于角色的权限控制(RBAC,Role-Based Access Control)。

实现方案有两种。最简单的做法是用户表里加一个role字段,登录之后把用户名和角色存到Session里,然后通过拦截器判断用户是否有权限访问某个URL。更规范的做法是建立用户表、角色表、菜单表、用户角色关联表、角色菜单关联表,做细粒度的权限控制。

我用第一版的时候就是user表加role字段,后端写一个自定义拦截器,在preHandle里检查session和请求路径的权限关系:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { response.sendRedirect("/login.html"); return false; } // 简单角色校验 String uri = request.getRequestURI(); if (uri.startsWith("/admin/") && !"ADMIN".equals(user.getRole())) { response.sendError(403); return false; } return true; } }

有一个问题我用了一段时间才发现:直接在session里存明文用户名和角色是很不安全的,别人拿到session就能伪造身份。实际项目中至少要把密码加密存储,可以在登录时用MD5加盐的方式处理用户密码,或者使用更强的SHA-256或BCrypt。对于课程设计来说,MD5加盐已经足够了。更重要的是,密码明文比对毫无防御力,数据库一旦泄露,所有账号都裸奔。写代码的时候一定别偷懒。

2.3 商品管理:CRUD背后的细节比你想的多

商品管理是系统的核心,功能无非是商品的增删改查。但这里面的细节挺多。

商品字段通常包括:商品编号、名称、条码、分类、进价、售价、库存量、预警库存、供应商、上架状态、创建时间、更新时间。条码是个很重要的字段,超市实体场景里,收银台扫的就是条码,所以系统里条码必须做唯一约束。

商品查询建议做分页+多条件组合查询。按照商品名称模糊查询、分类查询、库存状态查询(比如库存低于预警值),用一个动态SQL实现。这正好是MyBatis动态SQL的经典应用场景,新手可以用<where>标签和<if>标签来玩一玩:

<select id="queryProductList" resultType="com.supermarket.entity.Product"> SELECT * FROM product <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="stockStatus == 1"> AND stock &lt;= warn_stock </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>

“删除商品”这里有一个重要设计:不是直接DELETE FROM product WHERE id = ?,而是逻辑删除,加一个is_deleted字段,查询的时候默认过滤掉已删除的数据。道理很简单,商品一旦被删除,历史订单、进货记录里引用的商品就查不到了,整个系统的数据完整性就破坏了。用逻辑删除,既能隐藏商品,又能保留历史痕迹。这一点面试官也很喜欢问。

2.4 库存管理:进销存的核心就是库存变动记录

库存管理是这个项目的灵魂所在。进货增加库存,销售扣减库存,盘点修正库存。如果每次操作都直接update库存字段,系统跑一段时间就会出错,因为谁改过、什么时候改的、为什么改,完全没有记录。一旦库存对不上账,你根本无从查起。

所以要做库存流水表(stock_log),每次库存变动都插入一条记录,包含商品ID、变动类型(进货、销售、退货、盘点)、变动数量(正数增加,负数减少)、操作前库存、操作后库存、操作人、操作时间。

核心逻辑可以这样写,用一个事务保护起来:

@Transactional public void stockIn(StockLog stockLog) { // 1. 查询当前库存 Product product = productMapper.selectById(stockLog.getProductId()); // 2. 记录变动前库存 stockLog.setBeforeStock(product.getStock()); // 3. 计算变动后库存 int afterStock = product.getStock() + stockLog.getChangeNum(); if (afterStock < 0) { throw new RuntimeException("库存不足,无法操作"); } stockLog.setAfterStock(afterStock); // 4. 更新商品库存 productMapper.updateStock(stockLog.getProductId(), afterStock); // 5. 插入流水记录 stockLogMapper.insert(stockLog); }

@Transactional为什么能保证所有操作要么全部成功、要么全部失败回滚?因为Spring事务管理默认绑定了数据库连接,事务提交之前,所有SQL操作都在同一个事务里,任何一个环节抛异常,整个事务回滚,数据库不会出现“库存扣了但流水没写”这种脏数据。我不止一次看到有人去掉事务,结果测试的时候一报错,库存对不上账,一堆数据到处乱跳,相当折磨。

库存预警也是一个重要功能。比如商品设置一个预警库存值,当库存量低于预警值时,系统在首页或者商品列表里高亮提示。实现起来不复杂,就是查询stock <= warn_stock的商品,在管理端做一个醒目的提示面板。

2.5 收银模块:事务与并发是这里的重头戏

收银是超市场景的落地动作,收银员把商品加入购物车,结算时系统生成订单、扣减库存、记录交易流水。这个功能的数据库操作包含:插入订单主表、插入订单明细表、更新商品库存、插入库存流水。

这个过程必须是一个事务,否则会出现订单生成了但库存没扣,或者库存扣了但没有订单记录。之前提到的@Transactional在这里就是核心武器:

@Transactional public Order checkout(OrderVO orderVO) { // 生成订单主记录 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setTotalAmount(orderVO.getTotalAmount()); order.setCreateTime(new Date()); orderMapper.insert(order); // 保存订单明细 for (OrderItem item : orderVO.getItems()) { item.setOrderId(order.getId()); orderItemMapper.insert(item); // 扣减库存 productMapper.decreaseStock(item.getProductId(), item.getQuantity()); // 写库存流水 stockLogMapper.insert(new StockLog(..., -item.getQuantity(), ...)); } return order; }

这里还有一个隐藏的坑,就是内存不足的问题。热词里出现了“java: outofmemoryerror: insufficient memory”,这其实在开发调试阶段容易碰到。如果服务器堆内存设置太小,而订单数据量又很大,系统在处理大批量数据时会报OOM。解决方法是给JVM合理分配内存,启动参数可以用-Xms256m -Xmx1024m来设置初始堆内存和最大堆内存。另外,大量一次性加载到内存的集合数据也会造成OOM,查询大量数据时应该分页加载。收银模块这个场景还好,但统计报表模块就要特别小心,后面会讲到。

并发环境下扣库存容易出问题。举个例子,两个收银员同时卖同一件商品,库存只剩1件,两个人同时结账,如果不做控制,数据库最终库存可能变成-1。解决办法是扣库存时使用原子性SQL:

UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}

用这条SQL把“检查库存是否充足”和“扣减库存”合并成一个原子操作,数据库行锁能保证同一时刻只有一个事务能修改这条记录的库存值。如果更新的影响行数为0,说明库存不足,直接抛异常提示。这是我在实际项目中用到的比较成熟的方案。

2.6 供应商与进货管理:串起商品来源数据

单独管理商品但不管供应商,就会遇到一个问题:进货的时候你需要知道这批货从哪来的,进价是多少,什么时候进的货,质量出了问题找谁。所以供应商表也是系统的重要组成。

供应商表字段包括:供应商编号、名称、联系人、联系电话、地址、备注。进货单表(purchase_order)记录一次完整的进货行为:进货单号、供应商ID、操作人、进货日期、总金额。进货单明细表(purchase_order_item)记录每项商品进货多少个、进价多少。

进货入库时,系统流程是:填写进货单明细 -> 提交进货单 -> 更新商品库存 -> 记录库存流水。这个流程同样需要事务保证一致性。如果你在做这个项目时把这个流程实现完整,面试时就能清楚说出一个多表事务+联动的具体案例,比单纯说“我会增删改查”要强得多。

管理界面方面,可以做成进货单列表+详情页的结构。列表页显示进货单号、供应商名称、总金额、进货日期;点击详情能看到每项商品的进货明细。这就是典型的一对多查询场景,MyBatis里可以用collection标签做嵌套结果映射,也可以用两个查询拼装。

2.7 报表统计:SQL聚合函数是数据分析的起点

超市管理系统如果能统计销售额、利润、库存占用资金,这个系统就不用只是简单记流水账了。报表是展示你对SQL掌握程度的绝佳场景。热词里有“java基础”,这里正好检验SQL基础能力。

销售额统计有两种口径:按日汇总和按商品汇总。按日汇总的SQL可以这样写:

SELECT DATE(create_time) AS day, SUM(total_amount) AS amount FROM orders WHERE create_time >= #{startDate} AND create_time < #{endDate} GROUP BY DATE(create_time) ORDER BY day

按商品分类统计可以用多表联查:

SELECT c.name AS category_name, COUNT(DISTINCT o.id) AS order_count, SUM(oi.quantity) AS total_quantity, SUM(oi.quantity * oi.price) AS total_sales FROM order_item oi JOIN orders o ON o.id = oi.order_id JOIN product p ON p.id = oi.product_id JOIN category c ON c.id = p.category_id WHERE o.create_time >= #{startDate} AND o.create_time < #{endDate} GROUP BY p.category_id

利润统计则要算每个商品的售价减进价,再乘以销量。实现不同统计报表的过程,其实就是你对SQL的GROUP BY、聚合函数、日期函数、多表JOIN越来越熟练的过程。

报表查询这里我特别提醒一下分页和内存控制问题。如果超市规模较大,一天几万条订单,你把所有订单一次性查出来放在内存里再计算,JVM很快就给你报OOM。这时候应该在SQL层面做聚合计算,数据库算完只返回汇总结果,内存里只有一小撮统计数字,再多的历史数据也不会撑爆内存。

3. 数据库设计:从建表到索引优化

3.1 核心数据表结构设计

表的结构设计决定了系统能跑多远。这里给出一个标准规范化方案的参考建表脚本。

用户表:

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(64) NOT NULL, `real_name` VARCHAR(50) DEFAULT NULL, `role` VARCHAR(20) NOT NULL DEFAULT 'CASHIER', `status` TINYINT NOT NULL DEFAULT 1, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

商品表:

CREATE TABLE `product` ( `id` INT NOT NULL AUTO_INCREMENT, `product_no` VARCHAR(50) NOT NULL UNIQUE, `barcode` VARCHAR(50) NOT NULL UNIQUE, `name` VARCHAR(100) NOT NULL, `category_id` INT NOT NULL, `purchase_price` DECIMAL(10,2) NOT NULL, `sale_price` DECIMAL(10,2) NOT NULL, `stock` INT NOT NULL DEFAULT 0, `warn_stock` INT NOT NULL DEFAULT 5, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `is_deleted` TINYINT NOT NULL DEFAULT 0, `supplier_id` INT DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表:

CREATE TABLE `orders` ( `id` INT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL UNIQUE, `total_amount` DECIMAL(10,2) NOT NULL, `pay_method` VARCHAR(20) DEFAULT NULL, `user_id` INT NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单明细表:

CREATE TABLE `order_item` ( `id` INT NOT NULL AUTO_INCREMENT, `order_id` INT NOT NULL, `product_id` INT NOT NULL, `product_name` VARCHAR(100) NOT NULL, `price` DECIMAL(10,2) NOT NULL COMMENT '成交单价', `quantity` INT NOT NULL, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

库存流水表:

CREATE TABLE `stock_log` ( `id` INT NOT NULL AUTO_INCREMENT, `product_id` INT NOT NULL, `change_type` TINYINT NOT NULL COMMENT '1进货 2销售 3退货 4盘点', `change_num` INT NOT NULL COMMENT '正数入库 负数出库', `before_stock` INT NOT NULL, `after_stock` INT NOT NULL, `order_id` INT DEFAULT NULL, `user_id` INT DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计表结构时,注意使用DECIMAL(10,2)存金额,不建议用double/float。因为浮点数用二进制存储,在计算0.1+0.2这类金额时会出现精度丢失,一元钱的账差个0.0000001,钱少无所谓,但报表里出现这种精度问题,对账是真的会崩溃。DECIMAL是定点数,精度可控,适合金额计算。这个细节我在开发初期没在意,后来做统计报表发现总金额差几分钱,排查了半个下午才定位到是浮点精度问题。

3.2 索引设计的基本逻辑

索引不是越多越好,索引建的多了,写入速度会变慢,占用磁盘空间也大。一般情况下,给主键、外键、唯一约束字段、查询频繁的字段加索引就够了。

比如商品表的条码和商品编号用唯一索引;分类字段用普通索引,因为分类查询很频繁;订单表的时间字段应该加索引,因为报表统计经常按时间范围查询;订单明细表的order_id加索引,因为查询一个订单的所有明细很频繁。

快速判断字段是否需要索引的方法:这个字段会不会出现在WHERE子句里?会不会经常作为JOIN的关联字段?会不会作为排序字段?三个条件占一个以上,就可以考虑加索引。但要记住,查询优化器和执行计划只看SQL执行代价,索引不是万能的,表数据量小(几千行)的时候索引优势不明显,数据量大了之后差距才会显现。

3.3 表之间的关系与外键

订单主表和明细表是一对多关系,用order_id关联;商品表和分类表是多对一关系,用category_id关联;进货单表和明细表是一对多关系;商品表和供应商表是多对一关系。

外键约束能保证数据完整性,比如你不能往明细表里插入一个不存在的order_id。但很多企业开发中会禁用外键,原因是外键会让插入、删除操作多一次约束检查,影响写入性能,且分库分表后外键无法生效,用应用层逻辑保证关联关系更灵活。对于课程设计或练手项目,建议保留外键设计,因为这是一个良好的设计思路,而且能直接体现对数据库规范的理解;但实际开发时要注意外键在批量导入数据时带来的性能影响,可以先插入主表,再插入从属表,最后统一开启外键检查。

4. 核心代码实现与逻辑拆解

4.1 三层架构中的代码案例

不管用什么框架,核心逻辑都大同小异。Controller层的职责是把HTTP请求解析成参数,传给Service层,再把Service层返回的数据封装成JSON返回给前端。

以一个商品列表查询为例,Controller层这样写:

@RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize, String name, Integer categoryId) { PageResult<ProductVO> pageResult = productService.queryPage(page, pageSize, name, categoryId); return Result.success(pageResult); } }

Service层是业务逻辑体现的位置,比如校验商品名称不能重复、库存操作时要检查库存量是否充足、进货时要同时更新商品和写流水:

@Service public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Override public PageResult<ProductVO> queryPage(Integer page, Integer pageSize, String name, Integer categoryId) { int offset = (page - 1) * pageSize; List<ProductVO> list = productMapper.queryPage(offset, pageSize, name, categoryId); Long total = productMapper.countPage(name, categoryId); return new PageResult<>(total, list); } }

因为查询了两次,一次查列表,一次查总数,这里有一个潜藏的问题:如果这两次查询之间数据被修改,分页总数会与列表总数错位。实际业务里出现这种情况的概率极低,完全可以接受。但如果是对数据一致性要求极高的场景,可以在事务里执行这两个查询,或者直接用一个查询同时返回总数和列表,后面这种做法一般会相对复杂,不太适合新手。

4.2 Java面向对象思想如何落地

做这个项目时,热词里的“面向对象编程java”正好派上用场。有些人觉得面向对象就是个概念,和实操无关,其实不然。

比如商品和订单明细这两个实体,如果直接用Map到处传参,代码会非常难维护。更合理的做法是定义实体类,用属性封装数据,用方法封装行为。比如可以把“加减库存”的逻辑放在Product实体里:

public class Product { private Integer id; private String name; private BigDecimal purchasePrice; private BigDecimal salePrice; private Integer stock; private Integer warnStock; // 封装库存变更逻辑 public void increaseStock(int num) { this.stock += num; } public void decreaseStock(int num) { if (this.stock < num) { throw new RuntimeException("库存不足"); } this.stock -= num; } }

Service层拿到Product对象后,直接调用对象的decreaseStock方法,再交给Mapper执行update,这样业务规则就不散落在各个Service方法里,数据和行为绑定在一起,逻辑更清晰,测试也更方便。

面向对象还体现在复用上。比如写一个BaseEntity,把id、createTime、updateTime这些公共字段抽出来,所有实体类继承它。又比如写一个统一返回结果类Result,所有接口返回相同结构,前端处理的时候就不用判断各种奇怪的返回格式。这些都是Java开发中面向对象思维的落地表现。

4.3 事务管理和异常处理的常见问题

Spring事务里最坑的坑是“事务不生效”。最常见的场景是:类内部方法互相调用,比如Controller调Service的方法A,事务在A上;A内部调用同类中的方法B,B上标了@Transactional,但这个调用不会经过Spring的代理,B的事务不会生效。

解决办法是不要在同类的内部调用带事务的方法,把需要事务的方法拆到不同的Service类中,或者用注入自身代理的方式调用。这个坑在写收银模块时最容易踩,因为收银逻辑往往就在一个Service里,下单、扣库存、写流水全都堆在一起,很容易出现“同一个类里方法调方法”的情况。

异常处理要特别留意RuntimeException。Spring默认只有RuntimeException和Error才触发回滚,受检异常(比如IOException)默认不会回滚。如果你在事务方法里抛出一个受检异常期望回滚,事务不会动,数据照旧提交,非常容易让人懵。比较稳妥的做法是在方法上显式指定回滚规则:

@Transactional(rollbackFor = Exception.class)

这个知识点很经典,面试也常考。我见过太多人写了@Transactional却没指定回滚类型,测试时发现数据不会回滚,各种困惑,其实根因就在这一行。

5. 常见问题与坑位排查记录

5.1 JDBC连接失败的常规排查顺序

连接数据库报错,通常是下面几个原因。

第一个是MySQL服务没启动,Windows下在服务管理里查看MySQL服务状态,Linux下用systemctl status mysql查看。

第二个是驱动类没加载。JDBC 4.0之后可以通过SPI机制自动加载驱动,但如果你用老版本驱动或者没把mysql-connector-java的依赖加到pom.xml里,就会报ClassNotFoundException。

第三个是密码或用户名不对。MySQL 8默认认证插件是caching_sha2_password,如果客户端驱动版本太老,连接时会报认证失败。解决方案是换新版驱动,或在创建用户时用mysql_native_password。推荐前者。

第四个是端口号不对。MySQL默认3306,如果你改了端口,连接串必须同步修改。可以在MySQL命令行里用show variables like 'port';查一下。

第五个是防火墙拦截。这个在我本机几乎没遇到过,但部署到云服务器或者同事电脑上时就可能出现,检查一下防火墙是否放行3306端口就行。

5.2 字符集乱码问题排查

网页显示中文乱码,多半是数据链路里字符集不统一。按这个顺序排查:数据库连接串有没有加characterEncoding=utf8;MySQL建表时是否指定了utf8mb4;后端代码里是否设置了请求和响应的编码;前端页面有没有声明<meta charset="UTF-8">

排查思路是把这条链路断开来看:先把固定的数据从前端到后端打印一遍,再用数据库客户端直接查询数据,一步步缩小范围。有一次我遇到的问题是数据库里的数据显示正常,但是网页打开乱码,查了半天发现是Tomcat默认编码不是UTF-8,后来在配置里指定:

server.servlet.encoding.charset=UTF-8 server.servlet.encoding.enabled=true server.servlet.encoding.force=true

这个问题在Spring Boot中基本不会遇到,因为默认就设置好了。但在原生Servlet时代是重灾区。

5.3 报“OutOfMemoryError”该怎么处理

开发阶段遇到OOM,通常是因为启动时的堆内存太小。可以把JVM的内存参数调大一点,比如-Xms256m -Xmx1024m。如果是部署到服务器上,还要考虑服务器的物理内存大小,别一股脑把堆内存设置成8G,机器只有4G内存,这样会直接卡死操作系统。

运行阶段遇到OOM,就要重点检查是不是有大量数据被一次性加载到内存了。最常见的场景是报表查询没有分页,把几百万条订单全查出来了,内存直接爆掉。解决办法是分页查询,或者在SQL层做聚合,只查汇总结果。另一个常见场景是循环里反复创建大对象,导致GC来不及回收,内存碎片越来越多,最后OOM。这时候就要优化代码逻辑,避免循环内创建无用大对象。

如果真的发生了OOM,一定要提前做好异常处理。比如给用户一个友好提示,而不是Java默认的大红堆栈页面。可以配置全局异常处理器,捕获OutOfMemoryError并返回一个提示页面。

5.4 商品库存变成负数,如何排查

库存变成负数,大概率是并发扣减没有做限制。如果SQL写的是:

UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId}

即使检查过stock >= quantity,在并发场景下还是可能出现超卖,因为两个请求同时读到库存为1,都通过了检查,然后都执行update,库存就变-1了。解决方法是把数量充足的条件写到UPDATE语句里:

UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}

这样数据库行锁天然帮你解决了并发问题,如果影响行数为0,就说明库存不足,直接抛出业务异常。这个方法逻辑简洁且效果可靠,比Java层的synchronized锁更合适,因为synchronized只在你一个应用实例内有效,多个应用实例(集群部署)时会失效,而数据库行锁是全局生效的。

5.5 快查表:错误现象-原因-解决方案

错误现象大概率原因解决方案
java.lang.ClassNotFoundException: com.mysql.jdbc.Driver缺少MySQL驱动依赖在pom.xml中添加mysql-connector-j,版本对应MySQL版本
Access denied for user用户名或密码错误核对数据库账号密码,用命令行验证
Unknown database数据库不存在创建数据库:CREATE DATABASE supermarket DEFAULT CHARSET utf8mb4
Communications link failureMySQL服务未启动或端口被防火墙拦截检查服务状态、端口监听,关闭或放行防火墙
Cannot create PoolableConnectionFactory数据库连接池初始化失败,连接参数有问题检查连接串、驱动版本、认证方式
java.sql.SQLException: No suitable driver found驱动没注册或连接串格式错误检查连接串是否以jdbc:mysql://开头
OutOfMemoryError: Java heap space堆内存太小或一次性加载过多数据调整-Xms/-Xmx,分页查询,SQL层聚合
中文乱码字符集不统一连接串加characterEncoding,建库建表用utf8mb4,代码指定UTF-8编码
事务不生效同类内部调用或异常类型不对拆分Service,或在方法上声明rollbackFor = Exception.class
删除商品后历史订单查不到商品物理删除导致关联数据丢失改用逻辑删除,is_deleted字段标记,查询时过滤

6. 系统扩展方向:从课程设计到工程化项目

如果这个项目要往工程化方向走,有几个明确的扩展点。

第一个是引入商品分类管理。商品分类表和商品表做关联,前台按分类筛选商品,后台管理分类的增删改查。

第二个是会员管理和会员价、积分功能。User表扩展出member表,再增加积分规则和会员折扣计算。这个功能和订单模块关联,能练习到不少规则引擎式编程思维。

第三个是数据导出。统计报表结果导出成Excel,使用Apache POI或者EasyExcel。开发中你会用到POI的Workbook、Sheet、Row、Cell这些API,流程不复杂,但要注意大数据量导出时的内存控制问题。

第四个是引入Redis做缓存。把商品列表、库存热点数据放到Redis里,减少数据库压力。这套可以做得很简单,也可以做得很复杂,想深挖的话可以有缓存穿透、缓存击穿、缓存雪崩这些扩展话题。

第五个是使用数据库连接池。原生JDBC中每次操作都手动获取Connection和关闭Connection,性能低且代码冗余。引入HikariCP或Druid,通过配置文件统一管理连接生命周期。Druid还自带监控面板,可以看到SQL执行次数、慢查询、连接池状态,对排查问题非常有帮助。

还有一些细节可以打磨,比如:订单号生成用时间戳+随机数,而不是数据库自增ID;删除操作做二次确认;登录接口做失败次数限制;统一记录操作日志。这些不是必须做,但做了之后,项目会比80%的练手项目完整得多。

7. 写在最后:实操中的几点体会

几次做下来,我感觉最值得强调的就是:这个项目难度不大,但五脏俱全,你把它的每个模块都吃透,Java基础、数据库设计、事务机制、面向对象这些东西就都串起来了。

我个人的体会是,开发过程中一定要重视基础环境的稳定性。很多人写了几天代码,一大半时间花在配置环境上——不是JDK环境变量配不好,就是数据库连接不上。扎实掌握“java环境变量配置”这些基础操作,把IDEA开发环境、Maven依赖管理、MySQL客户端都使用熟练,后面写业务代码才会专注在写逻辑本身。

另外,即使不是做毕设,我也建议上手写一遍这个系统。手写一遍CRUD,你会发现增删改查没那么可怕;手写一遍收银事务,你会发现数据一致性没有想象中那么简单;手写一遍报表SQL,你会发现聚合查询原来这么有用。把超市商品管理系统从头到尾亲手做一遍,远比你盯着教程看十遍收获大得多。踩了坑,解决了,这才是真正属于你的项目经验。

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

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

GLM-5 SWE-bench Pro 62.1分深度解读:开源SWE修复能力真相

GLM-5 SWE-bench Pro 62.1分深度解读&#xff1a;开源SWE修复能力真相 【免费下载链接】GLM-5 GLM-5: From Vibe Coding to Agentic Engineering 项目地址: https://gitcode.com/GitHub_Trending/gl/GLM-5 GLM-5 系列旗舰模型 GLM-5.2 在 SWE-bench Pro 上取得 62.1 分&…

作者头像 李华
网站建设 2026/9/1 10:09:46

10 分钟搭好团队知识库:Outline 部署与协作实战

10 分钟搭好团队知识库&#xff1a;Outline 部署与协作实战 【免费下载链接】outline The fastest knowledge base for growing teams. Beautiful, realtime collaborative, feature packed, and markdown compatible. 项目地址: https://gitcode.com/GitHub_Trending/ou/out…

作者头像 李华
网站建设 2026/9/1 10:08:35

宇树机器人二次开发实战:从SDK环境搭建到运动控制与调试

在实际机器人技术领域&#xff0c;宇树科技&#xff08;Unitree&#xff09;是一个绕不开的名字。从早期惊艳四座的仿生四足机器人&#xff0c;到如今面向消费级市场的通用人形机器人&#xff0c;宇树的产品迭代速度和市场声量都令人瞩目。其产品线覆盖了从科研教育、工业巡检到…

作者头像 李华
网站建设 2026/9/1 10:07:35

yuzu模拟器免费教程:5分钟在电脑和手机上跑起来Switch游戏

yuzu模拟器免费教程&#xff1a;5分钟在电脑和手机上跑起来Switch游戏 【免费下载链接】yuzu 任天堂 Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/yu/yuzu yuzu模拟器是一款完全开源免费的任天堂 Switch 模拟器&#xff0c;它让《塞尔达传说&#xff…

作者头像 李华
网站建设 2026/9/1 10:07:07

神马影视8.8源码部署实战:从解压到上线的完整流程

简介&#xff1a;一套面向影视站点开发与运维人员的视频播放网站后台管理系统源码&#xff0c;基于maccms内核&#xff0c;8.8优化版在性能、稳定性与功能细节上做了调整&#xff0c;适合用于搭建私人影视库、学习CMS二次开发或部署测试。资源压缩包共1个文件&#xff0c;类型为…

作者头像 李华