
简介这是一套基于无线点餐系统server/client架构的Java实战项目源码适合学过Java基础、希望通过完整项目巩固知识的学习者也适用于课程设计或移动开发入门练手。系统采用Android客户端与服务器端分离设计开发环境为MyEclipse 10 MySQL Tomcat覆盖从移动端到后端再到数据库的完整链路。压缩包内共296个文件、约4.94MB以84个Java源码、117个class编译产物、38张jpg界面切图、16个XML配置和7个依赖JAR为主另附可直接安装的APK包便于快速体验。已有129人学习下载在入门级Android项目中具备一定参考价值。通过研究该项目可理清登录验证、菜单与桌台管理、订单提交等功能的实现脉络掌握Android与服务端交互、MySQL读写、XML布局等实用技能是一份不错的实战练手素材。1. 无线点餐系统源码一套能跑通完整点餐流程的 Java 练手项目“无线点餐系统”这个名字听起来像教科书里的名词但把它和 Java 源码、Android 客户端、服务端放到一块儿就变成了一个特别适合 Java 学习者拆解的实战项目。整套系统做的事一句话能说清服务员拿着 Android 设备给顾客开台、点菜、下单、结账客户端负责交互服务端用 Servlet MySQL 存数据中间靠 HTTP 请求把两端连起来。很多人学完 Java 基础后卡在“语法都会但不知道能做什么”这套源码恰好填补这个空白——代码量不大但 Activity 跳转、网络请求、数据库访问、Servlet 映射这些核心技能点全都覆盖到。适合刚学完 Java 语法想动手做东西的人也适合课设需要一套 server-client 源码做改造的人。2. 服务端设计与业务链路从桌台、菜单到结账的服务端逻辑2.1 业务模型登录、开台、点单、结账四条主链路无线点餐和普通外卖点餐最大的区别是“桌台”。顾客不是配送地址而是桌号。整套系统按我的理解可以拆成四条链路登录链路服务员在 LoginActivity 输入工号和密码服务端验证合法后进入 MainMenuActivity 主菜单。登录这件事在服务端一般会写进 Session后续请求靠 Session 判断身份。开台/换桌链路顾客落座后被分配一个桌台TableProvider 负责提供桌台列表和维护桌台状态空闲、占用、待结账客户端拿到桌台后执行开台操作。点单链路菜单数据由 MenuProvider 提供点菜时每加一道菜就生成一条订单明细最后在 OrderActivity 提交整单。结账链路顾客喊服务员结账触发 PayServlet把当前桌台的订单金额汇总并修改订单状态。这里最值得研究的是第三条和第四条链路的衔接。点单和结账牵涉到状态变化很多初学者把桌子状态和订单状态混在一起结果出现“桌台还没结账就能被别人开台”的逻辑硬伤。这个项目本身就是练手级别服务端逻辑不会写得很复杂但 Provider、Servlet、DAO 的分层已经能看出正规项目的影子。建议拿到源码先顺着 UI 上的按钮把四条链路在代码里各走一遍。2.2 数据模型设计MySQL 中菜单、桌台、订单三张核心表服务端的数据库一般只用三张核心表加一张管理员表。如果压缩包里没有现成的建表 SQL按照这个项目常见的表结构用下面这段 SQL 初始化即可CREATE DATABASE IF NOT EXISTS wireless_order DEFAULT CHARACTER SET utf8; USE wireless_order; -- 菜单表存菜名、价格、分类、上下架状态 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 菜品名称, price DECIMAL(6,2) NOT NULL COMMENT 单价, category VARCHAR(16) COMMENT 分类热菜/凉菜/主食/饮品, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ); -- 桌台表table_no 是物理桌号status 是业务状态 CREATE TABLE table_info ( id INT PRIMARY KEY AUTO_INCREMENT, table_no VARCHAR(8) NOT NULL COMMENT 桌号, status TINYINT DEFAULT 0 COMMENT 0空闲 1占用 2待结账, create_time DATETIME ); -- 订单表一条记录一个菜同一桌多次点菜靠 batch_no 区分 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, table_id INT COMMENT 桌台ID, dish_id INT COMMENT 菜品ID, quantity INT DEFAULT 1 COMMENT 数量, total DECIMAL(8,2) COMMENT 本单金额, status TINYINT DEFAULT 0 COMMENT 0未结账 1已结账, order_time DATETIME );三条 SQL 注释里藏着两个关键设计点。第一订单表设计成“一行一个菜”同一个桌台点上五个菜就是五条记录这样 PayServlet 结账时只要按 table_id 和 status 汇总就能算出总金额。第二实际课设场景里建议给 orders 表加一个 batch_no 批量号字段——如果一桌顾客先点一轮菜服务生记成待结账状态顾客又加菜PayServlet 应该只结当前待结账的那一批而不是把所有历史记录全算上。常见做法就是每次开台或加菜时生成新的批号结账按批号过滤。2.3 MenuProvider 与 TableProvider服务端数据访问的分工在源码包里看到 MenuProvider.class 和 TableProvider.class 这两个类名时很容易误以为它们是 Android 端的类。其实从命名和项目结构看它们更可能是服务端的数据访问辅助类把 dish 表、table_info 表的读写封装成方法供上游 Servlet 调用。常见写法长这样// MenuProvider.java 服务端菜单数据访问类 public class MenuProvider { private Connection getConn() throws Exception { // 驱动名和连接串要匹配你本机的 MySQL 版本 Class.forName(com.mysql.jdbc.Driver); return DriverManager.getConnection( jdbc:mysql://localhost:3306/wireless_order?characterEncodingutf8, root, 123456); } // 查全量菜单返回 ListDish由 Servlet 转成客户端可识别的格式 public ListDish getMenu() { ListDish list new ArrayList(); String sql SELECT id, name, price, category FROM dish WHERE status 1; try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Dish d new Dish(); d.setId(rs.getInt(id)); d.setName(rs.getString(name)); d.setPrice(rs.getDouble(price)); d.setCategory(rs.getString(category)); list.add(d); } } catch (Exception e) { e.printStackTrace(); } return list; } }这个类里的 getConn() 是每次调用都重新建立连接练手项目可以这么写但生产环境千万别这么干——频繁开关连接非常损耗性能一般要用连接池。getMenu() 只做一件事把表里的字段读出来装进 Dish 对象。注意 SQL 里加了 status 1 条件下架的菜不会出现在客户端。TableProvider 的逻辑和 MenuProvider 基本对称只是把 SQL 换成 table_info 表的查询与更新。关键点在于服务端对 Android 客户端来说是一个“黑匣子”。客户端不知道 MySQL 怎么写只认 URL 和返回的字符串服务端把数据库查出来包装成客户端能识别的格式返回。很多新手图省事把 SQL 写在 Activity 里这个案例把 Provider 单独拆出来就是为了改动菜单表字段时只动 Provider不用去碰界面代码。这也是 Java Web 项目分层最基本的理由。3. Android 客户端拆解四大 Activity 与点餐提交的网络请求3.1 从 APK 与构建产物还原客户端工程结构拿到压缩包先别急着导入编辑器先看包里躺了哪些文件。WirelessOrder_Client.apk 是已经编译好的安装包可以直接扔进模拟器或者旧 Android 手机上看效果resources.ap_ 是 Android 构建过程用 aapt 打包资源后产生的中间文件jarlist.cache 是 MyEclipse/Eclipse 编译 Web 工程时生成的 jar 清单。看到这三个东西基本可以判断这是一个“客户端已经编译 服务端源码工程”的混合包。客户端编译产物里有 MainMenuActivity.class、OrderActivity.class、UpdateActivity.class、LoginActivity.class 四个类文件。这基本就是客户端的四个主界面入口按命名习惯可以推断LoginActivity登录界面负责身份验证MainMenuActivity主菜单界面开台、点菜、结账的入口都集中在这OrderActivity点餐下单界面负责选菜与数量管理UpdateActivity更新界面负责从服务端拉取最新菜单或桌台状态如果是用 Android Studio 重新打开这套客户端源码要注意两点老项目通常是 Eclipse ADT 时代建的工程没有 Gradle 配置新建项目后把 src、res、AndroidManifest.xml 拷进去比直接 import 靠谱另外 SDK 版本不要选太高compileSdk 和 targetSdk 别按默认的 34 去设否则老代码里已经废弃的 API 会直接编译失败。3.2 LoginActivity 与 MainMenuActivity登录跳转与主界面入口登录页的逻辑在客户端里算最直观的。输入框拿到工号和密码拼成 POST 参数发到服务端服务端返回一个标志文本客户端解析到 success 之后用 Intent 显式跳转到 MainMenuActivity。示意代码如下// LoginActivity.java 登录按钮点击事件 public void doLogin(View v) { String account usernameEdit.getText().toString().trim(); String password pwdEdit.getText().toString().trim(); if (account.length() 0 || password.length() 0) { Toast.makeText(this, 工号和密码不能为空, Toast.LENGTH_LONG).show(); return; } // 常见做法是走 HttpURLConnection参数拼接成表单格式 String result HttpHelper.post(LOGIN_URL, account account pwd password); if (success.equals(result)) { // 跳转主菜单并销毁登录页防止按返回键回到登录 Intent intent new Intent(LoginActivity.this, MainMenuActivity.class); startActivity(intent); finish(); } else { Toast.makeText(this, 用户名或密码错误, Toast.LENGTH_LONG).show(); } }这里两个细节新手容易漏。第一跳转后必须调用 finish()否则用户按返回键会退回登录页感觉像翻车第二LOGIN_URL 的 host 部分如果是模拟器要写成 10.0.2.2真机要写电脑局域网 IP不能写 localhost。MainMenuActivity 起来之后会加载桌台列表和菜单缓存桌台数据来自 TableProvider 对应的服务端接口客户端拿到后渲染到列表控件里。3.3 OrderActivity 点餐提交数据拼装与子线程请求OrderActivity 的难点在数据拼装而不在界面。界面上一般有三个区菜单分类、菜品列表、已选清单。用户每点一个菜就加入已选清单提交时要把“桌台号、菜品ID、数量”这几个信息拼成服务端能识别的格式再用子线程发出去。示意代码// OrderActivity.java 提交订单到服务端 private void submitOrder(int tableId, ListSelectedDish dishList) { // 拼装参数格式是 dishId:quantity多道菜用逗号分隔 StringBuilder sb new StringBuilder(); sb.append(table_id).append(tableId).append(dishes); for (int i 0; i dishList.size(); i) { if (i 0) sb.append(,); sb.append(dishList.get(i).dishId).append(:) .append(dishList.get(i).quantity); } String url BASE_URL /OrderServlet?typeadd; // 子线程发请求主线程直接网络操作会抛 NetworkOnMainThreadException new Thread(() - { String resp HttpHelper.post(url, sb.toString()); runOnUiThread(() - Toast.makeText(OrderActivity.this, resp, Toast.LENGTH_LONG).show()); }).start(); }有个细节要注意参数用逗号和冒号分隔服务端要用 split 拆两次。这种紧凑格式在练手项目里很常见但可读性差毕业设计答辩时容易被问“为什么不用 JSON”。建议改造成 JSON 格式——用 JSONObject 拼一个数组服务端用解析库读代码反而更清爽。另外new Thread 里拿到结果后要切回主线程更新 UI直接用 runOnUiThread 就行。如果哪个按钮点了没反应先看看是不是 UI 更新写在了子线程里。4. 部署复现MyEclipse 10 导入、数据库初始化与服务端接口测试4.1 服务端导入 MyEclipse 10 并部署到 Tomcat这个项目的开发环境是 myeclipse10 mysql tomcat。导入步骤打开 MyEclipse 10File - Import - General - Existing Projects into Workspace选择服务端源码目录等构建完成。部署前先确认三件事JDK 版本MyEclipse 10 默认支持 JDK 1.6/1.7如果用 JDK 8 及以上编译报一堆 source 1.3 之类错误时到 Window - Preferences - Java - Compiler 里把编译级别改成 1.7。Tomcat 版本在 Servers 视图里添加 Tomcat 6/7不要选 Tomcat 8/9老项目用的 Servlet API 版本比较旧。web.xml 是否存在没有的话项目无法被 Tomcat 识别成 Web 应用。配置好 Tomcat 后右键项目 - Run As - MyEclipse Server Application选 Tomcat 启动。日志里出现 Server startup in xxx ms 就说明服务端起来了。这时候先别急着开客户端直接用浏览器访问 http://localhost:8080/工程名/能看到页面或者接口返回说明部署成功。如果 404多半是部署到了错误的 webapps 目录或者工程名大小写没对上。4.2 MySQL 初始化与数据库连接配置数据库部分建议手动执行第 2 章的建表 SQL再往 dish 表插几条测试菜品。服务端连接数据库的配置一般写在 src 下的 db.properties 或 DBHelper.java 里内容大同小异# db.properties jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/wireless_order?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456数据库连接这一环最容易翻车的是驱动版本。MyEclipse 10 时代自带的 mysql-connector-java 是 5.x如果你本机装的是 MySQL 8.x运行时大概率报 Communications link failure。解决方法MySQL 5.x 用 5.1.49 驱动MySQL 8.x 用 8.0.x 驱动注意 8.x 驱动的类名是 com.mysql.cj.jdbc.Driver连接串还要加上 useSSLfalse 和 serverTimezoneAsia/Shanghai不然会报时区错误。改完关闭 Tomcat 重新启动。4.3 先做服务端接口测试再联调 Android 客户端我拿到这类项目有个习惯先别急着开 App用浏览器或 curl 把服务端接口打一遍确认返回结构再去动客户端。服务端接口大概有登录、菜单、下单、结账四个逐一验证# 登录接口返回 success 说明账号密码可用 curl -X POST http://localhost:8080/ProjectName/LoginServlet \ -d account1001pwd123456 # 菜单接口返回菜品字符串或 JSON curl http://localhost:8080/ProjectName/MenuProvider?typegetMenu # 下单接口注意参数格式与客户端一致 curl -X POST http://localhost:8080/ProjectName/OrderServlet \ -d table_id1dishes1:2,3:1 # 结账接口 curl -X POST http://localhost:8080/ProjectName/PayServlet \ -d table_id1做完这轮服务端接口测试后再回到客户端联调。模拟器访问宿主机不能用 localhost要用 10.0.2.2真机则填电脑的局域网 IP并且保证手机和电脑在同一 Wi-Fi 下。如果登录、菜单、下单三条链路在 curl 下都是通的客户端连不上问题几乎一定出在 URL 配置而不是业务代码上这时候优先检查 BASE_URL。5. 避坑指南MyEclipse 10 到 Android 模拟器的五处老坑这个项目最让我觉得值得记录的是它在不同环境下暴露出来的环境问题比业务问题还多。下面五条都是我实际踩过或者看周边人踩过的坑每一条都按现象、原因、解决记录。5.1 模拟器连不上服务端localhost 指向了谁现象APK 在 Android 模拟器上运行点击登录一直提示网络错误但服务端明明已经启动。原因客户端把服务端地址写成了 localhost 或 127.0.0.1。这是老项目最常见的玄学问题——在模拟器里这两个地址指向模拟器自己而不是宿主机。解决模拟器环境把地址改成 10.0.2.2真机环境把 host 改成电脑的局域网 IP并关掉防火墙或单独放行 8080 端口。改完清理客户端数据重新登录。判断是不是这个问题最直接的办法就是在模拟器的浏览器里访问 http://10.0.2.2:8080/工程名/能打开就是地址的问题。5.2 中文乱码字符集三个环节必须一致现象客户端显示菜品名全是问号或乱码服务端日志打出来也是乱码。原因至少三个环节里有一个用了非 UTF-8——MySQL 表默认字符集、JDBC URL 的 characterEncoding 参数、Tomcat 连接器 URIEncoding。这三处只要有一处不一致中文就保不住。解决建表时指定 DEFAULT CHARACTER SET utf8JDBC URL 后面加 useUnicodetruecharacterEncodingutf8在 Tomcat 的 server.xml 里给 Connector 加 URIEncodingUTF-8。三个地方统一成 UTF-8 后重启服务端问题基本消失。如果还乱就是 Android 端接收字节流后用错了编码改成 new String(bytes, UTF-8)。5.3 主线程网络请求崩溃Android 4.0 以后的硬规则现象真机运行点登录直接崩溃日志里是 NetworkOnMainThreadException模拟器上偶尔能跑。原因老代码在 Activity 里直接开了同步网络请求。Android 4.0 起禁止主线程执行网络操作模拟器慢反而没触发真机一按就崩。解决把网络请求全部放进子线程或 AsyncTask。最快的改法是一层封装把所有 HTTP 调用挪到 HttpHelper 内部用线程加回调返回结果。如果项目里每个按钮都在主线程发请求建议用 Android Studio 打开后全局搜 HttpURLConnection 和 HttpClient逐个改掉。还有些老代码用的 HttpClient 在 Android 6.0 之后被移除了默认支持真机编译时会报找不到类需要在 build.gradle 里加 useLibrary org.apache.http.legacy。5.4 结账后桌台状态不更新订单状态与桌台状态要成对修改现象服务员在客户端点结账显示成功但返回桌台列表时依然显示占用。原因PayServlet 只更新了订单表的状态漏掉了 table_info 表的状态或者两个更新不在同一个事务里前一个成功、后一个失败。解决结账逻辑里订单状态和桌台状态必须成对更新并且放进同一个事务。JDBC 下用 conn.setAutoCommit(false) 包住两条 UPDATE全部成功再 commit任一条失败就 rollback。具体的做法是先按 table_id 把 status0 的订单查出来逐个更新成已结账再更新 table_info 的状态最后统一提交事务。改完后再用 curl 对 PayServlet 做一次服务端接口测试确认两个表的 status 都被改掉。5.5 导入后 ClassNotFoundjar 包没进 WEB-INF/lib现象项目导入 MyEclipse 后启动报 ClassNotFoundException 或页面 500 错误看异常栈是找不到 com.mysql.jdbc.Driver 或 javax.servlet.Servlet。原因servlet-api.jar、mysql-connector-java.jar 依赖没有放进 WEB-INF/lib。Eclipse 项目的 lib 引用只存在 .classpath 文件里换一台电脑导入时关联的 jar 路径失效。解决把两个 jar 复制到 WEB-INF/lib 下然后右键项目 - Build Path - Configure Build Path检查 Libraries 里的引用路径同时确认 Deployment Assembly 里把 WEB-INF/lib 勾选为部署项。还有一种情况是 jar 包版本冲突WEB-INF/lib 和 Tomcat 自带的 lib 里各自有一份 servlet-api.jar导致接口变成 405 或者 NoSuchMethodError这时把 WEB-INF/lib 里的 servlet-api.jar 删掉只保留 Tomcat 自带的即可。做完这步静态页面能加载、Servlet 能访问才算部署真没问题。6. 进阶改造从点餐系统到毕业设计的三个方向6.1 服务端加一层会话校验过滤器改造方向一在服务端加一个登录过滤器把 LoginServlet 里写入的 Session 变成一道关卡。现在源码里只要知道接口地址就能直接调用毕业设计答辩时这通常是减分项// LoginFilter.java 最简会话校验过滤器 public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { ((HttpServletResponse) resp).sendRedirect(login.html); return; } chain.doFilter(req, resp); } }过滤器最核心的是 request.getSession(false) 这个写法参数传 false 表示不主动创建 Session判断登录态时拿不到就重定向。这样 OrderServlet、PayServlet 这些接口都受保护只有登录后拿到 Session 才能继续访问。6.2 把 PayServlet 改成订单状态机改造方向二现在的 PayServlet 大概率是把订单表的 status 改成 1 就结束了。可以往状态机方向扩新增“已下单、制作中、已上菜、已结账”四个状态客户端每完成一步就往服务端发一次状态变更请求。这样服务端才真正成为业务的中心而不是一个只负责存取数据的接口。6.3 用轮询或 WebSocket 让后厨实时收到新单改造方向三当前方案如果后厨也装客户端需要刷新才能看到新单。传统做法是 OrderActivity 提交后后厨页面每隔几秒轮询一次订单表进阶做法是上 WebSocket服务端接到订单后推送给后厨端及时性和体验都更好也能作为简历上的一个亮点。写到这里回想这套系统真正训练人的地方不是某个框架或者某个算法而是数据的流转。从那以后我拿到任何老项目第一件事一定是顺着一条业务数据从客户端拼装、服务端解析、数据库落表再返回的完整路径走一遍再动手改代码。这个习惯帮我省下了大量查问题的冤枉时间希望帮到你。本文还有配套的精品资源点击获取