十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Java点餐系统源码实战:订单状态机、支付回调与Android联调

Java点餐系统源码实战:订单状态机、支付回调与Android联调 简介这是一份Java点餐系统完整源码包包含Android客户端与服务端代码面向学完Java基础、希望把知识点落到真实项目中的学习者。系统基于无线点餐Server/Client结构开发环境为MyEclipse 10 MySQL Tomcat可支持移动端点餐、桌台管理、服务端处理等典型流程。压缩包共296个文件仅4.94MB内含84个Java源文件、117个class编译文件、16个XML配置、7个JAR依赖库及APK安装包等结构清晰便于导入工程学习。目前已有129人学习下载。通过学习源码可梳理Android界面与逻辑层组织理解Servlet服务端、MySQL表设计及前后端联调适合课程设计或毕业设计参考。1. 订单状态从“待支付”变成“已支付”不只是 update 一条记录一个点餐系统源码工程摆在面前多数人第一反应是先把 Android 端跑起来看界面但真正决定这个项目能不能上线的是服务端那些看不见的状态流转。点餐系统的核心业务并不复杂菜单查询、桌台绑定、下单、支付回调、后厨接单数据量也不大复杂度和高并发关系不大难在订单状态不能被绕过去。这个标题里的“java点餐系统源码”属于典型的 Java 全栈学习项目服务端通常基于 Spring Boot 提供接口Android 端负责选菜和下单它正好覆盖了后端表设计、接口鉴权、客户端联调这一整套链路。适合正在找 Java 后端或 Android 初级岗位的开发者也适合想系统梳理一次订单业务的从业者。2. 服务端源码先理这两条线工程结构和订单状态机2.1 Maven 多模块怎么拆api、service、mapper 各管什么拿到服务端源码后我一般先看根目录的pom.xml确定它是单模块还是多模块。点餐系统规模不大但多数工程会按模块拆分这样接口定义、业务逻辑和数据库访问能各自独立编译也方便后面把 service 层单独打包部署。modules modulepoint-common/module modulepoint-mapper/module modulepoint-service/module modulepoint-api/module /modulespoint-common放统一返回体、异常枚举和工具类point-mapper对应 MyBatis 的 mapper 接口和 XMLpoint-service写业务逻辑point-api暴露 REST 接口并做参数校验。实际开发中有人会把 api 和 service 合并项目初期可以省事但后面每次加接口都要重新编译整个服务模块所以拆开更稳。确认模块结构后再去找application.yml里的数据源配置和端口配置。模块职责关键类或目录point-common响应体、异常、常量ApiResponse、ErrorCodepoint-mapper数据库访问OrderMapper、DishMapperpoint-service业务逻辑与事务OrderService、StockServicepoint-api接口入口与参数校验OrderController、MenuController2.2 订单表设计金额字段和状态字段最容易埋雷订单表是点餐系统的核心表设计时最常踩的坑有两个金额用float或double存储订单状态用字符串裸存。前者会产生精度丢失后者会让状态流转失去约束。常见的做法是金额用decimal(10,2)状态用tinyint并配一张状态枚举表或在代码里用枚举定义。CREATE TABLE orders ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号对外使用, table_id int(11) NOT NULL COMMENT 桌号, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3已上菜 4已完成 5已取消, total_amount decimal(10,2) NOT NULL COMMENT 订单实付金额, pay_time datetime DEFAULT NULL COMMENT 支付时间, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里order_no必须加唯一索引它是业务订单号对外展示给用户也用于支付回调时反查订单不能用数据库自增 id 代替。status字段用数字而不是字符串是为了索引体积和查询性能前端展示时再去映射文案。表结构里还要注意updated_at的自动更新这能帮你在排查问题时快速看出订单状态是否发生过变化。2.3 状态机用代码表达把非法流转挡在入口处订单状态如果没有约束Service 层很容易出现“已完成订单被再次取消”“已取消订单被支付回调改回已支付”这类问题。常见的做法是用枚举加一个合法的流转映射表来声明状态机所有状态变更都通过统一方法执行。public enum OrderStatus { PENDING_PAY(0), PAID(1), MAKING(2), SERVED(3), COMPLETED(4), CANCELLED(5); private final int code; private static final MapOrderStatus, SetOrderStatus TRANSITIONS new EnumMap(OrderStatus.class); static { TRANSITIONS.put(PENDING_PAY, EnumSet.of(PAID, CANCELLED)); TRANSITIONS.put(PAID, EnumSet.of(MAKING, CANCELLED)); TRANSITIONS.put(MAKING, EnumSet.of(SERVED)); TRANSITIONS.put(SERVED, EnumSet.of(COMPLETED)); TRANSITIONS.put(CANCELLED, EnumSet.noneOf(OrderStatus.class)); TRANSITIONS.put(COMPLETED, EnumSet.noneOf(OrderStatus.class)); } public boolean canTransitionTo(OrderStatus target) { return TRANSITIONS.get(this).contains(target); } }使用这套状态机时所有修改订单状态的代码都要先走canTransitionTo校验。这里注意PAID允许流转到CANCELLED是特例现实中已支付订单取消会触发退款流程回调处理逻辑会复杂很多如果是学习项目没有对接真实支付可以把这条流转去掉减少边界情况。同时要留意CANCELLED和COMPLETED是终态任何操作都不能把它们改回去这是状态机里最重要的约定。3. Android 端对接服务端接口Retrofit、拦截器与三个联调坑3.1 统一返回体和 Retrofit 接口定义服务端接口返回的结构如果不统一Android 端每个回调都要写一套错误判断。常见的做法是服务端统一返回{ code, message, data }Android 端建一个泛型类ApiResponseT来接收Retrofit 接口直接声明CallApiResponseDishVO由 Gson 完成反序列化。public class ApiResponseT { private int code; private String message; private T data; public boolean isSuccess() { return code 0; } } public interface MenuApi { GET(api/menu/list) CallApiResponseListDishVO getMenuList(); POST(api/order/create) CallApiResponseOrderVO createOrder(Body CreateOrderRequest request); }定义接口时要注意路径不要写死全域名baseUrl 统一放在 Retrofit 初始化处方便切换测试环境和生产环境。CreateOrderRequest里建议直接传orderNo、tableId和商品明细列表不要传后端生成的金额字段金额必须由服务端根据菜品单价重新计算否则用户改请求体就能改价格。3.2 OkHttp 拦截器统一处理 token 和调试日志token 刷新逻辑放在拦截器里比在每个请求回调里判断“请重新登录”要省事得多。点餐系统的 token 一般用登录接口换取的 JWT 或自定义字符串拦截器负责在请求头带上凭证并在收到 401 时尝试刷新 token 后重放一次请求。public class AuthInterceptor implements Interceptor { Override public Response intercept(Chain chain) throws IOException { Request original chain.request(); Request request original.newBuilder() .header(Authorization, Bearer tokenManager.getToken()) .header(Content-Type, application/json) .build(); Response response chain.proceed(request); if (response.code() 401 tokenManager.refresh()) { Request retry original.newBuilder() .header(Authorization, Bearer tokenManager.getToken()) .build(); return chain.proceed(retry); } return response; } }注意拦截器里不能用chain.proceed无限重试必须加一个重试次数标记否则 token 刷新接口本身也返回 401 时会形成死循环。调试日志也要单独写一个拦截器用BuildConfig.DEBUG控制是否打印请求体和响应体避免发布包把用户敏感信息打到 logcat 里。3.3 Android Studio 联调时必踩的 3 个坑用 Android Studio 跑点餐系统客户端最常见的问题是连不上开发机上的服务端。下面的表格是几个高频场景和对应处理方式。现象原因处理模拟器Connection refused模拟器里的 localhost 指向自身访问不到宿主机模拟器用http://10.0.2.2:8080访问宿主机真机用局域网 IP真机请求失败提示明文流量不允许Android 9 起默认禁止http明文请求在AndroidManifest.xml的 application 标签加android:usesCleartextTraffictrue跑了adb reverse后仍连不上adb reverse只对 USB 连接的设备生效无线调试无效确认adb devices状态是device并重新执行adb reverse tcp:8080 tcp:8080application android:usesCleartextTraffictrue android:networkSecurityConfigxml/network_security_config /applicationnetworkSecurityConfig比usesCleartextTraffic更细粒度可以只允许 debug 环境的 IP 走明文正式环境强制 HTTPS。文件里用domain-config声明可信任的域名注意cleartextTrafficPermittedtrue只对列出的域名生效。这里还要提醒一句不要为了省事直接把usesCleartextTraffic写成true留在生产包接口数据里包含用户手机号和订单信息明文传输在正式环境是安全隐患。4. 下单、支付回调、库存扣减三个最容易出 Bug 的位置4.1 下单幂等用户连点两次“提交订单”怎么办Android 端做按钮防抖只能挡住手快的用户挡不住网络重试或服务端接口重放。服务端要自己保证幂等。常见做法是客户端下单前先从服务端获取一个幂等 token下单接口要求携带该 token服务端收到后先校验 token 是否已被使用。public void createOrder(CreateOrderRequest request) { String idempotentKey request.getIdempotentToken(); // redis 里不存在才继续存在则直接返回上次结果 Boolean first redisTemplate.opsForValue() .setIfAbsent(order:idempotent: idempotentKey, 1, Duration.ofMinutes(10)); if (Boolean.FALSE.equals(first)) { throw new BusinessException(订单不要重复提交); } // 再走创建订单逻辑 }这里有几个参数要根据业务场景调整锁的过期时间一般设 10 到 30 分钟太短用户支付超时后重试会被拦截太长会堆积无用的 keysetIfAbsent必须配合过期时间一起用避免服务端异常时 key 永远不释放。除了 redis 方案还有数据库唯一索引方案即在订单表加一个idempotent_token唯一键重复插入直接报错。两种方案各有适用场景redis 方案对已有 redis 依赖的项目改动更小数据库方案则适合不想引入额外组件的学习项目。4.2 支付回调先查订单再更新状态别直接 update真正的点餐系统对接微信或支付宝时支付结果是由支付平台异步回调通知服务端的回调接口必须做到两件事验签和幂等。验签用官方 SDK 提供的工具类完成这里不展开密钥管理细节重点说幂等。PostMapping(/api/pay/notify) public String payNotify(RequestBody String notifyData) { // 1. 验签失败直接返回失败标记 // 2. 从 notifyData 解析出 orderNo 和实付金额 Order order orderMapper.selectByOrderNo(orderNo); if (order null) { return fail; } // 3. 已经是终态直接返回成功避免重复处理 if (order.getStatus() ! OrderStatus.PENDING_PAY.getCode()) { return success; } // 4. 金额不一致拒绝 if (!order.getTotalAmount().equals(payAmount)) { return fail; } orderMapper.updateStatus(orderNo, OrderStatus.PAID.getCode()); return success; }回调处理的核心是第 3 步状态非待支付时直接返回成功。因为支付平台会多次回调同一个订单如果每次收到回调都直接update状态已支付的订单可能被后续迟到回调覆盖成已支付问题不算严重但已取消的订单如果被改回已支付就会造成资金损失。金额校验要放在状态校验之后并且用equals而不是BigDecimal的比较的是地址而不是数值。回调接口返回给支付平台的字符串必须是success或fail这种明确标记任何异常都要返回fail让支付平台继续重试。4.3 库存扣减不要先查再减用条件更新点餐系统的菜品库存常在高峰期被并发扣减如果先select stock判断大于零再update stock stock - 1两个请求同时读到库存为 1 时会卖出两份。常见做法是把判断条件写进 update 语句利用数据库行锁保证原子性。UPDATE dish_stock SET stock stock - 1 WHERE dish_id #{dishId} AND stock 0;这条语句执行后返回的影响行数如果为 0 说明库存不足或菜品不存在。注意这里不适合用悲观锁SELECT FOR UPDATE点餐系统场景下并发冲突并不频繁悲观锁会占用数据库连接反而增加响应时间。更复杂一点的做法是预扣库存加超时释放用户下单时先扣减可售库存支付超时后再回补这对点餐这种强时效业务更有意义但也会引入分布式事务问题。学习项目做到条件更新这一层就足够应付大多数场景面试时能说清影响行数的判断逻辑即可。5. 本地跑通的最小命令和配置分离技巧拿到源码工程先在本地把服务端和客户端跑起来建议用下面这组命令快速验证。# 服务端启动dev profile 指向本地数据库 mvn clean package -DskipTests mvn spring-boot:run -Dspring-boot.run.profilesdev # Android 端连接开发机服务端 adb reverse tcp:8080 tcp:8080服务端启动后先用浏览器或 Postman 请求一次菜单接口确认返回 JSON 正常再去启动 Android 应用。这一步能直接把问题定位到服务端还是客户端避免两边同时排查。接口自测时可以关注响应里的code字段是否统一为 0很多源码工程只在成功路径上返回了标准结构异常路径直接抛了 500 错误页这是需要补全的地方。最后建议把服务端的配置拆成application-dev.yml和application-prod.yml两份用spring.profiles.active切换。点餐系统源码的常见问题是数据库连接、七牛云存储 key、支付密钥全部写在默认配置里一旦要用到真实环境就必须改代码而配置分离能让你只通过启动参数区分环境。排查线上问题时还能用Actuator暴露的env端点确认当前生效的配置项但记得生产环境要为该端点加上权限控制避免数据库地址和密钥泄露。本文还有配套的精品资源点击获取
返回列表