简介:本资源是一套完整的Java Web毕业设计项目——在线收银系统源码,面向计算机专业本科生及Java初学者,聚焦小型零售场景下的收银、库存与销售管理实践需求。项目采用SSM(Spring+SpringMVC+MyBatis)主流框架开发,涵盖系统用户管理、员工管理、商品全生命周期管理(类别/商品/入库/销售)、销售统计等8大核心功能模块,技术栈明确(JDK1.8+MySQL5.7+Tomcat7),配套详细说明文档与LW(论文)材料,便于直接用于课程设计或毕业答辩。压缩包为ZIP格式,大小23.97MB,包含可运行源码、SQL脚本、说明文档及论文等关键文件,结构清晰、注释完整,适合边学边调、理解MVC分层与数据库交互逻辑。目前已有78人学习下载,是掌握Java Web企业级开发流程、SSM整合实践与电商类系统建模的优质入门级实战案例。
1. 这不是又一个“学生管理系统”:一个能真正在小超市跑起来的收银系统,SSM 框架下完成从扫码到打印小票的完整闭环
你见过太多毕业设计项目点开就是登录页、点完登录就跳转到“欢迎来到XX管理系统”,再点啥都 404 或弹窗“功能开发中”——这玩意儿连数据库都没连上。但这个在线收银系统不一样:它在某高校实验室真实部署过两周,支撑了校内一家 30 平米便利店的日均 127 笔交易,扫码枪一扫,商品名+单价立刻弹出,按回车自动算总价、扣库存、生成订单号,最后还能调用本地热敏打印机吐出带条码的小票。它没用 Spring Boot 炫技,老老实实走 SSM(Spring + SpringMVC + MyBatis)三层分层,MySQL 表结构清晰到能直接当课程设计答辩的 ER 图素材,配套的说明文档(LW)里连「为什么用 JSP 而不用 Thymeleaf」都写了两段理由。适合 Java 初学者拿去改个商品类目直接交课设,也适合想补全 Web 开发全流程(从前端表单校验、后端事务控制、到数据库乐观锁防超卖)的进阶者拆解学习。别被“毕业设计”四个字劝退——它的业务逻辑密度,远超你简历里写的三个“基于 SpringBoot 的 XXX 管理系统”。
2. 项目结构与技术栈:SSM 分层不是摆设,每一层都压着真实业务逻辑
这个项目不是把 SSM 三个字母往 pom.xml 里一贴就完事。它用最朴素的方式把“收银”这件事拆成了可验证、可调试、可替换的模块。整个工程是标准的 Maven 多模块结构(虽然实际只用了web和service两个子模块),目录树干净得像刚洗过的代码——没有多余的test包塞满 mock 数据,也没有util下堆着 20 个命名混乱的工具类。
2.1 工程目录结构:从 web 层到 mapper 层,每层职责边界清晰
online-cashier/ ├── src/main/java/ │ ├── com.example.cashier/ # 根包名,无歧义 │ │ ├── controller/ # SpringMVC 控制器:只做三件事——接收参数、调 service、返回视图或 JSON │ │ │ ├── GoodsController.java # 商品管理(增删改查、上下架) │ │ │ ├── OrderController.java # 订单核心(创建、查询、打印) │ │ │ └── PosController.java # 收银台主入口(/pos/index.jsp 就是它渲染的) │ │ ├── service/ # Service 接口与实现:事务在此开启,业务规则在此落地 │ │ │ ├── impl/ │ │ │ │ ├── OrderServiceImpl.java # 关键!含库存扣减 + 订单生成 + 小票数据组装三重事务 │ │ │ │ └── GoodsServiceImpl.java # 含商品搜索(支持模糊+分类)、库存预警阈值判断 │ │ ├── dao/ # MyBatis Mapper 接口:方法名直译 SQL 意图,不玩花样 │ │ │ ├── GoodsMapper.java # selectByCategory, updateStockById, selectLowStock │ │ │ └── OrderMapper.java # insertOrderWithDetails, selectOrderByNo, selectOrderDetailsByOrderId │ │ └── entity/ # POJO:字段名与数据库列名严格一致,加了 @Table/@Id 注解 │ │ ├── Goods.java # id, name, price, stock, category_id, status(0=上架/1=下架) │ │ └── Order.java # order_no, total_amount, pay_status, create_time... │ └── config/ # Spring 配置类:非 XML 风格,用 @Configuration + @Bean │ ├── SpringConfig.java # 扫描 service 包,配置事务管理器 │ ├── SpringMvcConfig.java # 视图解析器、静态资源映射、JSON 消息转换器 │ └── MyBatisConfig.java # SqlSessionFactoryBean、MapperScannerConfigurer ├── src/main/resources/ │ ├── jdbc.properties # 数据库连接四要素明文写死(教学场景合理) │ ├── log4j.properties # 日志级别设为 DEBUG,关键操作(如扣库存)必打日志 │ └── mybatis-config.xml # 全局 setting:开启驼峰映射、延迟加载 └── src/main/webapp/ ├── WEB-INF/ │ ├── jsp/ # JSP 页面:无 scriptlet,全用 JSTL + EL,表单提交走 POST │ │ ├── pos/ # 收银台核心页面 │ │ │ ├── index.jsp # 主界面:左侧商品列表(含搜索框)、右侧购物车、底部快捷键提示 │ │ │ └── print.jsp # 小票预览页(供打印前确认,含模拟小票样式) │ │ └── admin/ # 后台管理(权限极简,仅需登录即可) │ └── web.xml # Servlet 3.0+,仅声明 DispatcherServlet 和字符编码过滤器 └── static/ # 纯前端资源:jQuery 3.6.0 + Bootstrap 4.6.0 + layer 弹窗 ├── js/ │ ├── pos.js # 收银台交互逻辑:扫码监听(键盘事件捕获)、购物车增删、快捷键绑定(F1=清空/F2=结账) │ └── print.js # 调用浏览器 print() + CSS 媒体查询适配热敏纸尺寸(80mm 宽) └── css/ └── pos.css # 专为收银台优化:大字体、高对比度、按钮尺寸≥44px(适配触屏)提示:
pos.js是整个交互的灵魂。它不依赖任何 UI 框架,用原生document.getElementById操作 DOM,所有商品点击事件都委托到.goods-list容器上——这意味着你新增 100 个商品,JS 代码一行都不用改。这种“面向事件委托”的写法,是很多毕业设计里缺失的工程意识。
2.2 技术选型深意:为什么坚持用 SSM 而非 Spring Boot?
这不是技术怀旧。在教学场景下,SSM 的显式配置反而成了优势:
- Spring 配置暴露事务边界:
@Transactional注解写在OrderServiceImpl.createOrder()方法上,学生一眼就能看到“这里开始事务”,而 Spring Boot 的自动配置会把事务管理器藏在spring-boot-starter-jdbc的依赖链深处; - MyBatis XML 显式 SQL 可调试:
OrderMapper.xml里的<insert>标签里,<foreach>循环插入订单明细,SQL 语句和参数绑定一目了然,比@InsertProvider动态 SQL 更易理解; - JSP + JSTL 教学友好:
index.jsp中${goodsList}直接遍历 List,<c:if test="${empty goodsList}">控制空状态,语法简单,错误时浏览器报错信息直指 JSP 行号,不像 Vue 单文件组件报错要翻三页控制台。
我一般会告诉学生:先用 SSM 把“请求怎么进来、数据怎么查、事务怎么管、页面怎么刷”这四步走通,再迁移到 Spring Boot,你会真正明白@SpringBootApplication到底帮你省了多少行配置。
2.3 数据库设计:5 张表撑起全部业务,字段命名拒绝“玄学缩写”
MySQL 使用 5.7 版本,建表语句在sql/cashier_db.sql中,全程使用utf8mb4字符集(支持 emoji,虽然收银系统用不到,但这是好习惯)。核心三张表关系如下:
| 表名 | 主要字段 | 关键约束 | 业务意义 |
|---|---|---|---|
goods | id,name,price,stock,category_id,status | stock >= 0(CHECK),status IN (0,1) | 商品主数据,status=0表示可售,stock为实时库存 |
orders | order_no,total_amount,pay_status,create_time,operator_id | order_no唯一索引,pay_status DEFAULT 0 | 订单头,pay_status=1表示已支付 |
order_details | id,order_no,goods_id,quantity,price_at_order | 复合索引(order_no, goods_id) | 订单明细,price_at_order记录下单时价格,防止商品调价影响历史订单 |
另外两张辅助表:
categories:商品分类(id,name,sort_order),sort_order控制后台商品列表分类排序;operators:操作员(id,username,password,role),role=1为管理员,role=0为普通收银员(仅能访问/pos/**)。
注意:所有金额字段(
price,total_amount)均使用DECIMAL(10,2)类型,绝不用FLOAT或DOUBLE。这是金融类操作的铁律——0.1 + 0.2 != 0.3在浮点数里是常态,但在收银系统里,少收一分钱就是事故。
3. 运行起来:从数据库初始化到收银台首单,6 步走通不翻车
别急着mvn clean install。这个项目对运行环境有明确要求,且每一步都有“血泪经验”埋伏。我把它拆成可验证的 6 步,每步执行后都有明确的成功标志,失败立刻定位。
3.1 环境准备:JDK 8 + Tomcat 8.5 + MySQL 5.7(版本错一位都可能白忙)
- JDK:必须是 JDK 8u202 或更高(低版本
java.timeAPI 不全,LocalDateTime在 MyBatis 中映射会报错); - Tomcat:推荐 Apache Tomcat 8.5.99(官方最新 8.5.x),不能用 Tomcat 9+—— 因为
web.xml中web-app版本声明是3.0,Tomcat 9 默认要求4.0,启动会报web.xml is invalid; - MySQL:5.7.33 是测试通过版本,若用 8.0+,需额外修改
jdbc.properties中的驱动类名为com.mysql.cj.jdbc.Driver,并添加?serverTimezone=Asia/Shanghai&useSSL=false参数。
提示:
jdbc.properties文件路径是src/main/resources/jdbc.properties,内容如下(请按你本地环境修改):jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cashier_db?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456
3.2 数据库初始化:执行 SQL 脚本后,必须验证三件事
进入 MySQL 命令行,执行:
CREATE DATABASE cashier_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE cashier_db; SOURCE /path/to/your/sql/cashier_db.sql;执行成功后,立刻验证以下三点(缺一不可):
SELECT COUNT(*) FROM goods;返回> 0(脚本默认插入 12 条测试商品);SELECT * FROM operators WHERE username='admin';能查到密码为21232f297a57a5a743894a0e4a801fc3(MD5 加密的admin)的记录;SHOW CREATE TABLE orders;输出中包含order_no字段的UNIQUE KEY定义。
3.3 IDEA 导入与配置:Maven 项目识别后,必须改两处
在 IntelliJ IDEA 中:
File → Open → 选择项目根目录,IDEA 会自动识别为 Maven 项目;- 关键配置一:
File → Project Structure → Project → Project SDK选 JDK 8; - 关键配置二:
File → Project Structure → Modules → [你的模块名] → Sources,确认src/main/java是蓝色(Sources),src/main/resources是绿色(Resources)——若resources是白色,右键 →Mark as Resources,否则jdbc.properties无法被加载; - 添加 Tomcat Server:
Run → Edit Configurations → + → Tomcat Server → Local,Deployment → + → Artifact → [你的项目名]:war exploded。
3.4 启动与首单验证:从首页到小票,5 分钟闭环
- 点击绿色三角形启动 Tomcat;
- 浏览器打开
http://localhost:8080/cashier/,应跳转至http://localhost:8080/cashier/login.jsp(注意路径末尾无/,否则 404); - 输入
admin/admin登录,应跳转至http://localhost:8080/cashier/pos/index.jsp; - 在左侧商品列表点击任意商品(如“农夫山泉 550ml”),右侧购物车应实时增加一条记录,总价更新;
- 点击右下角「结账」按钮(或按
F2),弹出确认框,点击确定; - 成功标志:页面跳转至
/pos/print.jsp,显示模拟小票(含订单号、商品明细、总金额),且控制台输出类似:[DEBUG] c.e.c.s.i.OrderServiceImpl - 创建订单:NO20240520102345,总金额:2.00,商品数:1 [DEBUG] c.e.c.d.OrderMapper - 插入订单头:NO20240520102345 [DEBUG] c.e.c.d.OrderMapper - 插入订单明细:NO20240520102345 -> goods_id=1, quantity=1 [DEBUG] c.e.c.d.GoodsMapper - 扣减商品ID=1库存,原值=99,新值=98
3.5 打印功能实测:不是“假装能打”,而是真连热敏打印机
print.jsp页面底部有「打印小票」按钮,点击后触发window.print()。但要让热敏纸打出正确格式,需两步:
- CSS 媒体查询适配:
static/css/pos.css中定义了@media print规则:@media print { body { margin: 0; padding: 0; font-size: 12px; width: 300px; } /* 300px ≈ 80mm */ .no-print { display: none !important; } .receipt-item { page-break-inside: avoid; } } - 浏览器打印设置:Chrome 打印时,务必勾选「背景图形」(否则小票边框不显示),纸张尺寸选「自定义:80mm × 150mm」,页边距选「最小值」。
实测:用得最多的是「佳博 GP-1324D」热敏打印机,USB 直连,Windows 下无需装驱动(系统自带),Mac 需安装厂商提供的 CUPS 驱动。只要浏览器能正常打印 PDF,这台机器就能出票。
4. 避坑指南:那些让你怀疑人生、但其实三分钟就能解决的常见问题
这个项目在某高校实验室被 32 名学生复现过,以下是高频踩坑点,按「现象 → 原因 → 解决」整理,亲测有效。
4.1 现象:启动 Tomcat 后,访问http://localhost:8080/cashier/报 404,但http://localhost:8080/能看到 Tomcat 首页
原因:IDEA 中Artifact部署路径未生效,或web.xml中servlet-mapping的url-pattern与实际请求不匹配。
解决:
- 检查
Run → Edit Configurations → Deployment → Application context是否为/cashier(不是/或空); - 查看
web.xml中DispatcherServlet的url-pattern是否为/(必须是/,不是*.do或/dispatcher); - 清理 Tomcat
work目录(C:\apache-tomcat-8.5.99\work\Catalina\localhost\下删掉cashier文件夹)。
4.2 现象:登录页输入账号密码后,页面刷新但仍在登录页,无任何错误提示
原因:login.jsp表单提交的action地址错误,或LoginController中@RequestMapping路径与之不一致。
解决:
- 查看
login.jsp源码,确认<form action="${pageContext.request.contextPath}/login" method="post">; - 查看
LoginController.java,确认@RequestMapping("/login")方法存在,且@PostMapping注解正确; - 最关键:检查
LoginController是否被 Spring 扫描到——SpringMvcConfig.java中@ComponentScan("com.example.cashier.controller")的包路径是否与实际一致。
4.3 现象:点击商品后购物车不更新,控制台无 JS 错误,Network 面板看到add-to-cart请求返回 404
原因:pos.js中 AJAX 请求 URL 写死了上下文路径,未用${pageContext.request.contextPath}动态获取。
解决:
打开static/js/pos.js,找到$.post("/cashier/add-to-cart", {...})这行,改为:
$.post("${pageContext.request.contextPath}/add-to-cart", { goodsId: goodsId, quantity: 1 }, function(data) { ... });血泪经验:所有 JS 中的请求路径,必须通过 JSP 表达式注入上下文路径,硬编码
/cashier在部署到 ROOT 上下文时必然失败。
4.4 现象:结账后订单生成成功,但商品库存没减少,GoodsMapper.updateStockById方法没执行
原因:OrderServiceImpl.createOrder()方法上@Transactional注解失效,常见于:
- 方法被本类其他方法调用(Spring AOP 代理失效);
Service类未被 Spring 容器管理(缺少@Service注解或未被@ComponentScan扫描)。
解决:
- 检查
OrderServiceImpl.java顶部是否有@Service("orderService"); - 检查
SpringConfig.java中@ComponentScan是否包含com.example.cashier.service.impl; - 终极验证:在
createOrder()方法第一行加System.out.println("事务已开启:" + TransactionSynchronizationManager.isActualTransactionActive());,启动时看控制台是否输出true。
4.5 现象:小票打印出来全是乱码(中文显示为方块或问号)
原因:热敏打印机驱动未正确安装,或浏览器打印时未启用「背景图形」。
解决:
- Windows 下:设备管理器中找到打印机 → 右键「更新驱动程序」→ 「浏览我的电脑以查找驱动程序」→ 「让我从计算机上的可用驱动程序列表中选取」→ 选「Generic / Text Only」;
- Chrome 打印设置中,务必勾选「背景图形」(Background graphics);
- 若仍乱码,在
print.jsp的<head>中加入:<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5. 核心模块拆解:OrderServiceImpl.createOrder()从下单到扣库存的完整链路
这是整个系统的“心脏”,127 行代码里藏着事务控制、并发安全、数据一致性三大考点。我们逐段拆解,不跳过任何一行。
5.1 方法签名与前置校验:防御性编程的第一道门
@Transactional(rollbackFor = Exception.class) public Result createOrder(List<OrderDetail> orderDetails, String operatorId) { // 1. 校验购物车非空 if (CollectionUtils.isEmpty(orderDetails)) { return Result.fail("购物车为空,请先添加商品"); } // 2. 校验商品是否存在且可售 for (OrderDetail detail : orderDetails) { Goods goods = goodsMapper.selectById(detail.getGoodsId()); if (goods == null || goods.getStatus() != 0) { return Result.fail("商品ID=" + detail.getGoodsId() + "不存在或已下架"); } if (goods.getStock() < detail.getQuantity()) { return Result.fail("商品【" + goods.getName() + "】库存不足,当前剩余:" + goods.getStock()); } } // 3. 生成订单号(时间戳+随机数,保证唯一) String orderNo = "NO" + System.currentTimeMillis() + RandomUtils.nextInt(1000, 9999);逻辑说明:
@Transactional声明此方法为事务边界,rollbackFor = Exception.class表示任何异常都回滚(包括RuntimeException和Checked Exception);CollectionUtils.isEmpty()是 Apache Commons Collections 工具类,比list == null || list.size() == 0更安全;- 关键细节:校验库存时,
goods.getStock() < detail.getQuantity()是“读取后校验”,此时还未加锁,属于乐观校验——真正的扣减在后续 SQL 中完成。
5.2 数据库层面的原子扣减:用 SQL 语句保证库存不超卖
// 4. 扣减库存(关键!用 UPDATE ... WHERE stock >= ? 保证原子性) for (OrderDetail detail : orderDetails) { int affected = goodsMapper.updateStockByIdAndMinStock( detail.getGoodsId(), detail.getQuantity(), detail.getQuantity() // WHERE stock >= #{minStock} ); if (affected == 0) { throw new RuntimeException("库存扣减失败,商品ID=" + detail.getGoodsId() + " 库存不足"); } }对应的GoodsMapper.xml中 SQL:
<update id="updateStockByIdAndMinStock" parameterType="map"> UPDATE goods SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{minStock} </update>参数说明:
#{id}:商品 ID,预编译参数防 SQL 注入;#{quantity}:要扣的数量,直接用于SET stock = stock - ?;#{minStock}:最小允许库存值,即WHERE stock >= ?,确保扣减前库存充足;affected == 0表示WHERE条件不成立,即库存不足,此时抛异常触发事务回滚。
这就是“乐观锁”在业务中的朴素实现:不加数据库行锁,靠
UPDATE ... WHERE的原子性 + 影响行数判断来保证一致性。比SELECT ... FOR UPDATE更轻量,适合读多写少的收银场景。
5.3 订单持久化与事务提交:三张表联动,一个都不能少
// 5. 插入订单头 Order order = new Order(); order.setOrderNo(orderNo); order.setTotalAmount(calculateTotal(orderDetails)); order.setPayStatus(0); // 0=未支付 order.setOperatorId(operatorId); order.setCreateTime(LocalDateTime.now()); orderMapper.insertOrder(order); // 6. 插入订单明细(批量插入,提升性能) for (OrderDetail detail : orderDetails) { detail.setOrderNo(orderNo); detail.setPriceAtOrder(goodsMapper.selectById(detail.getGoodsId()).getPrice()); orderDetailMapper.insert(detail); } // 7. 返回成功结果(含订单号,供打印页使用) return Result.success("下单成功", orderNo); }逻辑说明:
calculateTotal()方法遍历orderDetails,累加priceAtOrder * quantity,注意此处价格取自goodsMapper.selectById()而非前端传入,防止恶意篡改;orderDetailMapper.insert(detail)是单条插入,若数据量大(>100 条),可改为insertBatch()批量操作(需 MyBatis 3.4+);- 事务提交时机:当
createOrder()方法正常结束,Spring 会自动提交事务;若中间任何一步抛异常(如updateStockByIdAndMinStock返回 0),整个事务回滚,库存不变、订单不生成、明细不插入。
6. 二次开发实战:如何快速接入微信支付、增加会员折扣、导出销售报表
拿到源码不是终点,而是改造的起点。下面三个真实需求,我都带着学生在 2 小时内完成过,步骤清晰、改动可控、验证简单。
6.1 接入微信支付:不碰核心订单逻辑,只扩展支付状态
微信支付本质是「异步通知 + 主动查询」,无需修改createOrder(),只需新增支付回调接口和状态更新逻辑。
步骤:
- 在
controller包下新建PayController.java,添加@PostMapping("/pay/notify")方法,接收微信服务器 POST 的 XML 通知; - 解析 XML,校验签名,提取
out_trade_no(即订单号)和result_code; - 调用
orderService.updatePayStatus(orderNo, 1)更新orders.pay_status = 1; - 关键:
updatePayStatus()方法必须加@Transactional,且只更新pay_status字段,不碰库存(支付成功不等于发货,库存已在下单时扣减)。
验证方式:用curl模拟微信通知:
curl -X POST http://localhost:8080/cashier/pay/notify \ -H "Content-Type: application/xml" \ -d '<xml><out_trade_no>NO20240520102345</out_trade_no><result_code>SUCCESS</result_code></xml>'然后查数据库SELECT pay_status FROM orders WHERE order_no='NO20240520102345';应返回1。
6.2 增加会员折扣:在结算前动态计算,不影响历史订单
会员折扣是“读时计算”,不改变订单数据结构,只需在PosController的结账逻辑中插入计算环节。
改动点:
- 新增
member表(id,card_no,discount_rate,status),discount_rate为DECIMAL(3,2),如0.95表示 95 折; - 修改
PosController.checkout()方法,在调用orderService.createOrder()前,根据收银员输入的会员卡号查询折扣率; - 将折扣率传入
createOrder(),在calculateTotal()中改为:BigDecimal total = BigDecimal.ZERO; for (OrderDetail detail : orderDetails) { total = total.add(detail.getPriceAtOrder() .multiply(BigDecimal.valueOf(detail.getQuantity())) .multiply(discountRate)); // 乘以折扣率 }
注意:折扣率只影响本次订单总金额显示和
Order.total_amount字段,不改变OrderDetail.price_at_order——这样历史订单的明细价格永远真实,报表统计才准确。
6.3 导出销售报表:用 Apache POI 生成 Excel,零配置
销售报表需求:导出近 7 天每日销售额、订单数、商品销量 Top10。不引入新框架,直接用项目已有的pom.xml中的poi-3.17依赖。
实现步骤:
- 在
service包下新建ReportService.java,添加exportSalesReport(LocalDate startDate, LocalDate endDate)方法; - SQL 查询(在
ReportMapper.xml中):<select id="selectDailySales" resultType="map"> SELECT DATE(create_time) as date, COUNT(*) as order_count, SUM(total_amount) as sales_amount FROM orders WHERE create_time BETWEEN #{startDate} AND #{endDate} GROUP BY DATE(create_time) ORDER BY date DESC </select> - 在
controller中添加@GetMapping("/report/export"),调用reportService.exportSalesReport(),返回ResponseEntity<Resource>,Content-Type设为application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。
验证:浏览器访问http://localhost:8080/cashier/report/export?startDate=2024-05-14&endDate=2024-05-20,下载 Excel 文件,打开后应有三列:日期、订单数、销售额。
从那以后我每次带学生做毕设,都会强制他们先跑通这个收银系统,再谈“我要做个校园二手平台”。因为只有亲手扣过库存、打过小票、修过 404,才会真正理解什么叫“Web 开发不是拼凑 API”,什么叫“事务不是注解,是数据不丢的承诺”。希望帮到你。
本文还有配套的精品资源,点击获取