简介:这是一套基于JavaWeb与Vue2的前后端分离点餐系统后端源码,面向Java Web开发初学者及全栈学习者,适用于课程设计、毕业项目或中小餐饮系统原型开发。资源共157个文件,含137个Java业务与Servlet核心类(如OrderService、PayService、AuthService等)、12个MyBatis-Flex映射XML配置、2个MySQL建表与初始化SQL脚本、2个properties配置文件,以及HTTP接口测试样例和README说明,整体压缩包仅168KB,轻量易部署。已有454人学习下载,体现了其在教学实践中的实用热度。读者可直接运行后端服务,快速掌握JWT+Redis的认证授权与单点登录实现、ZXing二维码生成集成、MyBatis-Flex简化DAO层开发等关键技术点,并通过清晰分层的服务模块(订单、账单、购物车、用户、支付)理解典型电商类业务逻辑设计。
1. 这不是又一个“学生课设”:JavaWeb点餐系统源码里藏着企业级认证链、Redis缓存穿透防护和真实支付闭环的骨架
你搜“JavaWeb点餐系统源码”,十有八九点开是JSP+Servlet+MySQL三件套,登录页写死admin/123,下单后数据直接INSERT进数据库,连事务回滚都靠try-catch硬扛。但这份名为基于javaWeb的点餐系统源码.zip的资源,从文件列表里的auth-api.http、cuisine-api.http到PayService.java,再到摘要里明确写的“Redis+JWT实现认证授权及单点登录”“ZXing生成二维码”“Mybatis-Flex简化操作”,它根本不是教学演示品——这是个已跑通核心业务流、预留了高并发改造接口、且把安全边界划得清清楚楚的生产级后端骨架。它适合两类人:一是想甩掉Spring Boot黑盒、亲手拧紧每个认证环节的中级Java开发者;二是正被“如何让Servlet项目支持JWT+Redis会话同步”卡住的课程设计者。前端虽未上传(Vue2+Element-UI+Vant),但后端API设计已按RESTful规范拆解,OrderService里订单状态机、CartService中购物车合并逻辑、BillService_bak.java这个备份文件暗示着历史迭代痕迹——它不完美,但足够真实。你拿到的不是玩具,而是一台能点火、有油路、带刹车的发动机。
2. 从零启动:IDEA配置Servlet环境、MySQL8建库、Redis连接与JWT密钥注入全流程
2.1 IDEA中配置原生JavaWeb项目:绕过Spring Boot自动装配的“手动挡”设置
很多新手在IDEA里新建JavaWeb项目时,习惯性勾选“Spring Web”或“Maven Archetype”,结果一运行就报ClassNotFoundException: javax.servlet.http.HttpServlet。这不是代码问题,是环境没对齐。必须用最原始的方式搭Servlet容器:
- 新建Project → 选择“Empty Project”(空项目),绝不选任何模板;
- 右键Project → “New Module” → “Java Enterprise” → 勾选“Web Application”,取消勾选所有框架支持;
- 在
web/WEB-INF/web.xml中手动声明Servlet:
<servlet> <servlet-name>AuthServlet</servlet-name> <servlet-class>com.cuisine.auth.AuthServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>AuthServlet</servlet-name> <url-pattern>/api/auth/login</url-pattern> </servlet-mapping>提示:
web.xml是Servlet 3.0之前的标配,本项目沿用此方式,意味着它兼容Tomcat 7+,但必须确保IDEA的Artifact配置中包含web.xml和lib目录。右键Project → “Open Module Settings” → “Artifacts” → 点击“+” → “Web Application: Archive” → 在“Output Layout”中确认WEB-INF/web.xml和WEB-INF/lib/下有mybatis-flex-1.7.4.jar等依赖。
2.2 MySQL8建库与Mybatis-Flex初始化:解决时区、SSL和驱动版本三重陷阱
摘要明确要求MySQL8,但直接执行CREATE DATABASE cuisine DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;后,UserService.java中的@Select("SELECT * FROM user WHERE username = ?")仍可能报java.sql.SQLException: The server time zone value 'XXX' is unrecognized。这是因为MySQL8默认启用serverTimezone=UTC,而JDBC驱动未显式声明。必须在jdbc.properties中补全参数:
jdbc.url=jdbc:mysql://localhost:3306/cuisine?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false jdbc.driver=com.mysql.cj.jdbc.Driver注意:
allowPublicKeyRetrieval=true是MySQL8.0.13+必需参数,否则AuthService.java中BCryptPasswordEncoder验证密码时会因SSL握手失败而卡死;useSSL=false非妥协,而是因本项目未配证书,强制开启SSL会导致Connection refused。Mybatis-Flex的Mapper类(如UserMapper.java)需继承BaseMapper<User>,其selectOne方法底层调用SELECT * FROM user WHERE id = ?,但BillService.java中updateBillStatus()使用了lambdaQuery().eq(Bill::getId, id).set(Bill::getStatus, status).update(),这说明Flex已启用Lambda条件构造器——你无需手写SQL,但必须确认pom.xml中mybatis-flex版本≥1.7.0,否则lambdaQuery()方法不存在。
2.3 Redis连接池与JWT密钥注入:单点登录不是“把token塞进Redis”这么简单
AuthService.java中generateToken()方法返回JWT.create().withSubject(userId).sign(Algorithm.HMAC256(secretKey)),而secretKey来自application.properties。但摘要强调“单点登录”,这意味着同一用户在多设备登录时,旧token必须失效。auth-api.http文件里有一行GET http://localhost:8080/api/auth/logout,其背后逻辑藏在AuthService.java的invalidateToken()中:
public void invalidateToken(String token) { String userId = JWT.decode(token).getSubject(); String redisKey = "user:token:" + userId; redisTemplate.delete(redisKey); // 关键:删除用户维度的token键 }这就是单点登录的核心:不是删token本身,而是删
user:token:{userId}这个键。因此Redis连接不能只配host/port,必须启用连接池防雪崩:
@Bean public RedisConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("localhost", 6379); GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); poolConfig.setMaxTotal(20); // 防止并发突增打满Redis poolConfig.setMinIdle(5); LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() .commandTimeout(Duration.ofSeconds(2)) .pool(poolConfig) .build(); return new LettuceConnectionFactory(config, clientConfig); }若跳过连接池配置,PayService.java中createOrderQrCode()调用ZXing生成二维码后,再存入Redis缓存时,高并发下会出现Cannot get Jedis connection错误。
3. 核心服务链路解析:从用户登录到支付二维码生成的7层调用栈与关键参数
3.1 认证服务(AuthService):JWT签发、Redis存储与令牌刷新的原子性保障
AuthService.java的login()方法是整个系统的入口,它不只做密码校验,还承担会话生命周期管理:
public Result login(LoginDTO dto) { User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, dto.getUsername())); if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.fail("用户名或密码错误"); } // 1. 生成JWT token String token = JWT.create() .withSubject(String.valueOf(user.getId())) .withExpiresAt(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) // 24小时 .sign(Algorithm.HMAC256(secretKey)); // 2. 将token存入Redis,设置过期时间与用户ID绑定 String redisKey = "user:token:" + user.getId(); redisTemplate.opsForValue().set(redisKey, token, 24, TimeUnit.HOURS); // 3. 返回token及用户基础信息 return Result.success(Map.of("token", token, "user", user)); }这里有三个必须理解的参数:
withExpiresAt():JWT自身过期时间,必须短于Redis中key的TTL,否则会出现token已过期但Redis未删,导致用户无法登出;redisTemplate.opsForValue().set(..., 24, TimeUnit.HOURS):Redis中key的TTL,必须等于或略长于JWT过期时间,本例设为相同值,确保双保险;Result.success()返回的Map结构,直接决定前端axios拦截器如何提取token——response.data.token是硬编码约定,若前端改用data.jwtToken,此处必须同步修改。
3.2 订单服务(OrderService):状态机驱动与库存扣减的强一致性设计
OrderService.java中createOrder()方法没有用@Transactional注解,而是手动控制事务:
public Result createOrder(OrderDTO dto) { // 1. 检查库存(先查后扣) List<CartItem> cartItems = cartService.getCartItems(dto.getUserId()); for (CartItem item : cartItems) { Integer stock = cuisineMapper.selectStock(item.getCuisineId()); // 查询菜品库存 if (stock < item.getQuantity()) { return Result.fail("菜品" + item.getName() + "库存不足"); } } // 2. 扣减库存(for循环内逐条更新) for (CartItem item : cartItems) { cuisineMapper.reduceStock(item.getCuisineId(), item.getQuantity()); // 执行UPDATE } // 3. 创建订单主表 Order order = new Order(); order.setUserId(dto.getUserId()); order.setStatus(OrderStatus.WAITING_PAYMENT.getCode()); orderMapper.insert(order); // 4. 创建订单明细 for (CartItem item : cartItems) { OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setCuisineId(item.getCuisineId()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } return Result.success(order.getId()); }这种写法暴露了原生Servlet项目的典型取舍:放弃Spring的声明式事务,换来自定义回滚能力。若第3步
orderMapper.insert()失败,库存已扣减却无订单,系统将出现脏数据。因此reduceStock()方法在CuisineMapper.java中必须加@Update("UPDATE cuisine SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}"),WHERE条件中的AND stock >= #{quantity}是最后防线——它保证UPDATE语句只在库存充足时生效,返回影响行数为0即表示扣减失败,此时可手动回滚前面的扣减操作。
3.3 支付服务(PayService):ZXing生成二维码与Redis缓存订单状态的协同机制
PayService.java的createOrderQrCode()是技术亮点:
public Result createOrderQrCode(Long orderId) { // 1. 查询订单并校验状态 Order order = orderMapper.selectById(orderId); if (order == null || !order.getStatus().equals(OrderStatus.WAITING_PAYMENT.getCode())) { return Result.fail("订单状态异常"); } // 2. 生成支付二维码内容(格式:pay://order/{orderId}) String content = "pay://order/" + orderId; // 3. 使用ZXing生成二维码 BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, 300, 300); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream); byte[] qrCodeBytes = outputStream.toByteArray(); // 4. 将二维码图片存入Redis,设置15分钟过期(支付超时) String redisKey = "qr:order:" + orderId; redisTemplate.opsForValue().set(redisKey, qrCodeBytes, 15, TimeUnit.MINUTES); // 5. 返回base64编码的图片数据 return Result.success(Base64.getEncoder().encodeToString(qrCodeBytes)); }关键参数解析:
MultiFormatWriter().encode(..., 300, 300):二维码尺寸为300×300像素,过小则手机扫码失败,过大则前端渲染卡顿;redisTemplate.opsForValue().set(..., 15, TimeUnit.MINUTES):Redis缓存时间必须与前端轮询支付状态的间隔匹配,cuisine-api.http中GET /api/pay/status?orderId={id}接口应每30秒调用一次,15分钟缓存确保覆盖完整支付周期;Base64.getEncoder().encodeToString():前端Vue2中可直接用<img :src="'data:image/png;base64,' + data"渲染,避免额外图片服务器请求。
4. 避坑指南:5个真实翻车现场与血泪修复方案
4.1 现象:启动Tomcat后访问/api/auth/login返回404,但web.xml中已配置Servlet映射
原因:IDEA的Artifact未正确打包web.xml,或web.xml中<url-pattern>路径与HTTP请求路径不一致。例如web.xml写<url-pattern>/auth/login</url-pattern>,但前端请求的是/api/auth/login。
解决:检查web/WEB-INF/web.xml中<servlet-mapping>的<url-pattern>是否以/api/开头;右键Project → “Open Module Settings” → “Artifacts” → 展开web exploded→ 确认WEB-INF/web.xml在输出路径中;若用IDEA 2022+,还需在“Project Structure” → “Modules” → “Sources”中,将web目录标记为“Resources”。
4.2 现象:AuthService.java中JWT.decode(token)抛InvalidSignatureException,提示签名不匹配
原因:application.properties中的jwt.secret值含空格或特殊字符(如#),被Properties加载器截断;或前后端传递token时,前端未在Header中加Bearer前缀(如Authorization: Bearer xxx)。
解决:将jwt.secret改为纯字母数字组合(如jwt.secret=MySecretKey2024);在ServletUtils.java的getTokenFromRequest()方法中,添加日志打印原始Header值:System.out.println("Raw Authorization: " + request.getHeader("Authorization"));,确认是否为Bearer xxx格式。
4.3 现象:OrderService.java创建订单时,cuisineMapper.reduceStock()执行后库存为负数
原因:MySQL的UPDATE cuisine SET stock = stock - #{quantity} WHERE id = #{id}未加AND stock >= #{quantity}条件,且数据库隔离级别为READ_COMMITTED,导致并发扣减时超卖。
解决:修改CuisineMapper.java中的SQL为@Update("UPDATE cuisine SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}");在createOrder()方法中,检查reduceStock()返回值:if (cuisineMapper.reduceStock(...) == 0) { return Result.fail("库存不足"); }。
4.4 现象:PayService.java生成二维码后,前端显示空白图片
原因:ZXing生成的BitMatrix未正确转换为PNG字节数组,或MatrixToImageWriter.writeToStream()抛IOException被静默吞掉。
解决:在createOrderQrCode()中添加异常捕获:
try { MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream); } catch (Exception e) { e.printStackTrace(); // 必须打印,否则看不到IO错误 return Result.fail("二维码生成失败"); }同时确认pom.xml中ZXing依赖为<artifactId>zxing-core</artifactId><version>3.5.0</version>,低版本不支持MultiFormatWriter。
4.5 现象:Redis连接正常,但redisTemplate.opsForValue().set()存入的数据在Redis CLI中GET为空
原因:redisTemplate默认使用JdkSerializationRedisSerializer序列化,存入的是二进制对象,Redis CLI中GET显示乱码;或redisTemplate未正确注入,代码中用了new RedisTemplate<>()导致空指针。
解决:在RedisConfig.java中配置字符串序列化器:
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); // 关键 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; }并在AuthService.java中确认@Autowired private RedisTemplate<String, Object> redisTemplate;已注入。
5. 前端对接与支付闭环验证:用Postman跑通从登录到支付成功的全链路
5.1 Postman模拟登录与获取token:验证JWT签发与Redis存储
在Postman中新建请求:
- Method: POST
- URL:
http://localhost:8080/api/auth/login - Body: raw → JSON
{ "username": "admin", "password": "123456" }发送后,响应体应为:
{ "code": 200, "message": "success", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": 1, "username": "admin", "role": "ADMIN" } } }此时立即打开Redis CLI执行
KEYS user:token:*,应看到类似user:token:1的key;再执行GET user:token:1,返回值应与响应体中的token完全一致。这是单点登录的基石验证——若Redis中无此key,说明AuthService.java的redisTemplate.set()未执行;若key存在但值不匹配,则JWT.create().sign()与redisTemplate.set()的token生成逻辑不一致。
5.2 构造订单并触发支付:用cuisine-api.http文件快速复现
cuisine-api.http是IntelliJ IDEA的HTTP客户端脚本,直接复制内容到.http文件中可一键发送:
### 登录获取token POST http://localhost:8080/api/auth/login Content-Type: application/json { "username": "user1", "password": "123456" } ### 创建订单(需替换token) POST http://localhost:8080/api/order/create Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Content-Type: application/json { "userId": 2 } ### 生成支付二维码(需替换orderId) GET http://localhost:8080/api/pay/qrcode?orderId=123 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...执行第三步时,响应体为base64字符串,将其粘贴到在线base64转图片网站(如https://base64.guru/converter/decode/image),即可看到二维码。扫描该二维码后,前端应跳转至支付页面,并开始轮询
/api/pay/status?orderId=123。此时在MySQL中执行SELECT * FROM order WHERE id = 123,初始状态为1(WAITING_PAYMENT);支付成功后,PayService.java的updatePaymentStatus()会将其改为2(PAID),这就是闭环验证。
5.3 数据库状态与Redis缓存联动验证:三步定位支付状态不同步
当支付完成后,前端轮询/api/pay/status仍返回“未支付”,需按顺序排查:
- 查MySQL订单状态:
SELECT status FROM order WHERE id = 123;若返回2,说明支付服务已更新DB; - 查Redis缓存:
GET qr:order:123若存在,说明二维码缓存未过期,但不影响状态查询;GET pay:status:123若存在且值为0,说明PayService.java中cachePaymentStatus()存入了错误状态; - 查
PayService.java的getPaymentStatus()方法:其逻辑应为
public Result getPaymentStatus(Long orderId) { // 先查Redis缓存 String cacheKey = "pay:status:" + orderId; Object cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return Result.success(Integer.parseInt(cached.toString())); // 直接返回缓存值 } // 缓存未命中,查DB Order order = orderMapper.selectById(orderId); int status = order == null ? 0 : order.getStatus(); // 写入缓存,过期时间设为5分钟(短于订单总生命周期) redisTemplate.opsForValue().set(cacheKey, String.valueOf(status), 5, TimeUnit.MINUTES); return Result.success(status); }若缓存key写成
pay:order:123而非pay:status:123,或set()时未传TimeUnit.MINUTES,将导致缓存永不过期,后续DB更新无法生效。
6. 生产就绪加固:给Servlet项目加上HTTPS、日志脱敏与慢SQL监控三道锁
6.1 Tomcat启用HTTPS:用自签名证书撑起TLS通道(非线上推荐,但开发必备)
PayService.java中二维码内容为pay://order/{id},若前端在HTTPS页面中请求HTTP接口,Chrome会阻止混合内容。必须让Tomcat支持HTTPS:
- 生成证书:
keytool -genkeypair -alias tomcat -keyalg RSA -keystore keystore.jks -storepass changeit -validity 3650; - 修改
conf/server.xml,取消注释HTTPS Connector:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true" scheme="https" secure="true" keystoreFile="${catalina.base}/conf/keystore.jks" keystorePass="changeit" clientAuth="false" sslProtocol="TLS" />- 重启Tomcat,访问
https://localhost:8443/api/auth/login,浏览器提示证书风险时点击“高级”→“继续访问”。
这步看似多余,但它是验证前端Vue2能否在HTTPS下正常调用后端API的唯一方式。若跳过,
axios会因CORS或混合内容被拦截,而错误日志只显示net::ERR_INSECURE_RESPONSE,排查成本极高。
6.2 日志脱敏:防止UserService.java中明文打印用户密码
UserService.java的register()方法含log.info("新用户注册:{}", user),若User对象未重写toString(),日志中将出现password: $2a$10$...。必须脱敏:
// 在User.java中重写toString() @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", role='" + role + '\'' + ", createTime=" + createTime + '}'; // 故意不打印password字段 }更彻底的做法是在Logback配置中过滤:
<filter class="ch.qos.logback.core.filter.EvaluatorFilter"> <evaluator> <expression> message.contains("password") || message.contains("Password") </expression> </evaluator> <onMatch>DENY</onMatch> </filter>这是GDPR和等保2.0的硬性要求,不是玄学优化。
6.3 慢SQL监控:用P6Spy拦截Mybatis-Flex的执行耗时
OrderService.java中createOrder()涉及多次DB操作,若某次cuisineMapper.reduceStock()耗时超2秒,需立刻告警。引入P6Spy:
pom.xml添加依赖:
<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency>src/main/resources/spy.properties配置:
modulelist=com.p6spy.engine.spy.P6SpyFactory,com.p6spy.engine.logging.P6LogFactory logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat customLogMessageFormat=%(currentTime)|%(executionTime)|%(category)|connection%(connectionId)|%(sqlSingleLine)jdbc.url改为jdbc:p6spy:mysql://localhost:3306/cuisine?...。
启动后,控制台将打印每条SQL的执行时间,如
1712345678901|1250|statement|connection1|UPDATE cuisine SET stock = stock - 2 WHERE id = 101 AND stock >= 2。超过1000ms即标红,这是我的硬性阈值——从那以后我每次上线新功能,都强制跑一遍OrderService的压测脚本,盯着P6Spy日志看有没有红色数字。希望帮到你。
本文还有配套的精品资源,点击获取