今年毕业设计选题那会儿我面前其实放了三个差不多的方向一个是 Java 写的汽车 4S 店管理系统一个是面向 Android 的公交线路查询与运行监测平台还有一个是基于 Android 的公交动态信息速查系统。很多同学看到这类题目第一反应是挑个好做的我却觉得反正都是做不如选一个既能体现 Java 功底又能展示移动端实时交互能力的题目。最后我把后两个公交方向的题目揉成了一个基于 Android 的公交动态信息速查系统顺便把后台运行监测也做了进去。这篇文章就围绕这个毕设展开记录我从选题、架构设计、数据模拟、Android 客户端实现到答辩准备的完整思路。如果你也在做类似的 Java Android 项目或者正准备拿公交类应用当毕业设计可以直接抄作业的思路应该不少。1. 从4S店管理系统到公交动态速查毕设选题的重新思考1.1 三个候选方向的差距在哪里乍一看4S 店管理系统和公交动态速查系统都属于信息管理 移动端展示但实际做起来完全是两种体量。4S 店管理系统核心是车辆、客户、订单、维修记录的增删改查典型的管理后台公交动态速查系统则涉及线路建模、站点排序、车辆定位、实时刷新、地图呈现是典型的移动互联网应用。我把两个项目拉在一起比过区别很明显对比维度4S店管理系统公交动态速查系统核心数据订单、客户、库存线路、站点、车辆位置交互特点表单录入、列表展示LBS定位、实时刷新、地图交互难度重点数据库关系设计实时数据同步、UI刷新、位置计算技术含金量CRUD为主定位、网络、地图、多线程答辩演示效果操作表格地图上车辆移动直观我当时问了自己一个问题评委看到订单列表和看到地图上公交车辆实时移动哪个更能说明工作量答案不言自明。公交方向项目天然具备可视化和实时性这两个属性恰好是毕业设计答辩中最加分的点。1.2 我为什么没有硬选车来了那种大厂方案市面上的公交应用早就把功能做得很完善上下行、首末班、实时位置、到站预测、拥挤度、线路规划甚至还有语音播报。毕设肯定不能照着那种体量做否则做到毕业也做不完。另一个问题在于真实公交数据接口基本拿不到大多数所谓实时公交都是模拟数据这反而是好事——论文里可以光明正大地写采用模拟数据源验证系统功能核心价值在于链路完整而不是数据真实性。所以我定下的边界很明确不做全网公交线路规划只做指定线路的站点查询和车辆位置展示不做多城市覆盖只做单个城市的大约 10 条线路保证演示够用不做真实 GPS 车辆接入用服务端模拟车辆按线路行驶运行监测功能单独做一个后台页面专门展示车辆状态和异常告警。这样整个系统就是一个有闭环的小项目后台模拟数据 - 接口下发 - Android 客户端展示 - 运行监测判断异常。链路通了工作量也讲得清。2. 技术栈和整体架构Android全链路方案2.1 客户端用原生Android后台也用Java图什么毕业设计往往有必须用 Java的硬性约束我身边不少同学用 Kotlin但题目既然写了 Java那就老老实实用 Java。Android 端语言用 Java后台接口也选了 Spring Boot好处是整个项目语言统一JavaBean 可以直接从后台复制到 Android 端字段对照方便排查问题时不用来回切换思维。地图这块我没有自研直接接第三方地图 SDK 的标准地图和 Marker 组件。坐标系统需要额外处理国内地图 SDK 用的是 GCJ-02 加密坐标服务端模拟数据直接当成 GCJ-02 存客户端拿到就显示不做基准转换这样绕开了一个很容易踩的坑。2.2 系统里的三条业务主线我把系统拆成了三条主线后面所有开发都围绕这三条线展开线路查询用户输入线路编号或点击热门线路返回线路经过的站点、首末班时间、上下行方向动态速查用户选择某条线路后地图上展示当前在线车辆的位置列表里展示每辆车距当前站还能坐几站、大概几分钟到运行监测后台定时接收车辆位置上报计算偏航、超速、长时间未动、掉线等异常并在监测页面展示。这样的拆分让 Android 端和后台各自有了明确职责。Android 端做查询展示和刷新后台做数据生成、存储和异常判定。两边通过 JSON 接口通信接口约定好字段前后端可以并行开发这也是我后来在答辩时重点提到的协作开发思路。2.3 数据对象先定义好后面少返工系统涉及的核心对象我一开始就梳理成了四个线路Line、站点Station、车辆Bus、车辆位置快照LocationRecord。代码里用 JavaBean 来对齐接口字段减少类型不匹配。public class BusPosition { private int busId; private int lineId; private double lat; private double lng; private int currentStationIndex; // 当前所在站点序号 private long updateTime; // 位置上报时间 private int speed; // 当前速度 km/h }后端下发这个对象Android 端拿到后直接转成地图坐标和列表文案。字段越简单前后端联调越省事。有些同学会在对象里塞很多冗余字段比如车辆颜色、司机名字、车牌号实际展示根本用不上我只保留了 UI 层真正需要的字段。3. 数据层设计公交实时数据的模拟也能做出真实感3.1 拿不到真实公交数据就自己让车跑起来毕业生很难拿到公交集团的实时 GPS 数据但这不代表系统没价值。我的方案是写一个数据模拟模块根据每条线路的站点经纬度生成若干辆车在线路上往返运行每 3 到 5 秒更新一次位置模拟数据写入数据库和内存缓存。模拟车辆的行驶逻辑也不复杂本质就是沿着站点坐标走// 伪代码车辆沿线路移动 Station next line.getStations().get(simulator.getNextIndex()); GeoPoint current simulator.getCurrentPoint(); double delta speedMetersPerSecond * intervalSeconds; GeoPoint newPoint moveTowards(current, next, delta); simulator.setCurrentPoint(newPoint); if (distance(newPoint, next) 30) { simulator.setCurrentStationIndex(next.getIndex() 1); }这个模拟器必须和真实接口隔离。我单独开了一个数据源接口调试时用 MockDataSource将来有真实接口时实现同一个接口换掉就可以。答辩时如果评委问你数据怎么来的你可以很坦诚地说这是模拟数据源接口层已经做了抽象——这句话本身就比硬着头皮说接了真实数据更可信。3.2 实时刷新用轮询还是WebSocket这是本项目最核心的技术决策之一。实时公交本质上要回答现在车在哪但 Android 端并不需要每秒钟都刷新。我对比过两种方案方案优点缺点适合场景HTTP轮询实现简单调试直观请求频繁、耗电、延迟取决于间隔数据变化频率在5秒以上WebSocket服务端主动推送实时性强代码复杂、断线重连要处理秒级更新、高并发场景我做的是毕业设计更重要的是把链路跑通、逻辑讲清。轮询足以支撑 5 秒刷新间隔代码简单不容易出问题。所以我选了轮询Android 端每 5 秒请求一次车辆位置接口只增量刷新地图上的 Marker 和列表文字。这里有个小技巧可以用Handler.postDelayed代替Timer因为它跑在主线程上更新 UI 前可以少写一层线程切换。private void startLoop() { handler.removeCallbacksAndMessages(null); handler.postDelayed(new Runnable() { Override public void run() { fetchBusPositions(); handler.postDelayed(this, 5000); } }, 0); }WebSocket 那条路我在文档里提了一句可扩展实现但没有在代码里做。这属于把复杂度控制在自己能驾驭的范围。毕业设计不是炫技把技术选型的理由说清楚比堆一堆高端名词更有意义。3.3 数据库表结构设计我不建议毕设一上来就用复杂表结构。我的数据库一共五张核心表表名作用关键字段t_line线路信息id, line_name, direction, start_time, end_timet_station站点基础信息id, station_name, lat, lngt_line_station线路与站点关联id, line_id, station_id, station_ordert_bus车辆基础信息id, line_id, bus_code, statust_bus_location车辆位置记录id, bus_id, lat, lng, speed, station_index, report_time其中t_line_station里的station_order是关键字段它决定了线路的上下行顺序。判断还有几站到达直接靠station_index差值速度快逻辑简单。实时位置要不要落库我纠结过。每 5 秒写一条记录10 辆车一天就是 17 万条对毕设数据库有点浪费。我最后的做法是正常运行的车辆位置只放内存缓存只有异常事件超速、偏航、掉线才写t_bus_location的历史记录表。这样既保证了监测记录可追溯又避免了海量垃圾数据。3.4 车辆位置模拟器把实时做真模拟器最简单的一版只做匀速直线移动。后来我发现一个问题车辆到了站之后如果继续匀速走用户看到的是车不停靠、直接穿过站点观感很假。所以我给模拟器加了两个小逻辑——到站停车 5 秒再启动以及站点之间速度按 30km/h 匀速。没有做加减速模型已经很接近公交在跑的感觉了。每条线路派 2 到 3 辆车分上下行方向交替发车地图上看起来就有错落感。演示时给评委看的效果就是车辆沿站点逐个移动停一下继续走。这种观感带来的真实感远超过数据精度上的细微差距。4. Android端核心功能线路查询与动态信息速查4.1 线路查询页面怎么设计才能让评委一眼看懂线路查询页分上下两个区域。上半部分是搜索框输入1路2路这样的编号自动联想匹配下半部分是热门线路列表默认展示当前系统里模拟的 10 条线路。选中一条线路后跳到详情页。详情页的核心是站点列表。查询方式其实很简单就是根据线路 ID 从t_line_station表查出所有站点按station_order升序排列。但这里有个容易疏忽的点上下行两个方向要分开存或者用 direction 字段区分。我的做法是同一线路存两条记录direction 分别标 0 和 1这样站点顺序天然区分开不需要在代码里做反向反转。SELECT s.station_name, ls.station_order FROM t_line_station ls JOIN t_station s ON ls.station_id s.id WHERE ls.line_id ? AND ls.direction ? ORDER BY ls.station_order;列表上的第一个和最后一个站点用不同的背景色标出始发站和终点站中间插入当前车辆在图上的对应位置。这样页面信息密度刚刚好也方便评委理解当前车辆在第几站之间。4.2 地图上车辆位置怎么刷新才不卡顿地图刷新是 Android 端最容易卡的地方。一个常见的错误写法是每次拿到数据就清空所有 Marker再重新 add。数据量小可能没事刷新几次后就会明显掉帧。我用的是Marker 复用策略用 busId 作为 key 存一个HashMapInteger, Marker只有当新数据里出现未曾见过的 busId 时才创建 Marker其余情况直接调用marker.setPosition()更新位置。这个改动看着小但对刷新的流畅度影响非常大。for (BusPosition pos : list) { Marker marker markerMap.get(pos.getBusId()); LatLng target new LatLng(pos.getLat(), pos.getLng()); if (marker null) { marker map.addMarker(new MarkerOptions() .position(target) .title(pos.getBusCode())); markerMap.put(pos.getBusId(), marker); } else { marker.setPosition(target); } }同时还需要清理已经不在线车辆的 Marker。判断依据是服务端返回的在线车辆列表如果某个 Marker 的 busId 不在新数据中就移除它并删掉 HashMap 里的引用。这个逻辑不放进去时间长了地图上会残留一堆不存在的车辆图标。4.3 动态速查的列表信息怎么算出还差几站动态速查页在详情页下半部分展示每辆车距离选择站点的信息。算法不复杂已知车辆的currentStationIndex再拿到你选中的站点的station_index两者相减就是还剩多少站。如果差值为 0 或者负的就说明车已经过了这个站或者正停在这个站。int remainStops targetStationIndex - currentStationIndex; if (remainStops 0) { textView.setText(还有 remainStops 站到 stationName); } else if (remainStops 0) { textView.setText(正在本站); } else { textView.setText(已驶离本站); }预计到达时间我用了一个粗略算法每个站点之间的平均行驶时间设为 90 秒那么剩余分钟数就是remainStops * 90 / 60。这个公式不准但演示效果足够。答辩时如果要深入可以补一句真实场景下应该结合历史到站数据做ETA模型这属于加分回答。4.4 定位权限与首次定位的细节动态速查依赖用户当前定位Android 6.0 以上的运行时权限必须处理。我在MainActivity里同时申请定位和网络权限并且在onRequestPermissionsResult里处理拒绝情况——拒绝后进入浏览模式只用模拟定位展示功能。这个兜底策略很重要因为演示场地未必有 GPS 信号总不能让 App 直接闪退或者白屏。uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.INTERNET /5. 运行监测后台的正确打开方式5.1 车辆偏航判定点到路线的最短距离运行监测是后台项目的重头戏也是我当时在论文里花最多篇幅介绍的部分。判定车辆偏航的核心思路是计算车辆当前位置到线路路径的垂直距离超过设定阈值就认为偏航。线路路径本质上是一系列站点连成的折线计算最短距离不需要做复杂的几何库直接遍历所有相邻站点线段用点到线段距离公式求解即可。到了车辆密度比较大的区域再按busId分组在每组内部独立比较避免一辆车偏航导致其他车全部误判。阈值我设了 80 米。城市公交车道一般不会偏离线路超过 50 米80 米是一个比较安全的判断线。如果改得太小模拟器里车辆拐弯时容易误报改得太大车开到隔壁街道也检测不出来。这个参数是调出来的不是拍脑袋定的。5.2 超速与长时间未动别只看一个时间点超速判定很简单车辆上报的 speed 字段超过线路限制速度 60km/h 就记录一次。但要避免连续超速刷屏我的做法是同一辆车同一个时间段内只记一次告警等车辆恢复低速后再重新计数。长时间未动和掉线是两个不同的概念。长时间未动表示位置还在上报但坐标基本不变通常是堵车或故障掉线表示超过固定时间没收到上报比如 30 秒。我把两者分开处理监测页面上用不同颜色区分未动是黄色掉线是红色。long lastReport record.getUpdateTime(); long current System.currentTimeMillis(); if (current - lastReport 30000) { status 掉线; }为了不让监测页面死等接口后台也自行定期扫描数据库里的最新位置记录。这部分我写成独立的调度任务每分钟跑一次。位置上报由模拟器触发监测任务自己按节奏检查两边互不干扰。5.3 监测页面的展示逻辑运行监测页面我单独做了一个 Web 页面用表格展示所有车辆的实时状态。表格中包含车辆编号、所属线路、当前站点、速度、状态、最后上报时间。状态列会根据告警类型显示对应颜色监控人员一眼能看到哪辆车出了问题。表格数据由后台接口返回前端可以手动刷新。我没有过度复杂化没有做自动 WebSocket 推送因为监测场景下每分钟看一次足够。这个页面的重点不是炫酷而是把异常判定结果清晰地呈现出来。毕业设计答辩时我直接投屏到电脑上一边让 Android 模拟器里的车跑一边在监测后台里看到对应车辆的位置跳动和状态变化闭环演示的效果非常直观。6. 开发中遇到的四个硬骨头和排查过程6.1 模拟器位置不动定位模块像哑巴第一次跑通地图时我在 Android 模拟器里发现当前位置始终固定在一个点不动像没收到定位一样。排查过程我分了三步先确认模拟器是否开启了 GPS 模拟定位结果模拟器默认没有再确认代码里是否用了FusedLocationProviderClient发现用了但这个依赖在模拟器上确实需要额外设置最后检查 Android 模拟器的 Extended Controls发现有个 Location 选项卡可以手动输入经纬度或者播放轨迹文件。我的解决办法是提前录制一条沿线路移动的 GPX 轨迹在演示时让模拟器播放这条轨迹。这样在地图上就能看到蓝色圆点沿线路移动配合模拟车辆 Marker整个演示效果一下子活了。这个坑不踩一次根本意识不到模拟器定位和真机定位的差异。6.2 接口请求从 Android 端访问本地后台失败开发时我把后台跑在本机 8080 端口Android 端却怎么都请求不到数据。后来发现 Android 模拟器里的localhost并不是电脑本机而是模拟器自己。这时要用10.0.2.2才能访问宿主机的服务。还有一个隐蔽问题是 Android 9 起默认禁止明文 HTTP 流量本地调试接口是 HTTP 的需要声明允许明文流量。这个如果不处理请求会被直接拦掉报错信息还不直观。!-- network_security_config.xml -- network-security-config domain-config cleartextTrafficPermittedtrue domain includeSubdomainsfalse10.0.2.2/domain /domain-config /network-security-config我用的是这个方案只对调试域名放开明文其他域名保持默认安全限制。有同学图省事直接给整个应用开明文流量也能跑通但不够严谨答辩时容易被追问安全策略。6.3 地图 Marker 越来越多界面越来越卡发现这个问题是在跑了二十分钟模拟器之后地图上车辆一多拖动地图时明显掉帧。原因是早期版本我把车辆 Marker 全部放一个集合每次刷新clear()后重新加回去HashMap 引用变成了脏数据导致旧 Marker 没有被及时回收。后来我改成按 busId 复用的方案后帧率恢复到流畅。这个优化做起来不难关键是意识到问题出在反复创建和销毁对象而不是地图本身渲染能力不足。写 Android 的同学应该养成一个习惯任何频繁更新的 UI 元素优先考虑复用而不是重建。6.4 后台服务被系统回收导致数据停止刷新车辆模拟器是在 Android Service 里跑的还是典型的后台任务。测试时发现锁屏一段时间之后车辆位置冻结了打开手机一看Service 被系统回收了。原因是 Android 对后台进程有省电限制长期不活跃的后台服务会被杀掉。我的解决办法是提醒用户保持应用在前台运行并且把模拟刷新逻辑放到应用可见状态再做。这个局限我当时就意识到了答辩时也直接说了毕设演示阶段用前台 App 展示后续如果要支撑后台长期监控需要引入前台服务或改用服务端定时任务。把这种已知边界主动讲出来评委反而觉得你有工程判断力。7. 答辩准备把项目讲出层次感7.1 演示流程要有B计划答辩现场最容易翻车的不是代码而是演示环境。投影仪分辨率、模拟器性能、网络、定位都有可能出现问题。我给自己定了三条演示铁律第一条所有页面都要准备截图。万一现场环境出问题至少能投屏截图把流程讲完整第二条模拟器轨迹播放器提前切到手动模式防止它一到现场就自动播放导致车辆跑出范围第三条本地后台和 Android 端之间用无线局域网连接提前在答辩教室测试一遍避免临时切网络导致接口不可用。因为提前做了这些准备我答辩时整个演示过程只用了十分钟就完整走完没有出现等待加载白屏的尴尬场面。7.2 评委最关心的五个问题我提前写好了回答我整理了一份高频问题清单每个问题都给出了明确的回答方向常见提问回答思路数据是真实来源吗明确是模拟数据源接口层已抽象可无缝切换真实数据实时性如何保证5秒轮询一次满足公交场景源码里有刷新逻辑说明偏航算法怎么实现点到折线最短距离80米阈值分车辆独立判定为什么不用WebSocket复杂度与场景不匹配轮询足够扩展方向已论证系统有哪些不足未做多城市支持、未做ETA预测、无真实数据接入已留接口扩展这些问题本身不难但很多人因为平时只埋头写代码没有想过为什么这么设计的答案现场支支吾吾。提前把选型理由写下来答辩会从容很多。7.3 复盘时觉得最值的工作量分配整个项目做下来最花时间的是数据层和异常判定不是界面。很多同学做这类题目会把大量时间耗在调 UI 样式上我却把重点放在了三块模拟真实运动的车辆、刷新性能优化、偏航掉线判定。这三块内容恰好是论文里最能展开写算法与方案的部分。我最大的感受是毕业设计选一个有实时感和可视化的题目收益远远大于做十个普通管理系统。公交动态速查系统既用到了 Java 面向对象能力又涉及 Android 多线程、网络请求、资源复用后面还接上了运行监测的后台判断。链路完整技术点密集答辩时就很容易把话题引到自己熟悉的领域。如果你也在做类似方向我给你一个最实际的建议尽早把技术选型理由写进文档开发过程中遇到的每一个 bug 都记录解决方案。这些不只是论文素材更是答辩时自信的来源。