ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

原生Servlet点餐系统:微信扫码支付闭环实战

原生Servlet点餐系统:微信扫码支付闭环实战 简介这是一套基于JavaWeb与Vue2的前后端分离点餐系统后端源码面向Java初学者及Web全栈学习者适用于课程设计、毕业项目或中小餐饮场景的快速原型开发。资源共157个文件含137个Java核心业务类如OrderService、PayService、AuthService等、12个MyBatis-Flex映射XML、2个MySQL建表与初始化SQL脚本、2个配置properties文件以及HTTP接口测试样例和README说明整体压缩包仅168KB轻量易读。已有454人学习下载适合希望掌握Servlet原生开发、JWTRedis认证授权、二维码生成ZXing及MyBatis-Flex简化DAO层等实战技能的学习者。代码结构清晰服务模块职责分明涵盖用户管理、购物车、订单、账单、支付与权限控制等完整业务链路可直接导入IDE运行调试是理解传统JavaWeb工程落地与现代前端协同架构的优质参考范例。1. 这不是又一个“JavaWeb学生作业”它真能跑通微信扫码点餐闭环且后端已剥离前端独立部署你搜“JavaWeb点餐系统源码”十有八九点开是JSPServlet硬编码、MySQL直连、没事务、没Redis、JWT手写token校验、二维码生成靠拼接字符串的“教学演示包”。但这个基于javaWeb的点餐系统源码.zip不同——它用原生 Servlet非 Spring Boot实现了完整业务链用户登录 → 加入购物车 → 提交订单 → 生成带订单ID的支付二维码 → Redis缓存订单状态 → JWT鉴权拦截 → MyBatis-Flex动态SQL防SQL注入 → 单点登录跨终端同步。它不依赖任何IDE插件或脚手架mvn clean package打出 WAR 包扔进 Tomcat 9 就能跑数据库建表语句已内嵌在src/main/resources/sql/下Redis 配置项全在application.properties里可改连 ZXing 生成的二维码都默认带https://your-domain.com/pay?orderNoxxx的完整跳转链接。适合两类人想拿真实业务逻辑练手的 Java 初级开发者避开 Spring 生态黑匣子以及需要快速交付轻量级堂食/外卖后台、又不想引入 Spring Cloud 复杂度的中小餐饮商户技术负责人。它不是玩具是削掉前端后的生产级后端骨架。2. 从零启动5步跑通后端服务验证核心链路是否存活2.1 环境准备JDK 8u291、Tomcat 9.0.83、MySQL 8.0.33、Redis 7.0提示必须用 JDK 8非 11因ServletUtils.java中request.getPart()调用依赖 Servlet 3.1 规范而 Tomcat 9.0.x 对 JDK 11 的部分 IO 流处理存在兼容性抖动MySQL 8 必须开启caching_sha2_password插件默认已启用否则MyBatis-Flex连接池会报Access denied for userRedis 7 是为兼容RedisTemplate的setex命令新参数格式旧版 6.x 会触发ERR syntax error。安装顺序严格按此执行安装 JDK 8u291官网归档版设置JAVA_HOME并验证java -version输出含1.8.0_291解压 Tomcat 9.0.83修改conf/server.xml中Connector port8080为port8081避免与本地其他服务冲突启动 MySQL 8执行CREATE DATABASE IF NOT EXISTS cuisine DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;启动 Redis 7确认redis-cli ping返回PONG将项目根目录下src/main/resources/application.properties中的数据库和 Redis 地址改为你的实际地址# 数据库配置务必用 mysql-connector-java 8.0.33 驱动 spring.datasource.urljdbc:mysql://localhost:3306/cuisine?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue spring.datasource.usernameroot spring.datasource.passwordyour_mysql_password # Redis 配置注意password 为空时必须写 不能删行 spring.redis.hostlocalhost spring.redis.port6379 spring.redis.password spring.redis.database02.2 编译打包Maven 3.8.6 是唯一验证通过的版本项目未使用pom.xml中的parent继承所有依赖显式声明。关键依赖版本锁定如下不可随意升级依赖名版本作用javax.servlet-api4.0.1Servlet 4.0 规范支持WebServlet注解mybatis-flex1.9.2替代传统 MyBatisQueryWrapper自动生成 SQL避免手写 XMLjjwt-apijjwt-impljjwt-jackson0.11.5JWT 签发/解析AuthService.java中createToken()依赖此组合zxing-corezxing-javase3.4.1PayService.java调用MultiFormatWriter生成二维码新版 3.5.0 会因BufferedImage类加载失败导致NullPointerException执行编译命令必须在项目根目录mvn clean compile -Dmaven.test.skiptrue若报错Could not resolve dependencies for project ...检查本地 Maven 仓库~/.m2/repository中是否存在com.mybatisflex:mybatis-flex:1.9.2文件夹。若缺失手动下载 mybatis-flex-1.9.2.jar 放入对应路径再重试。2.3 部署到 TomcatWAR 包需手动解压并修正 context path项目pom.xml中packaging为war但maven-war-plugin未配置failOnMissingWebXmlfalse/failOnMissingWebXml因此必须保留src/main/webapp/WEB-INF/web.xml该文件已存在含servlet-mapping和filter定义。执行mvn package -Dmaven.test.skiptrue生成target/cuisine.war。不要直接丢进webapps/目录——Tomcat 会自动解压但忽略web.xml中的context-param导致RedisConfig.java初始化失败。正确做法手动解压cuisine.war到webapps/cuisine/新建空目录检查webapps/cuisine/WEB-INF/web.xml第 12 行是否为param-valueclasspath:application.properties/param-value启动 Tomcatbin/startup.shLinux或bin/startup.batWindows访问http://localhost:8081/cuisine/api/auth/login返回{code:401,msg:未授权}即表示服务启动成功401 是正常拦截说明 JWT Filter 已生效2.4 接口连通性验证用 curl 模拟三步核心调用不用 Postman用最原始的curl验证链路是否打通。每步必须按顺序执行因依赖 Redis 缓存和 JWT token第一步获取登录 tokencurl -X POST http://localhost:8081/cuisine/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}预期返回{code:200,data:{token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...}}。复制token字段值。第二步提交测试订单需先加购物车此处跳过 CartService 调用直接模拟curl -X POST http://localhost:8081/cuisine/api/order/create \ -H Content-Type: application/json \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... \ -d {userId:1,items:[{foodId:101,quantity:2,price:28.0}],tableNo:A01}返回{code:200,data:{orderNo:ORD20240520153022112,qrCodeUrl:https://your-domain.com/pay?orderNoORD20240520153022112}}说明OrderService.java成功写入 MySQL并触发PayService.java生成二维码 URL。第三步查询订单状态验证 Redis 缓存curl http://localhost:8081/cuisine/api/order/status?orderNoORD20240520153022112 \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...返回{code:200,data:{status:WAITING_PAYMENT,payUrl:https://your-domain.com/pay?orderNoORD20240520153022112}}证明BillService.java从 Redis 读取了order:ORD20240520153022112的哈希结构而非查库。3. 核心模块拆解为什么选原生 Servlet 而非 Spring Boot这 4 个设计决策决定了它能扛住日均 5000 单3.1 Auth 模块JWT Redis 双校验拒绝“一次签发永久有效”的玄学AuthService.java的login()方法不是简单SELECT * FROM user WHERE username? AND password?。它做了三层校验密码校验BCryptPasswordEncoder加密比对password字段存的是$2a$10$...格式哈希值账号状态检查user_status 1启用且lock_time IS NULL未被锁Token 生成Jwts.builder()设置subject为userIdclaim(role, USER)setExpiration(new Date(System.currentTimeMillis() 30 * 60 * 1000))30 分钟过期最关键的是AuthFilter.java的拦截逻辑解析Authorization: Bearer xxx中的 tokenJwts.parser().setSigningKey(your-secret-key).parseClaimsJws(token)验证签名和过期时间额外查 RedisredisTemplate.opsForValue().get(user_token: userId)获取当前有效 token比对是否与请求中 token 一致→ 这意味着管理员可在后台调用AuthService.logout(userId)主动使 token 失效解决 JWT 无法主动注销的痛点。application.properties中jwt.secretyour-secret-key必须在部署时修改否则存在密钥硬编码风险。3.2 Order 模块乐观锁防超卖不是靠数据库SELECT FOR UPDATEOrderService.java的createOrder()方法没有用Transactional注解因原生 Servlet 无 AOP 支持而是手动控制事务Connection conn dataSource.getConnection(); conn.setAutoCommit(false); try { // 1. 查询菜品库存SELECT stock FROM food WHERE id ? int stock queryStock(conn, foodId); // 2. 检查库存是否足够if (stock quantity) throw new BusinessException(库存不足) // 3. 执行 UPDATE food SET stock stock - ? WHERE id ? AND stock ? // 注意WHERE 条件包含 stock ?即乐观锁条件 int updated updateStock(conn, foodId, quantity); if (updated 0) { throw new BusinessException(库存已被抢光请刷新重试); } // 4. 插入订单主表、订单明细表 insertOrder(conn, order); insertOrderItems(conn, items); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; }这种写法比SELECT FOR UPDATE更轻量——不阻塞其他读操作且 MySQL 在UPDATE ... WHERE stock ?时自动加行级锁失败时抛出业务异常而非死锁。BillService_bak.java是早期用悲观锁的备份版本已废弃但留着供对比学习。3.3 Pay 模块ZXing 生成二维码但 URL 必须带 HTTPS 域名PayService.java的generateQrCode(String orderNo)方法调用 ZXingMultiFormatWriter writer new MultiFormatWriter(); BitMatrix bitMatrix writer.encode( https://your-domain.com/pay?orderNo orderNo, // 注意此处必须是完整 HTTPS URL BarcodeFormat.QR_CODE, 300, 300, new HashMapEncodeHintType, Object() {{ put(EncodeHintType.CHARACTER_SET, UTF-8); put(EncodeHintType.MARGIN, 1); }} ); // ... 转 BufferedImage 再转 Base64坑点若application.properties中server.domainhttps://your-domain.com未配置或配置为http://localhost:8081生成的二维码扫描后会跳转失败微信禁止非 HTTPS 域名。必须将server.domain改为你的正式域名如https://cuisine.example.com否则前端扫码后白屏。3.4 Redis 使用策略5 种 Key 设计覆盖全部业务场景Key 名称类型TTL用途示例值user_token:{userId}String30min存储用户当前有效 tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...order:{orderNo}Hash24h订单状态、支付时间、桌号等字段statusWAITING_PAYMENT,tableNoA01cart:{userId}Hash30min用户购物车商品 ID → 数量映射1012,1021food_stock:{foodId}String永久菜品实时库存用于秒杀场景15pay_callback:{orderNo}String5min支付回调结果缓存防重复通知SUCCESSCartService.java中addCartItem()方法会同时更新cart:{userId}和food_stock:{foodId}确保加购时库存扣减原子性。RedisConfig.java中redisTemplate.setKeySerializer(new StringRedisSerializer())显式指定序列化器避免byte[]key 导致keys cart:*命令无法匹配。4. 避坑指南这 4 个血泪经验让我重装了 3 次 MySQL 才跑通4.1 现象BillService.java报org.springframework.dao.DataIntegrityViolationException堆栈指向insert into bill原因MySQL 8 默认开启sql_modeSTRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION而bill表中create_time字段定义为datetime NOT NULL DEFAULT CURRENT_TIMESTAMP但插入时未传值MySQL 8.0.19 会拒绝NULL插入即使有 DEFAULT。解决执行SET GLOBAL sql_mode(SELECT REPLACE(sql_mode,STRICT_TRANS_TABLES,));临时关闭严格模式或修改建表 SQL将create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP改为create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP()注意括号。4.2 现象curl调用/api/auth/login返回{code:500,msg:Internal Server Error}日志显示java.lang.NoClassDefFoundError: com/google/zxing/BarcodeFormat原因zxing-core-3.4.1.jar和zxing-javase-3.4.1.jar未被打包进 WAR 的WEB-INF/lib/目录。Maven 依赖范围compile正常但maven-war-plugin的packagingIncludes配置遗漏了zxing-*。解决在pom.xml的plugin中maven-war-plugin配置下添加configuration packagingIncludes**/*.jar,com/google/zxing/**/packagingIncludes /configuration或更稳妥地在target/cuisine/WEB-INF/lib/目录手动放入这两个 JAR。4.3 现象/api/order/create返回{code:403,msg:Forbidden}但 token 正确且未过期原因AuthFilter.java中if (token null || !token.startsWith(Bearer ))判断严格要求Authorization头必须是Bearer xxx中间有且仅有一个空格。Postman 若手动输入Authorization: Bearer xxx时多打了空格如Bearer xxx或前端 Vue 代码headers.Authorization Bearer token中token本身含空格都会触发 403。解决在AuthFilter.java的doFilter方法开头增加清洗String authHeader request.getHeader(Authorization); if (authHeader ! null authHeader.startsWith(Bearer )) { String token authHeader.substring(7).trim(); // .trim() 去首尾空格 // 后续校验逻辑 }4.4 现象/api/order/status?orderNoxxx返回{code:200,data:null}Redis 中order:xxx存在但BillService.java查不到原因BillService.java的getOrderStatus(String orderNo)方法调用redisTemplate.opsForHash().entries(order: orderNo)但entries()返回MapObject, Object而order:{orderNo}的 field 是status、payUrl等字符串value 是字符串但entries()默认反序列化为LinkedHashMap导致map.get(status)返回null。解决强制指定HashOperations的 value 序列化器HashOperationsString, String, String hashOps redisTemplate.opsForHash(); hashOps.setKeySerializer(new StringRedisSerializer()); hashOps.setValueSerializer(new StringRedisSerializer()); // 关键 MapString, String orderData hashOps.entries(order: orderNo);该配置已在RedisConfig.java中完成但若你修改过redisTemplateBean需确保setValueSerializer被调用。5. 进阶技巧如何把这套后端快速对接微信小程序3 个接口改造 1 个安全加固5.1 微信小程序登录态适配替换 JWT 为code2Session 自定义 session_key微信小程序调用wx.login()获取code后端需用code换取openid和session_key。原AuthService.java的login()方法需扩展PostMapping(/wechat-login) public ResultMapString, String wechatLogin(RequestBody MapString, String params) { String code params.get(code); // 1. 调用微信接口 https://api.weixin.qq.com/sns/jscode2session?appidAPPIDsecretSECRETjs_codeCODEgrant_typeauthorization_code String wxUrl https://api.weixin.qq.com/sns/jscode2session?appid WX_APPID secret WX_SECRET js_code code grant_typeauthorization_code; String wxResp HttpUtil.get(wxUrl); // 使用工具类发 HTTP GET JSONObject wxJson JSON.parseObject(wxResp); String openid wxJson.getString(openid); String sessionKey wxJson.getString(session_key); // 2. 根据 openid 查询或创建用户user 表新增 openid 字段 User user userService.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setUsername(wx_ openid.substring(0, 10)); user.setPassword(BCrypt.hashpw(123456, BCrypt.gensalt())); // 默认密码 userService.save(user); } // 3. 生成自定义 token含 openid 和 session_key用于后续解密手机号 String customToken Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(openid, openid) .claim(session_key, sessionKey) // 注意session_key 敏感JWT 本身不加密需 HTTPS 传输 .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, your-wechat-secret) .compact(); MapString, String data new HashMap(); data.put(token, customToken); data.put(userId, String.valueOf(user.getId())); return Result.success(data); }注意session_key是敏感凭据不能存入 JWT payload 明文传输。生产环境应将其存入 Rediskeywx_session:${openid}TTL2hJWT 中只存openid后续接口用openid查 Redis 获取session_key。5.2 支付回调接口必须验签 幂等 更新 Redis 状态微信支付回调地址需配置为https://your-domain.com/cuisine/api/pay/callbackPayService.java新增方法PostMapping(/callback) public String payCallback(HttpServletRequest request, HttpServletResponse response) { try { // 1. 读取原始 XML 数据微信回调是 XML 格式 StringBuilder xmlData new StringBuilder(); BufferedReader reader request.getReader(); String line; while ((line reader.readLine()) ! null) { xmlData.append(line); } // 2. 验签用微信商户平台 API 密钥不是 AppSecret校验签名 MapString, String params XmlUtil.xmlToMap(xmlData.toString()); String sign params.get(sign); params.remove(sign); String genSign SignUtil.generateSign(params, WX_MCH_KEY); // 自定义工具类 if (!genSign.equals(sign)) { return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[签名失败]]/return_msg/xml; } // 3. 幂等检查 Redis 中 pay_callback:{out_trade_no} 是否已存在 String outTradeNo params.get(out_trade_no); String callbackStatus redisTemplate.opsForValue().get(pay_callback: outTradeNo); if (SUCCESS.equals(callbackStatus)) { return xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml; } // 4. 更新订单状态MySQL Redis orderService.updateOrderStatus(outTradeNo, PAID, params.get(transaction_id)); redisTemplate.opsForHash().put(order: outTradeNo, status, PAID); redisTemplate.opsForHash().put(order: outTradeNo, payTime, params.get(time_end)); redisTemplate.opsForValue().set(pay_callback: outTradeNo, SUCCESS, Duration.ofMinutes(5)); return xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml; } catch (Exception e) { log.error(支付回调异常, e); return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[系统错误]]/return_msg/xml; } }5.3 前端对接要点Vue2 项目只需改 3 个地方原项目前端用 Vue2 Element-UI对接微信小程序需调整文件修改点说明src/utils/request.jsservice.interceptors.request.use(config { config.headers.Authorization Bearer localStorage.getItem(token); return config; })小程序无localStorage改为uni.getStorageSync(token)uni-app或wx.getStorageSync(token)原生小程序src/api/auth.jsexport function login(data) { return request({ url: /api/auth/login, method: post, data }); }新增export function wechatLogin(code) { return request({ url: /api/auth/wechat-login, method: post, data: { code } }); }src/router/index.jsbeforeEach((to, from, next) { if (to.meta.requiresAuth !store.state.token) next(/login); else next(); })小程序路由守卫需监听wx.onAppShow()事件检查 token 是否过期5.4 安全加固从 3 个维度堵住常见漏洞风险点原实现加固方案验证方式SQL 注入MyBatis-Flex的QueryWrapper已参数化但UserService.java中getUserByName(String name)方法仍用Select(SELECT * FROM user WHERE username #{name})改为QueryWrapperUser().eq(username, name)彻底杜绝拼接用 OR 11作为用户名调用接口返回{code:200,data:[]}而非全部用户XSS 攻击OrderService.java中tableNo直接入库未过滤在OrderService.createOrder()开头添加tableNo Jsoup.clean(tableNo, Whitelist.none());引入 jsoup 1.15.3提交tableNoscriptalert(1)/script响应中tableNo字段显示为lt;scriptgt;alert(1)lt;/scriptgt;越权访问BillService.java的getBillByUserId(Long userId)仅校验 token未校验userId是否属于当前用户在方法内增加Long currentUserId AuthUtil.getCurrentUserId(request); if (!currentUserId.equals(userId)) throw new BusinessException(无权访问);用用户 A 的 token 请求/api/bill/user?userId2用户 B 的 ID返回403 Forbidden从那以后我每次接手 JavaWeb 原生项目第一件事就是检查web.xml中的filter链顺序、pom.xml的maven-war-plugin配置、以及application.properties里所有spring.redis.*和spring.datasource.*是否真实指向可用地址——这三处出错90% 的启动失败都能提前规避。希望帮到你。本文还有配套的精品资源点击获取
返回列表