简介:面向毕业设计移动端的校园快递代拿跑腿App源码案例,压缩包内为完整可参考工程,适合计算机相关专业学生用于毕设选题或课程实践。包体共52个文件,整体约540KB,主体由20个JS脚本和19个Vue组件构成,同时包含CSS样式、HTML入口、MySQL数据库脚本及package等配置,并附有3张预览图,轻量紧凑、目录清晰。项目围绕校园快递代取核心场景,前端覆盖登录、下单、订单状态查询等流程,涉及组件化页面、路由配置、网络请求封装;数据库端通过SQL脚本设计用户表、订单表、快递信息表,可实现基础业务关联。目前已有116人浏览学习。通过这份源码,可梳理前端工程目录结构与交互逻辑,对照数据库脚本理解数据表关系,并在此基础上扩展跑腿接单、配送员管理、消息推送等功能,为独立完成同类选题提供良好起点。
1. 从毕业设计答辩现场说起:这个跑腿App到底要做多少事
校门口快递驿站堆成山,代拿跑腿的需求在每个大学都存在,这个标题让你用Android Studio把它做成App。基于这个标题毕业的源码案例,本质上是一个小型C2C交易系统:学生发布代拿订单,跑腿员抢单配送,管理员在后台看数据。很多人拿到这个题目就急着写界面,其实答辩评委真正想听的不是你有几个Activity,而是你有没有把订单状态流转、角色权限和并发冲突这些业务逻辑讲明白。这篇文章从架构设计、数据库建模讲到真机联调,把每个环节该设的参数和该踩的坑都排一遍,照着走完,你手里会是一个能在两台手机上同时演示的完整项目。
2. 整体架构与业务建模:先把订单状态机画出来,再写代码
做之前花一天把架构定下来,代码反而走得快。很多拿到这个项目的同学想在手机上用SQLite存所有订单,客户端直连数据库直接读写,这样写出的东西答辩时一问“两个用户怎么共享同一单数据”就卡住了。一个能跑起来的快递代拿App至少需要客户端、服务端和数据库三层,客户端只负责展示和输入,业务规则放在服务端,数据统一落到MySQL里。
2.1 客户端-服务端-数据库三层:为什么毕业设计不能只写单机Demo
客户端和服务端的分工要明确。Android端做的事:登录、发单、浏览订单列表、抢单、更新订单状态、查看个人中心;服务端负责:用户认证、订单合法性校验、状态流转控制、数据统计。为什么不能把业务规则写在Android端?因为客户端可以被绕过。比如发单时只在界面上校验“余额大于0”,攻击者直接调用接口就可以带着负数金额下单,服务端同样要校验一遍。
常见做法是服务端返回JSON数据,Android端用OkHttp或Retrofit发起请求,数据校验在服务端做。这个项目的技术栈选型一般是:开发工具Android Studio,后端用Spring Boot或Servlet/JSP,数据库用MySQL。如果你的毕设要求不高,后端也可以用PHP写,但Spring Boot生态更完整,答辩时也更好讲。下面这张对比表能帮你确定该走哪条路:
| 对比项 | 客户端直连数据库 | 客户端-服务端-数据库 |
|---|---|---|
| 实现速度 | 两三天能跑通,但后期返工概率大 | 前期多一个服务端工程,中期反而省事 |
| 演示效果 | 单台模拟器自导自演 | 两个账号一台手机就能演示抢单全流程 |
| 答辩风险 | 被问“数据存在哪”容易露馅 | 架构完整,逻辑自洽 |
| 代码量 | 少,但跨端协作没法演示 | 稍重,但每个模块边界清晰 |
带学生做毕业设计时,遇到选直连数据库的,十个有九个在中期改回服务端方案。所以别省这一步,直接把三层架构立起来,后面每个功能点都有清晰的落点。
2.2 订单状态机与角色权限:代拿业务的核心是“状态流转”
快递代拿的业务可以简化成六个状态:已发布(0)、已接单(1)、取件中(2)、配送中(3)、已完成(4)、已取消(5)。用户发布订单后状态是0;跑腿员抢单成功变1;跑腿员到达驿站开始取件变2;取完件往宿舍楼走变3;双方确认收货变4;只有状态是0和1时,发布者可以取消订单。每次状态更新时服务端要校验“操作者是不是这个订单的合法角色”,不能用户随便传个status=4就把订单改了。
角色权限分三类:普通用户能发单、取消自己的订单(限0和1状态)、确认收货;跑腿员能抢状态为0的订单、更新配送状态(1→2→3)、在后台看自己的接单历史;管理员能查看所有订单、处理投诉、禁用违规账号。users表里的role字段区分,0是普通用户,1是跑腿员,2是管理员。抢单是并发冲突最严重的环节,第4章会专门讲乐观锁解法,这里先记住一个原则:任何状态变更都必须是服务端校验通过后的原子操作,不能在客户端拼字符串传状态。
2.3 技术栈选型:Java还是Kotlin、SQLite还是MySQL、网络库怎么选
这个标题写的是Android Studio,但具体的开发语言在源码里不能含糊。课程里教的多是Java,熟悉度高的就选Java;如果你愿意学Kotlin,它的协程写网络请求比回调更顺手。我一般建议Java,因为你和答辩评委都熟悉,代码示例多,遇到问题搜起来快。网络库用Retrofit + OkHttp,Retrofit把JSON解析和接口定义绑在一起,体感清爽;不想用框架也可以只上OkHttp,但Retrofit在代码组织上更有条理。
数据层要分清两边:服务端必须用MySQL(或SQLite先顶研发环境,上线前换MySQL),Android端本地可以用SQLite做缓存和离线存储,但不能拿它当主数据库。图片加载用Glide,自带内存缓存和磁盘缓存,省去手写图片压缩的麻烦。服务端框架如果自选,Spring Boot和SSM二选一就够了,不需要上微服务那套,那是给自己找麻烦。还有一个细节:Android Studio本身的工程配置,gradle-wrapper.properties里的distributionUrl对应特定Gradle版本,JDK版本也要匹配,不然打开别人的源码工程会一直被版本问题卡住,这个放到第5章避坑里细说。
2.4 数据库表设计:订单表、用户表、接单记录表的核心字段与建表SQL
数据库是整个项目的底盘,表结构设计不好,后面每写一个查询都是坑。我一般会建三张核心表:用户表users、订单表orders、接单记录表take_logs。用户表存账号、密码散列、昵称、手机号、角色、余额;订单表是业务核心,存发布者、接单者、取件地址、送达地址、跑腿费、状态、时间戳;接单记录表记录每一次接单行为,答辩时用来展示数据统计。
订单表的建表SQL如下,字段备注直接写清楚,方便答辩时照着讲:
CREATE TABLE `orders` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '订单主键', `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号,时间戳+随机数,对外展示用', `user_id` INT NOT NULL COMMENT '发布者ID,关联users表', `take_user_id` INT DEFAULT NULL COMMENT '接单者ID,抢单成功后写入', `pickup_address` VARCHAR(64) NOT NULL COMMENT '取件地址,如菜鸟驿站2号柜', `delivery_address` VARCHAR(64) NOT NULL COMMENT '送达地址,如梅园3栋520', `fee` DECIMAL(5,2) NOT NULL COMMENT '跑腿费,保留两位小数', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0已发布 1已接单 2取件中 3配送中 4已完成 5已取消', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注,如快递单号', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后操作时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_status` (`user_id`, `status`), KEY `idx_take_status` (`take_user_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快递代拿订单表';字段类型和索引有几个参数需要说明。跑腿费用DECIMAL(5,2)而不用FLOAT,是因为二进制浮点数算钱会有精度误差,答辩时你主动提这一点,评委印象分会上去。status用TINYINT而不是VARCHAR,是为了状态更新语句里可以数字比较,配合乐观锁更干净。订单编号用唯一索引,因为现实中用户会拿order_no去驿站找件,如果撞号就乱套了。联合索引idx_user_status和idx_take_status分别服务“我的发布列表”和“我的接单列表”两个高频查询,这两个查询都有“当前用户ID + 状态”的条件,组合索引刚好命中。
用户表和接单记录表的建表不贴完整代码了,核心字段是:users表要有role、phone、avatar;take_logs表要有order_id、take_user_id、take_time,给order_id建普通索引就行。表建好后,记得在服务端启动时用一条初始化SQL插入一个管理员账号,否则后台管理入口永远登不进去。
3. 用Android Studio把用户端跑起来:登录、发单与订单列表的实现
架构和表结构定了,就可以进Android Studio码代码。这一章从工程结构、登录注册到发单和列表刷新,把用户端的主干功能串一遍。你拿到源码案例时,先别急着逐个文件看,先看工程结构是否清晰,再看网络层封装是否统一,最后看业务代码里有没有把状态校验写在客户端。Android Studio里常切一下目录视图,很多新手在默认的Android视图下找不到values文件夹,切到Project视图就能看到完整目录,这个跟IDE版本没关系,纯视图切换问题。
3.1 项目结构划分:按MVC分层,避免一个Activity塞两千行
拿到项目的源码工程,第一件事先看包结构。一个规范的项目应该这样分:activity包放页面,LoginActivity、RegisterActivity、PublishOrderActivity、OrderListActivity、OrderDetailActivity、PersonalActivity;adapter包放列表适配器,OrderAdapter、CommentAdapter;entity包放实体类,User、Order、ApiResult;network包放网络请求封装,RetrofitClient、ApiService、OkHttpInterceptor;utils包放工具类,MD5Util、SharedPreferencesUtil、ToastUtil。
这样分层的好处是:改动页面的逻辑只在activity,调整数据展示只在adapter,接口变动只在network,每块代码都能单独替换。我见过一个OrderListActivity写了1500行的项目,列表适配器、网络请求、图片加载全塞进去,改一个字段要全局搜,就是给自己埋雷。Android Studio里右键新包名,把类拖进去就可以,包名全部小写,别用中文。这块没有太多技术含量,但答辩时评委第一眼看的就是结构,混乱的包名和到处放的全是灰的类,印象分直接掉一档。
3.2 登录注册模块:MD5加盐存储与Token校验
登录注册是最常被答辩追问的模块。常见做法是密码不存明文,服务端存MD5加盐后的散列值。先看一个纯Java的MD5工具类,这个放在utils包里:
public class MD5Util { // 盐值固定存储在服务端配置里,不写死在客户端代码中 public static String md5WithSalt(String input, String salt) { String afterSalt = input + salt; try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(afterSalt.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { // 转成两位十六进制,不足两位补0 sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { e.printStackTrace(); // 工具类里别返回null,给个兜底值方便排查 return ""; } } }这里加盐和不加盐的区别:直接用MD5(“123456”)会被彩虹表秒破,几乎所有常见密码的MD5都有预计算结果;加盐之后就是MD5(“123456” + 盐值),原始密码完全相同的两个人,散列值也不同。注意盐不要写在客户端代码里,有人把盐值常量写在Android的BuildConfig里,反编译一下就暴露了,服务端读取配置更稳妥。
登录请求用Retrofit发POST。客户端把用户名和MD5加盐后的密文传给服务端,服务端校验通过后返回一个Token,客户端把Token和用户名存进SharedPreferences,后续每次请求都在Header里带Token。这套流程在毕业设计里够用,但你自己要清楚:真实App不会只用MD5,至少要走HTTPS加TLS,不过教材项目里MD5加盐演示能自洽,答辩不会在这题上深挖。
3.3 发单页:从选择快递点到提交订单的完整链路
发单页是用户端最核心的业务页面,它把前端表单和服务端数据串起来。一个完整的发单流程包括:选择取件驿站、填写送达地址和备注、显示平台建议跑腿费、提交订单。跑腿费可以做一个滑动条,最低1元、最高20元,方便演示时快速调整价格。
订单提交的核心逻辑是把表单数据封装成Order对象,调用服务端发布接口。用Retrofit写起来是这样的:
public void submitOrder(Order order) { Call<ApiResult<Order>> call = apiService.publishOrder(order); call.enqueue(new Callback<ApiResult<Order>>() { @Override public void onResponse(Call<ApiResult<Order>> call, Response<ApiResult<Order>> response) { // 服务端会二次校验,防止绕过客户端直接提交非法数据 if (response.body() != null && response.body().getCode() == 0) { ToastUtil.show("发布成功,等待跑腿员接单"); finish(); } else { ToastUtil.show("发布失败:" + response.body().getMessage()); } } @Override public void onFailure(Call<ApiResult<Order>> call, Throwable t) { // 网络异常时保留用户填的内容,不要直接清空表单 ToastUtil.show("网络异常,请检查连接"); } }); }注意一个细节:onFailure里不直接清空表单,因为很多情况下只是服务端暂时没响应,用户只需要重试提交,重新填写很影响体验。参数方面,Order对象要在entity包里建好,字段名和服务端JSON字段保持一致,Gson才能直接反序列化。跑腿费是BigDecimal类型,中文取值、驿站名都涉及编码问题,Android Studio默认是UTF-8,别改成GBK,否则换电脑后中文注释全变乱码。
3.4 订单列表刷新:轮询间隔怎么设才不费电
订单列表的刷新方式是这个项目躲不开的设计决策。常见做法有两种:轮询和长连接。毕业设计场景里我选轮询,因为WebSocket和MQTT要让服务端配套改造,工作量涨一倍,而订单数据本来就不是强实时,晚几秒看到新单没有影响。
轮询用Handler加Runnable实现,在订单列表页的onResume里启动。间隔我一般设15秒,兼顾及时性和电量。太短的话(比如3秒)一页打开一晚上要请求几千次,服务器扛不住,手机会持续唤醒网络模块;太长的话(60秒)演示时评委等你刷新等得无聊。
private final Handler refreshHandler = new Handler(Looper.getMainLooper()); private final Runnable refreshRunnable = new Runnable() { @Override public void run() { loadOrderList(); // 拉取最新数据并更新Adapter refreshHandler.postDelayed(this, 15000); // 15秒后再次执行 } }; @Override protected void onResume() { super.onResume(); loadOrderList(); // 从后台切回来时立即刷一次,不等待轮询 refreshHandler.postDelayed(refreshRunnable, 15000); } @Override protected void onPause() { super.onPause(); refreshHandler.removeCallbacks(refreshRunnable); // 页面不可见时停掉轮询 }这个写法有两个关键点。第一,onResume里先手动刷一次再启动轮询,用户从详情页返回列表时能立刻看到订单状态更新,不用干等15秒。第二,onPause里必须removeCallbacks,否则页面跳到详情页后轮询还在后台跑,白白浪费流量,还会在返回时触发重复数据闪烁,这就是典型的低续航和页面卡顿翻车现场。哪怕轮询间隔设好了,这两行代码漏掉,前面全白做。
4. 接单端与后台管理:跑腿小哥怎么抢单、管理员怎么审核
用户端能发单,跑腿员端能抢单,这才算一个完整的闭环。接单端是快递代拿项目里最容易把实现细节讲出彩的地方,原因是这个场景天然有并发问题:两个跑腿员在同一秒看到同一单,谁先抢到?这个问题的解法写清楚,答辩基本稳了。
4.1 抢单接口的设计:用状态字段做乐观锁,防止两人同时抢到
抢单操作从用户视角是“点一下按钮”,从服务端看是一次高风险的并发更新。最简单的错误写法是:先查订单状态,如果是已发布,就把它改成已接单。高并发下两个请求同时查到已发布,都会执行更新,结果就是同一单被两个跑腿员接走,这就是一次典型的并发竞态。
正确的做法是乐观锁,把状态判断下沉到UPDATE语句里。下面是服务端Mapper里的核心SQL:
UPDATE orders SET take_user_id = #{takeUserId}, status = 1, update_time = NOW() WHERE id = #{orderId} AND status = 0 AND take_user_id IS NULL这段SQL的关键在WHERE条件里:不仅要求当前状态是0,还要求接单者ID为空,双重校验。执行后看影响行数:返回1说明抢单成功,返回0说明订单已经被别人抢了,直接提示“手慢了”。乐观锁的意思就是这样,不加数据库层面的锁,而是靠条件WHERE把更新限制在安全范围内,谁先执行就锁住状态,后到的人自然更新0行。
对应的Java接口处理逻辑要加一个事务注解,抢单失败时抛业务异常回滚:
@Transactional public boolean grabOrder(Long orderId, Long takeUserId) { // 先做一次角色校验,非跑腿员角色直接拒绝 User user = userMapper.selectById(takeUserId); if (user == null || user.getRole() != 1) { throw new BusinessException("当前账号没有接单权限"); } int rows = orderMapper.grabByStatus(orderId, takeUserId); return rows > 0; }用@Transactional是必须的,因为抢单成功后可能还要往take_logs表插记录,这两个写入动作任何一个失败整体都要回滚。抢单成功后顺手插一条接单记录,后台统计接单量时有数据可查。这套逻辑里最关键的是SQL语句里的AND status = 0,它把“检查”和“更新”合并成一个原子操作,避免了检查后更新前别人插入的窗口期。
4.2 我的接单列表:RecyclerView多布局与订单状态切换
跑腿员端的“我的接单”和用户端的“我的发布”只是数据源相同、操作按钮不同,写的时候用RecyclerView多布局区分。getItemViewType根据订单status返回不同布局类型,Adapter里给不同状态展示不同按钮:状态1显示“取件”,状态2显示“开始配送”,状态3显示“确认送达”。用户端看到已接单就只显示“等待配送中”,不能出现取件按钮。
按钮点击事件放在Activity里处理,用接口回调从Adapter传出来,职责清晰。我见过直接把业务代码写在ViewHolder里的,改起来非常痛苦,按钮逻辑要复制好几份。正确的姿势是Adapter只管视图绑定,点击事件统一走一个OnOrderActionListener接口,Activity里根据订单ID和操作类型调对应的服务端接口。这里涉及一个细节:RecyclerView的条目复用会导致按钮点击错乱,所以每个按钮的点击监听里都要拿着holder.getBindingAdapterPosition()去取当前条目的订单数据,不能直接用position变量,这个坑在快速滑动时特别明显。
4.3 后台数据看板:统计SQL与图表选型
后台管理页面通常不在Android端做,但毕业设计里很多人把它并进同一个App,管理员登录后看到的就是一个统计页面。这个页面需要展示的数据一般有四块:今日新增订单、进行中的订单数量、累计注册用户、待处理投诉。用三到四条聚合SQL就能算出来:
-- 今日新增订单数 SELECT COUNT(*) FROM orders WHERE DATE(create_time) = CURDATE(); -- 进行中订单数(已接单到配送中这3个状态) SELECT COUNT(*) FROM orders WHERE status IN (1, 2, 3); -- 注册用户总数 SELECT COUNT(*) FROM users; -- 各状态订单分布,喂给饼图用 SELECT status, COUNT(*) AS cnt FROM orders GROUP BY status;第一段的聚合SQL返回值是单行单列,服务端直接包成Map返回即可。调试时如果发现统计数字对不上,先单独在Navicat里跑一遍SQL,确认是不是联表查询出了问题,再去看Java代码。
图表展示我一般用MPAndroidChart,饼图展示订单状态分布,折线图展示最近七天订单趋势。折线图要按日期分组,SQL写DATE(create_time) BETWEEN ... GROUP BY DATE(create_time),查出来是个List,端上转成Entry数组。图表库记得在build.gradle里引依赖,ProGuard混淆时必须keep住,否则发布版直接白屏,这就是为什么发布前必须测一次release包的原因。
4.4 服务端接口约定:统一返回格式与错误码
不论客户端还是后台,所有接口都返回同一个JSON包装结构,这是新手最容易忽略但最能体现工程规范的点。建议的格式是:code为int型,0表示成功,非0表示业务错误码;message为给用户看的提示语;data为泛型数据,可以是对象、列表或者空。
对应的错误码要有对照表:1001参数缺失、1002账号不存在、1003密码错误、1004账号被禁用、2001订单不存在、2002订单已被抢、2003无权操作此订单。把这些写在ApiResult实体类里,服务端和Android端共用一套语义。Android Studio里创建values文件夹时,多语言这种字符串资源可以放string.xml,但业务提示建议直接走message字段,服务端改文案不用重新发版,对毕业设计来说性价比更高。统一返回格式还有一个好处:Retrofit的解析逻辑只需要写一次,错误判断全部集中在onResponse的code分支里,不会出现每个接口各写一套异常处理的混乱局面。
5. 真机联调与打包发布:连不上服务器、闪退、OOM的排查避坑指南
这一章没有高深的原理,全是实操中会堵住你的真实坑点。每一步都标注了现象、原因和解决,都是这个项目里最常见的翻车点。遇到问题别盯着代码一行行看,先按下面的清单对一遍,大部分都能直接定位。
5.1 模拟器访问本机后端:10.0.2.2还是局域网IP
现象:Android Studio模拟器里,App请求http://localhost:8080/接口永远连不上,控制台报Connection refused。原因:模拟器里的localhost指向的是模拟器自己,而不是你的开发电脑,所以需要访问宿主机的特殊地址10.0.2.2。解决:开发环境把Retrofit的baseUrl写成http://10.0.2.2:8080/,真机调试时改成电脑的局域网IP,比如http://192.168.1.100:8080/。如果换了网络环境,IP要记得更新。我一般在Constants类里留一个isEmulator开关,跑模拟器时自动切到10.0.2.2。
这里有一个容易忽略的问题:真机调试时手机和电脑必须在同一WiFi,且要关掉电脑防火墙对Java进程的限制,否则服务端接口在浏览器里能打开,手机上死活连不上,玄学一样的现象背后就是防火墙规则。
5.2 Android 9+默认禁用HTTP明文流量:网络安全配置怎么加
现象:请求明明没写错,但抛出的异常是Cleartext HTTP traffic to xxx not permitted。原因:从Android 9(API 28)开始,系统默认禁止应用使用HTTP明文协议,而毕业设计里服务端通常没配HTTPS证书,所以全被拦截。解决:在AndroidManifest.xml的application标签里加一行android:usesCleartextTraffic="true",或者用网络安全配置只对调试域名放行。完整配置是这样:
<application android:label="@string/app_name" android:usesCleartextTraffic="true" ...> </application>这个开关在正式发布项目时应该分环境处理:Debug包开着方便联调,Release包关掉走HTTPS。但毕业设计没有Release上线压力,直接开着最省事。注意:如果把这个属性写在某个Activity上,那是无效的,必须写在application标签上。这个属性是API 23才有的,Android Studio里的minSdkVersion别设太低,不然低版本设备上直接编译报错。
5.3 图片加载OOM:缩略图尺寸与本地缓存
现象:订单列表里带了几张快递驿站照片,滑动几下应用就闪退了,Logcat里是OutOfMemoryError。原因:直接加载原图会让Bitmap占满堆内存,一张3000×4000的照片解码后大约占用45MB内存,滑动加载十几张就爆了。解决:使用Glide替代手动加载,它会按控件尺寸自动采样:
Glide.with(context) .load(imageUrl) .override(400, 400) // 强制解码400x400的缩略图 .centerCrop() .into(imageView);override参数是解决OOM的关键,Glide内部会先读图片宽高做采样计算,不会一次性解原图。磁盘缓存策略也可以打开,用skipMemoryCache(false)和diskCacheStrategy(DiskCacheStrategy.RESOURCE)配合,减少重复网络请求。图片不是这个App的主要数据,但OOM问题一旦出现,排查起来比业务代码复杂得多,所以先在前端拦一道。另外一个隐藏坑:Glide的图片加载在ListView或RecyclerView快速滑动时会有大量请求,如果服务端图片服务器带宽不够,图片加载会超时,这时候要在Glide的RequestOptions里设置timeout,别用默认的无限超时。
5.4 轮询通知被系统静默杀死:前台服务与WorkManager
现象:手机锁屏后,跑腿员的轮询请求不再执行,亮屏解锁后又恢复正常,以为是自己代码有问题。原因:从Android 6.0开始引入Doze模式,屏幕熄灭一段时间后系统会把后台网络请求挂起,这是系统级别的限制,跟代码逻辑无关。解决:把订单监听改成前台服务,在通知栏挂常驻通知,这样系统视为用户可见任务不会休眠。或者用WorkManager做周期任务,但WorkManager周期任务的最小间隔是15分钟,对实时接单场景不够用,所以我推荐前台服务方案。
前台服务在onStartCommand里startForeground,创建时传一个通知Channel,Android 8.0以上必须有Channel否则通知不显示。这个方案演示时有个小副作用:通知栏会一直有个“订单监听中”的提示,反而能成为答辩时展示你对Android系统机制理解的一个细节。注意前台服务启动后要在onDestroy里stopForeground和stopSelf,否则服务一直挂着,在设置界面看耗电排行会非常难看。
5.5 发布前的签名与混淆:哪些类在ProGuard里不能混淆
现象:Debug包跑得好好的,打正式包签名后用另一台手机安装,一启动就闪退,或者登录后返回数据解析失败。原因:正式包默认开启代码混淆,Gson解析需要访问实体类的字段名,混淆后字段名变成a、b、c,JSON映射就全乱了。解决:在proguard-rules.pro里保留实体类、网络请求接口和自定义注解不能混淆。常用规则是:
-keep class com.example.entity.** { *; } -keep class com.example.network.** { *; } -keep class com.google.gson.** { *; }发布前记得做一次全流程回归测试,用正式签名包跑一遍登录、发单、接单、确认收货。混淆是最容易在发布前一刻翻车的环节,别等答辩现场装正式包才发现白屏。Android Studio打不开别人的源码工程通常是gradle版本和JDK对不上,打开后先看gradle-wrapper.properties里的distributionUrl,缺什么版本就让它自动下载,IDE卡死把gradle jvm内存调大一点再重开。
6. 答辩演示脚本与验证清单:让评委相信你真正做过
答辩时不要从头点一遍页面,评委想看到的是你对业务的理解。按“异常优先”的顺序演示:先展示服务端的报错返回,再演示正常流程,最后用两个账号制造一次冲突。先用一个还没登录的账号点“发单”,演示客户端校验和401拦截;然后正常登录用户端发一单,跑腿员端抢单,展示订单状态从0变成1,同时注意观察服务端控制台打印的SQL日志,确认乐观锁的WHERE条件确实生效。接着再开一个跑腿员账号抢同一单,弹出“手慢了”,这就是给评委看并发处理的真实数据,比口说百句都强。
验证清单上建议列这几个场景:同一账号重复抢同一单、越权把订单状态改成4、管理员看板数据与订单表实际记录是否一致、两个账号同时抢单时最终只产生一条接单记录。前三项在演示时每个都值得停下来讲一句背后的判断逻辑,第四项是最有含金量的,因为很多毕业生都栽在这里。可以把orders表的status字段手动改成一个非法值比如99,然后调用接口,服务端应该返回2003无权操作,这个异常分支我也有一次忘了处理,答辩被评委问了一下才意识到状态枚举校验没做完。
最后给自己留一条后路:提前把SQL日志打印打开,真到演示翻车时就指着日志说“看,这里校验失败了”,比黑匣子强得多。整个项目做下来,你会发现自己能讲清楚的不只是几个页面,而是订单状态机、乐观锁、Doze模式、混淆配置这些真正值钱的问题。当年我做类似题目时走了客户端直连数据库的弯路,后来改服务端接口才把逻辑理顺,现在把这个习惯留给你。希望帮到你。
本文还有配套的精品资源,点击获取