每年带学生做毕业设计我见过太多“图书管理系统”“班级管理系统”这类纯CRUD练手项目。不是不能做而是这类系统的业务逻辑太直白了九成工作量在增删改查上真正能写进论文、能拿到答辩现场讲清楚的技术亮点几乎没有。如果你想用Java做一个既有完整业务闭环、又有并发处理、还能对接第三方接口的选题无人超市支付系统是我目前见过性价比最高的方向之一。它表面上是一套交易系统实际上把前端交互、后端服务、数据库设计、支付回调、库存一致性、异常兜底全串在了一起做完这一套你对Java Web开发的理解会有一个质的提升。这篇文章我不会给你贴一份完整的课程设计报告而是按照我自己带项目、包括实际开发中沉淀下来的思路把无人超市支付系统的设计与实现拆开讲清楚从系统边界、数据库建模到支付核心链路、并发防超卖再到无人场景里特有的识别和进出店处理最后附上调试和答辩阶段的避坑清单。内容主线是“基于Java技术的无人超市支付平台”但里面的设计思路放到任何一套电商交易系统里都通用。1. 为什么“无人超市支付系统”是Java毕设里性价比最高的选题之一很多同学选毕设题目时第一反应是选一个“看起来能做完”的学生管理系统、会议室预约、二手交易平台。这类系统不是不行但面试官和答辩老师一眼就能看出难度天花板在哪里——无非是几个表、几个页面、几个接口。无人超市支付系统不一样它天然带有几个让项目“立得住”的标签线下与线上的结合、支付回调的一致性、高并发下的库存控制、硬件设备的逻辑模拟。这几个点任何一个展开都能讲上十分钟。1.1 技术栈覆盖完整Java生态的主流组件全都能用上用Java做无人超市支付系统最舒服的一点是技术栈选择非常成熟几乎每个环节都有对应的主流方案。后端用Spring Boot做服务端框架MyBatis-Plus或者Spring Data JPA做持久层MySQL存业务数据Redis扛热点库存和分布式锁JWT或Session做用户登录态支付模块可以接入支付宝沙箱或者微信支付沙箱环境如果不想申请商户号也可以自己实现一个“模拟支付网关”把回调、验签、幂等这些机制完整模拟出来。这个组合基本就是国内中小型Java后端项目的标准配置做完之后你写进简历里每一行都不是虚的。1.2 业务场景足够复杂但又不会复杂到做不完无人超市和传统电商最大的区别在于用户是在线下物理空间里完成选购和支付的。这意味着你不仅要处理“加购物车→下单→支付”这条交易主链路还要处理“进门识别→商品识别→出门校验”这些线下场景特有的逻辑。复杂度比图书管理系统高了一个档次但相比真正的电商平台又少了营销、物流、售后这些分支刚好卡在一个“硕士论文有深度、本科毕设能做完”的甜点上。做完这套系统你不是在“写功能”而是在“设计一套交易闭环”这本身就是很好的工程训练。1.3 论文和答辩的亮点天然充足毕设最终是要过答辩的。一个只有CRUD的系统答辩时你能讲的只有“我建了四张表、写了五个接口”老师问两个问题就到底了。但无人超市支付系统不一样你可以讲订单状态怎么流转、支付回调怎么保证不重复处理、库存扣减怎么防超卖、Redis缓存和数据库怎么保持一致、RFID识别和视觉识别方案怎么取舍。这些问题每一个都有业界成熟方案也都有踩坑空间你只要真的动手做过答辩现场几乎不会被问倒。2. 先把业务边界画清楚无人超市的流程绝不是“扫码支付”四个字我见过很多同学拿到这个题目之后上来就建表写代码做到一半发现逻辑对不上用户到底什么时候生成订单支付成功了商品就算卖出去了吗如果有人拿着没付款的商品直接出门怎么办这些问题全是因为一开始没有把业务流程拆解清楚。无人超市支付系统核心不在于“扫码支付”而在于一套完整的线下自助交易闭环。2.1 一张流程图把整条业务主线定下来在做任何一个模块之前我建议你先用文字把主流程写出来。无人超市的标准流程是这样的用户扫码进入超市微信扫码或小程序授权系统记录进店记录分配一个用户会话。用户自由选购商品商品上贴有RFID标签或者依靠视觉识别/称重方案识别品类。用户在自助结算台扫描商品RFID批量读取或逐个扫描条码系统生成“待支付订单”。用户发起支付模拟支付/支付宝沙箱/微信沙箱支付成功后系统回调更新订单状态。用户携带商品走到出门闸机系统根据订单支付状态和商品清单校验校验通过后开门放行。如果用户未支付直接出门系统触发异常告警。这条链路是后面所有表结构和接口设计的根。你后面写的每个接口、建的每张表都应该能在主流程上找到对应的位置。如果某个表或者字段在主流程上没有落点那大概率是设计冗余。2.2 系统边界哪些是代码要做的哪些是模拟的无人超市涉及硬件但毕设阶段不可能真的造一台超市出来。所以一开始就要明确系统边界哪些模块是核心业务代码哪些模块只是“模拟接口”。我常用的做法是门禁管理定义EntranceService接口真实场景对接硬件毕设阶段用一个模拟实现类返回“扫码成功门已开”。商品识别定义ProductRecognizeService接口真实场景对接RFID读写器或视觉识别服务毕设阶段可以模拟一个按RFID编号查询商品的方法。支付通道定义PayChannel抽象接口子类分别是模拟支付通道、支付宝沙箱通道和微信沙箱通道。这样做的好处是你在论文里可以写“本系统采用接口隔离硬件依赖便于后续对接真实设备”这句话在答辩时是非常加分的因为你已经用代码证明了设计思想而不是停留在概念上。2.3 核心状态机订单、支付、库存三者各自的流转无人超市系统里最容易被忽略的是“订单状态”、“支付状态”、“库存状态”其实是三个独立的状态机它们之间靠事件驱动同步而不是靠一个大字段全部装下。订单状态一般有待支付、已支付、已取消、已退款、已完成。支付状态有未支付、支付中、支付成功、支付失败、已退款。库存维度的核心是预扣、确认扣减、回滚。这里有一个非常关键的设计决策什么时候扣库存电商常用的方案是“下单预扣、支付成功确认扣减、超时未支付自动回滚”。无人超市场景同样适用但在结算台生成订单后用户可能站在机器前很久不支付如果提前扣库存其他用户在同一时刻就买不到这件商品了。所以更合理的做法是下单时预扣支付超时后回滚。这个逻辑看似简单但涉及事务边界和定时任务很多初学同学在这里翻车。3. 数据库表设计订单、支付流水、库存之间如何优雅地解耦表结构是整个系统最见功力的部分也是论文里可以重点展示的成果。无人超市支付系统的核心表我建议至少包括这几张用户表、商品表、RFID标签表、购物车/结算单表、订单表、订单明细表、支付流水表、进出店记录表。这里我重点讲三张容易被设计错的表。3.1 订单表和订单明细表为什么必须拆分订单主表存的是订单维度的信息比如订单编号、用户ID、总金额、订单状态、支付状态、创建时间和支付时间。订单明细表存的是商品维度的信息比如商品ID、商品名称、单价、数量、小计金额。拆分的原因是一个订单可能包含多个商品如果全塞在同一张表里要么出现大量冗余字段要么一条订单记录对应多行商品、导致订单主信息无处安放。在设计订单表时有一个字段必须特别注意金额字段一律用decimal禁止使用float或double。金额精度问题在支付系统里是致命伤0.1 0.2 在浮点数计算里会得到一个很长的循环小数导致订单总金额和明细金额对不上。所有涉及金额的字段Java侧用BigDecimalMySQL侧用decimal(10,2)。这一点我在实际项目里踩过坑——线上环境出现过订单金额多出0.01元的投诉原因就是浮点累加精度丢失。订单主表的建表SQL可以简化成下面这样CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号全局唯一, user_id bigint(20) NOT NULL COMMENT 用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 订单状态0待支付 1已支付 2已取消 3已退款, pay_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 支付状态0未支付 1支付中 2支付成功 3支付失败 4已退款, pay_time datetime DEFAULT NULL COMMENT 支付完成时间, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;3.2 支付流水表为什么订单表之外还要单独建一张很多初学同学会把“支付状态”直接放在订单表里觉得支付流水表是多余的。这是一个很典型的认知误区。支付流水表的核心作用是对账和幂等。用户在支付过程中可能发生多次请求支付网关的回调也可能重复推送如果没有一张独立的流水表来记录每一次支付动作系统根本无法判断某一次回调是不是重复的、某一次请求对应的是哪笔支付。支付流水表至少要包含这些字段流水号业务唯一、订单号、用户ID、支付渠道、支付金额、请求参数摘要、回调参数摘要、支付状态、创建时间。每一次发起支付就插入一条流水状态为“支付中”回调到达后先查流水表判断是否已经处理过如果已经处理就直接返回成功不再修改订单。这张表就是整个支付环节的“操作日志”也是后面实现幂等的关键载体。3.3 库存表与预扣机制乐观锁是你最好的朋友商品表的库存字段推荐单独拆一张t_stock表和商品基本信息解耦。库存表里至少包含:商品ID、总库存、已预扣数量、可用库存。用三个字段而不是一个stock字段是因为预扣和实际扣减是两个动作下单时把可用库存减一、已预扣加一支付成功后把已预扣转成实际扣减支付超时再把已预扣回滚、可用库存加回。扣减库存的SQL必须使用乐观锁防止并发超卖。不要先查库存再在Java代码里判断而是用一条带条件的UPDATE语句UPDATE t_stock SET available_stock available_stock - 1, pre_sold_stock pre_sold_stock 1 WHERE product_id #{productId} AND available_stock 0;如果影响行数为0说明库存不足直接抛异常。这条SQL是防超卖的第一道防线也是你在论文和答辩里可以重点展示的并发处理手段。4. 支付核心链路实现从下单到回调通知的完整闭环数据库设计清楚了下面进入代码层面最核心的部分支付链路的实现。我见过很多项目下单、支付、回调三个模块各写各的接口之间完全割裂出了问题根本没法排查。正确的做法是把整条链路串起来每一步都有状态记录每一步都有日志。4.1 下单接口不只是插入一条订单记录下单接口是所有交易系统的入口但它绝不只是向t_order表插入一条数据那么简单。一个完整的下单操作应该在一个事务里完成这几件事Transactional(rollbackFor Exception.class) public OrderVO createOrder(CreateOrderRequest request) { // 1. 生成唯一订单号 String orderNo generateOrderNo(); // 2. 校验商品信息、计算订单总金额金额一律用BigDecimal ListOrderItem items buildOrderItems(request.getItems()); BigDecimal totalAmount calculateTotalAmount(items); // 3. 预扣库存使用乐观锁库存不足直接抛异常 for (OrderItem item : items) { boolean success stockService.preDeductStock(item.getProductId(), item.getQuantity()); if (!success) { throw new BusinessException(商品[ item.getProductName() ]库存不足); } } // 4. 插入订单主表和订单明细表 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(request.getUserId()); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.PENDING_PAYMENT); order.setPayStatus(PayStatus.UNPAID); orderMapper.insert(order); orderItemMapper.batchInsert(items); // 5. 返回订单信息等待用户发起支付 return buildOrderVO(order, items); }这里有一个细节很多人忽略事务一定要包住“预扣库存”和“插入订单”两个动作。如果扣了库存但订单插入失败事务回滚后库存也会自动回滚数据保持一致。如果不用Transactional就可能出现订单没建成功但库存少了的情况。4.2 支付抽象模拟支付网关和沙箱支付如何优雅切换支付模块是整个系统中“设计模式”价值最集中的地方。我推荐定义一个PayChannel接口public interface PayChannel { /** 渠道标识mock / alipay / wechat */ String getChannel(); /** 发起支付返回支付参数二维码内容、跳转链接等 */ PayResult pay(PayRequest request); /** 校验回调签名 */ boolean verifyNotify(MapString, String params); /** 处理回调返回处理结果 */ NotifyResult handleNotify(MapString, String params); }然后分别实现MockPayChannel、AlipaySandboxChannel、WechatSandboxChannel。在PayService里用工厂模式根据请求参数中的channel字段获取对应实现。这样做的好处是开发阶段用模拟支付毫秒级完成回调方便调试答辩演示时切换成支付宝沙箱展示真实支付流程论文里还可以强调系统的扩展性。模拟支付通道的实现其实很简单生成一个支付二维码内容字符串即可然后开启一个异步任务模拟用户“扫码支付成功”延迟3秒后主动调用自己的回调接口。这能让你在本地把整条支付链路完整跑通不需要真的拥有支付宝商户号。4.3 回调处理验签、幂等、异步更新订单状态支付回调是支付系统里最需要谨慎处理的环节。无论是模拟支付还是沙箱支付回调通知都可能因为网络原因重复推送。回调处理的三个核心原则第一验签。真实支付网关会对回调参数做签名服务端必须用配置好的密钥验签防止伪造回调。模拟支付通道可以用一个简单的规则代替比如回调参数里带一个token与下发支付时保存的token比对。第二幂等。回调到达后先根据订单号查支付流水表。如果流水状态已经是“支付成功”直接返回成功响应不再重复更新订单。第三状态更新使用异步方式。不要在回调方法里做太多耗时操作而是把核心状态更新放在事务里其余动作发送通知、记录日志放在事务之后。回调处理的核心代码逻辑Transactional(rollbackFor Exception.class) public void handlePayNotify(String orderNo, String channel, String tradeNo, BigDecimal amount) { // 1. 根据订单号查询支付流水判断是否已处理幂等 PayFlow payFlow payFlowMapper.selectByOrderNoForUpdate(orderNo); if (payFlow ! null PayStatus.PAID payFlow.getStatus()) { log.info(重复回调直接返回orderNo{}, orderNo); return; } // 2. 校验金额是否和订单金额一致防篡改 Order order orderMapper.selectByOrderNo(orderNo); if (order.getTotalAmount().compareTo(amount) ! 0) { log.error(回调金额与订单金额不一致orderNo{}, orderNo); return; } // 3. 插入或更新支付流水 if (payFlow null) { payFlow buildPayFlow(orderNo, channel, tradeNo, amount); payFlowMapper.insert(payFlow); } else { payFlow.setStatus(PayStatus.PAID); payFlowMapper.updateById(payFlow); } // 4. 更新订单支付状态和订单状态 order.setStatus(OrderStatus.PAID); order.setPayStatus(PayStatus.PAID); order.setPayTime(new Date()); orderMapper.updateById(order); }这段代码里selectByOrderNoForUpdate用了数据库行锁它的作用是当两个回调线程同时到达时第二个线程会被阻塞在查询这条流水上等第一个线程事务提交后第二个线程查到的是已经支付成功的流水直接走重复回调分支。这就是一个非常经典的“数据库锁状态判断”的幂等方案。5. 并发场景的硬仗防超卖、幂等与状态一致性如果你只是把功能跑通不处理并发问题项目也能运行但这套系统的“含金量”就掉了大半。无人超市在高峰期可能同时有几十个人在结算同一个商品可能被多个人同时加入购物车。这个时候并发问题就会暴露出来。这一章我把最容易出现的三个并发问题逐个讲透。5.1 防超卖乐观锁扣库存为什么比“先查再扣”可靠最原始的写法是先select stock from t_stock where product_id ?在Java代码里判断stock 0然后update t_stock set stock stock - 1。这个写法在并发量低的时候没问题但当两个请求同时查到库存为1两个都通过判断然后各自执行扣减最终库存变成-1超卖就发生了。关键问题在于“查询和更新不是原子操作”。解决办法之一就是我在前面提到的乐观锁更新把判断条件放到UPDATE语句的WHERE子句里available_stock 0数据库会保证这条语句的原子性并发执行时只有一条能成功。这是最简单、最可靠、也最容易在答辩时讲清楚的方案。很多同学一上来就“Redis分布式锁”反而把简单问题复杂化了——如果没有多实例部署的需求数据库乐观锁完全够用而且代码更易读。5.2 支付回调的重复通知数据库行锁状态机双保险支付回调的重复通知在前端没有任何表现但在后端日志里会非常普遍沙箱环境尤其明显。你可能会收到两三次甚至更多次相同的回调通知。如果回调处理没有幂等设计订单状态可能从“已支付”被打回“未支付”或者支付成功被重复记账。我的解决方案是双保险selectByOrderNoForUpdate行锁加上支付流水表状态判断。行锁解决并发冲突状态判断解决串行重复。两者配合能应付绝大多数回调重复场景。5.3 数据库事务边界扣库存和更新订单必须在一个事务里吗这是一个值得深入思考的问题。严格来说支付成功后“更新订单状态”和“确认扣减库存”并不一定强一致。在大型电商系统里这两者往往是最终一致的支付回调先更新订单状态然后通过消息队列通知库存服务确认扣减。但毕设系统不推荐引入消息队列太重了。更务实的做法是调整设计下单时预扣库存支付成功回调时把预扣转成实扣这个过程在同一个事务里完成代码简单可靠性高。真正应该拆出去的是给用户发通知、记录日志这些非核心动作它们不应该拖慢事务提交。6. 无人超市的“无人”怎么落地识别、进出门与异常兑付这一章是无人超市系统和普通电商系统最大的区别所在。很多同学做完交易系统之后发现“无人超市”只是一个壳换一个标题也能用这就是因为缺少了对线下场景的建模。6.1 商品识别方案RFID、视觉识别和条码的取舍无人超市的商品识别主要有三种技术路线。RFID方案每个商品贴RFID标签结算台用读写器批量读取优点是识别快、可以整篮扫描缺点是标签成本高。视觉识别方案通过摄像头和AI识别商品用户体验好但算法复杂不适合毕设阶段自研。条码方案用户逐个扫描条码成本最低、实现最简单。我推荐的毕设方案是RFID模拟加条码兜底。RFID模块定义好接口模拟实现直接根据编号返回商品信息同时支持手动输入条码。这样既在论文里体现了对行业主流方案的理解又避免了自己去折腾太多硬件。6.2 进出店记录把线下行为变成线上数据进店和出店是无人超市两个标志性节点。用户扫码进店时系统记录一条进店记录绑定用户ID和会话出门时系统根据当前会话判断用户是否有“已完成”的订单有则放行无则标记异常。这里有一个很容易踩的坑用户可能进店不买、直接出门或者支付成功但没走出门下次又进店。所以进出店记录不能简单用“有没有订单”来判断而要和订单绑定后做状态机管理。比如一条记录可以有“进店中、选购中、结算中、已出门、异常出门”五种状态。用户在店期间的所有操作都挂在这条记录下面这样任何时刻你都能回答“用户在店里干了什么”这个问题。6.3 异常场景兜底未支付出门、支付后门不开、商品未识别无人超市看起来“无人”其实背后要做大量异常处理。我在项目里至少预留了这几个异常处理接口用户未支付直接通过出口闸机系统应该触发告警并将用户ID和商品信息写入异常记录方便追溯。用户支付成功后闸机未开需要提供“客服呼叫”或者“订单申诉”入口用户出示支付记录人工放行。结算时部分商品识别失败应该允许用户手动移除或重新识别而不是直接阻塞整单。这些异常逻辑在答辩时非常容易引起老师兴趣因为大多数学生的系统只做了“正常流程”没有考虑真实场景的容错。你在代码里预留了这些处理论文里完全可以独立写一节“异常场景分析与系统容错设计”。7. 从调试到答辩模拟支付网关、日志与演示避坑清单最后这一章全是实际操作层面的东西我按“开发调试阶段”和“演示答辩阶段”两个场景来梳理。这些经验不是我拍脑袋编的都是实际开发中反复踩过的坑。7.1 开发阶段的日志设计支付链路的每一步都要留痕支付系统出问题最怕的就是日志不全。开发阶段我建议你强制自己做到每次发起支付时打印“订单号、渠道、金额”回调到达时打印“完整回调参数”订单状态更新时打印“订单号、旧状态、新状态”。这三个节点的日志串起来就是一条完整的支付链路。现场答辩时如果系统出问题你能快速从日志定位是下单环节还是回调环节这比现场临时加打印语句高效得多。7.2 模拟支付网关的定时回调如何让演示稳定不翻车模拟支付通道里我用了ScheduledExecutorService做延迟回调开发时把延迟设成3秒方便看效果。但在答辩演示时3秒会显得节奏慢我建议你在配置中心把延迟压到1秒或者做成可配置的。另外一个更关键的细节演示时不要打开微信开发者工具的真实支付一旦沙箱环境网络抖动整个演示就卡住了。最稳妥的方案是演示时使用模拟支付通道答辩PPT里放一段真实沙箱支付的录屏。这样既展示了完整流程又规避了现场网络风险。7.3 答辩前一定要检查的五个细节答辩翻车往往不是因为功能没做完而是因为小细节。我总结了一份自检清单每个做这个题目的同学都值得对照检查一遍检查项为什么重要订单号是否全局唯一重复订单号在回调对账时是灾难演示中一旦出现老师会直接质疑问数据库设计金额字段是否全部用BigDecimal浮点精度问题在演示时很容易被当场测试出来0.10.2不等于0.3模拟支付回调是否幂等重复点击支付按钮如果没有拦截会产生多笔订单或重复回调库存不足时是否有清晰提示如果只是抛出异常没有提示演示时会显得项目很业余数据库表关系是否有ER图论文和PPT里必须有一张清晰的ER图这是老师最喜欢看的7.4 答辩陈述时怎么讲这套系统答辩陈述不是照着PPT念而是讲清楚你做了什么决策、为什么这么做。我建议你围绕三个点来讲第一业务闭环——从进店到出门整个流程你是怎么设计的第二技术难点——预扣库存、幂等回调、事务一致性这些你是怎么解决的第三系统边界——哪些模块是模拟的哪些模块是真实可对接的你留了什么扩展点。这三个点讲完答辩时间基本就满了而且每个点都有实际代码支撑不虚。我做这个项目的最大体会是无人超市支付系统的价值不在于“无人”这两个字而在于它逼着你去思考一个交易系统如何做到“状态正确、并发安全、异常可控”。做完之后回头看你不再是只会写CRUD的工具人而是真的在用一个工程师的视角去设计一套系统。这对于一个即将走出校门、走进开发岗位的人来说比任何一门课程作业都有意义。