景区导航App开发实战:Spring Boot + Android 全流程

发布时间:2026/10/14 17:16:06
景区导航App开发实战:Spring Boot + Android 全流程
在景区里打开导航是最让人崩溃的事情之一地图上明明显示“您已到达目的地”可你面前只有两米高的灌木丛和一脑袋的问号。城市导航在景区失灵几乎是常态道路数据缺失、卫星信号被山体遮挡、POI分布稀疏这些问题的根源在于通用地图服务商不会为一个景区单独维护路网数据。所以当A同学提出用Spring Boot做后端、Android做客户端给青屿山景区定制一套旅游导航系统App时我几乎没有犹豫就接了这个项目。这个项目从立项到交付调试大约用了六周中间踩过的坑、推倒重来的模块、真机联调时的连环翻车都值得单独写一篇文章记录。如果你正好在准备毕业设计或者想完整走一遍“Android客户端 Spring Boot服务端”的实战流程这篇内容可以作为参考蓝本。我会把架构设计、数据库建模、关键接口实现、Android端地图接入、前后端联调排错以及打包交付前最容易忽略的细节全部拆开讲尽量做到拿来就能用。1. 立项前的调研景区导航App到底要解决什么问题1.1 景区导航和城市导航的差异到底在哪通用地图在城市里好用是因为城市路网数据完整有道路等级、有车道信息、有门牌号而且建筑遮挡相对可控。但换成山区景区情况完全不同。山上的步道、栈道、缆车线路通用地图上要么没有要么与实际走向偏差很大卫星信号经过山体反射后定位点会左右漂移十几米再加上景区里岔路口多、标识不全游客经常在某个“三岔路口”不知道该往哪走。这个项目要做的不是把通用地图搬进App而是围绕青屿山景区自建一套路网数据和景点POI体系再通过Android端的地图SDK把路线绘制出来。简单说我们要做的是“景区专用导航层”底层仍使用第三方地图SDK负责定位和底图展示但路线数据、景点位置、导航提示全部由我们自己的Spring Boot后端维护。还有一个容易被忽略的问题偏远景区网络状况普遍不稳定。如果每一次坐标请求都依赖实时网络App基本没法用。所以缓存策略是需求文档里单独列出来的一条核心约束后面我会详细讲我做的四级缓存方案。提示立项阶段如果只盯着“导航”两个字很容易把项目做成一个地图Demo。一定要先梳理清楚哪些数据必须服务端控制、哪些逻辑可以放本地、弱网下App如何降级表现。这个因素直接决定了后期开发会不会返工。1.2 功能范围怎么裁剪砍掉哪些、保留哪些需求评审时列出来的功能远比我最后做的多。AR实景导航、语音讲解、在线购票、实时客流热力图、UGC游记社区……每一个听起来都很加分但都能单独开一个更复杂的项目。作为以Spring Boot和Android为核心链路的实战项目我更倾向于把链路做完整而不是把功能面铺开。最终保留了六个功能模块用户注册与登录基于JWT的无状态认证包含密码加密存储和Token过期刷新机制。景点列表与详情分页加载青屿山景点数据展示图片、介绍、开放时间、建议游玩时长。地图定位与景点标注进入地图页自动定位把附近景点以Marker形式标注在底图上。路线规划与导航轨迹选择起点和终点后端返回路线坐标点集客户端绘制轨迹并提供转向提示。景点评论与评分登录用户可以发表评论和打分列表按时间倒序展示。收藏管理用户收藏景点个人中心查看收藏列表。砍掉的功能不是不好而是不符合这个项目的技术目标。比如AR导航需要额外的视觉识别能力语音讲解需要大量音频资源制作在线购票则牵扯支付渠道和退款流程。一个项目能在六周内做完整、能演示、能答辩比堆砌一堆半成品功能重要得多。2. 后端Spring Boot接口与数据表先定好传输协议再写界面2.1 五张核心表怎么设计数据库设计是整个项目的地基。我的原则是能用最简单的表结构表达清楚业务关系就坚决不加冗余字段。青屿山导航系统后端一共五张核心表。用户表主要存账号信息、密码盐值和用户昵称。密码字段存储的是加盐后的哈希值数据库不保存明文密码。景点表是内容核心包含经纬度、区域名称、封面图URL、开放时间、建议时长。经纬度存储有一个大学问Android端第三方地图SDK返回的坐标是GCJ-02坐标系如果后端用GPS原始WGS-84坐标两者混用会导致Marker偏出去好几十米。我统一采用服务端只存GCJ-02坐标客户端直接使用不做转换。路线表用来存储一条推荐游览路线的节点信息。表里面除了路线基本信息外专门设计了route_points字段以JSON数组存储该路线的全部途经点坐标包括经纬度、节点名称、节点类型。导航轨迹绘制本质就是解析这个JSON再把坐标点连成线。评论表和收藏表就是常规的主从关系表评论关联用户ID和景点ID收藏也类似。所有外键关系通过索引维护没有物理外键。表结构上值得多说一句的是业务表都加了create_time和update_time两个审计字段虽然会让插入语句稍微麻烦一点但在排查数据和联调问题时能省下大量时间。2.2 JWT登录认证用一个拦截器解决所有受保护接口用户认证我选择JWT而不是Spring Security Session理由很简单Android客户端是无状态的Token交互比Session Cookie方便得多而且毕业设计答辩时讲JWT的认证流程思路清晰、容易展开。后端引入jjwt库拦截器处理流程如下用户调用登录接口校验用户名密码通过后生成Token返回客户端。Token负载部分只放userId和username过期时间设置为2小时。自定义HandlerInterceptor在preHandle阶段从请求头Authorization中解析Token。校验签名和过期时间通过则把userId写入request属性后续Controller直接取用失败则返回401状态码。注册拦截器时放行登录、注册、景点列表、景点详情等不需要认证的接口。核心代码很短配置拦截器时有一个细节容易忽略拦截器的排除路径必须同时包含GET和POST两种方式。如果只写了路径没写HTTP方法会出现登录接口被拦截器误伤的奇观。注册接口的密码处理用的是加盐SHA-256每次注册生成随机盐值密码哈希值存储到数据库。这样做的主要目的是防止彩虹表攻击虽然加了盐也不能算绝对安全但在这个项目级别已经足够了。2.3 统一返回结构与分页接口设计接口协议不统一前后端联调就是灾难。后端从第一天起就固定了统一的返回结构Result 包含code、message、data三个字段。code为200表示成功401表示未认证500表示服务端异常。Android端所有的网络响应解析都建立在这个统一结构上不需要针对每个接口单独写解析逻辑。景点列表接口是一个典型的分页接口参数为pageNum和pageSize返回结构包含当前页数据、总条数和总页数。这里有一个实际开发中容易忽略的点分页返回的景点列表字段里有一个is_favorite标记表示当前登录用户是否已经收藏过该景点需要在SQL查询时根据userId做left join判断。一开始我忽略了这个字段导致列表页的收藏按钮状态显示错乱后来才补上。接口文档我用OpenAPI生成启动项目后访问Swagger UI就能看到每个接口的入参、出参和错误码说明。文档除了给自己看更重要的作用是给Android端开发同学也就是另一个身份的我自己提供联调依据不用每次翻代码去猜字段含义。3. Android端四个核心模块从列表到地图的完整实现3.1 网络层搭建Retrofit OkHttp Token拦截器Android端网络层我采用Retrofit 2 OkHttp Gson的组合。这个组合在Android生态里足够成熟Retrofit负责接口定义和响应转换OkHttp负责底层HTTP连接和拦截器Gson负责JSON序列化反序列化。ApiService使用接口加注解定义所有后端接口public interface ApiService { POST(api/user/login) CallResultUserInfo login(Body LoginRequest request); POST(api/user/register) CallResultString register(Body RegisterRequest request); GET(api/spot/list) CallResultPageResultSpotItem getSpotList( Query(pageNum) int pageNum, Query(pageSize) int pageSize ); GET(api/spot/detail/{id}) CallResultSpotDetail getSpotDetail(Path(id) int id); POST(api/review/add) CallResultString addReview(Body ReviewRequest request); }重点是OkHttp的Token拦截器。用户登录成功后Token保存到SharedPreferences发起请求时拦截器从SharedPreferences取出Token添加到请求头。如果请求返回401拦截器捕获响应后回调一个事件通知界面跳转到登录页。这个拦截器还有一个作用统一处理弱网重试。OkHttp的retryOnConnectionFailure方法配合合理的超时时间可以在Wi-Fi切换或信号波动时自动重试一次别小看这一次重试真实景区场景下经常就是这么一次重试救回了请求。BaseUrl的配置我单独拎到了BuildConfig里debug环境指向局域网IPrelease环境指向云服务器域名。后面打包部分会详细说这个配置的意义。3.2 地图SDK接入与路线绘制注意坐标系一致地图模块是整个App技术含量最高的部分也是我花时间最多的地方。Android端接入的是第三方地图SDK应用启动时在Application的onCreate中完成初始化地图页加载时开启定位。定位权限处理有个很容易被忽略的细节Android 6.0以上必须动态申请权限Android 12以上还需要精确定位权限的额外声明。如果只检查了ACCESS_FINE_LOCATION真机在Android 12上定位会静默失败控制台连错误日志都没有。定位成功后拿到当前坐标和景区边界用SDK的标记管理类把景点以Marker形式添加到地图。这里要特别注意后端返回的景点经纬度必须和地图SDK使用同一个坐标系。我在2.1节已经说过后端统一存储GCJ-02坐标客户端拿到后直接使用不要自作主张再转换一次否则Marker会偏移到奇怪的地方。路线绘制的核心逻辑是根据路线ID请求后端接口拿到route_points坐标点数组遍历数组构建PolylineOptions对象设置线段颜色和宽度后添加到地图。转折点处加方向箭头终点处添加终点Marker。导航提示做成了Toast形式在轨迹每个关键节点坐标附近触发保证用户不用一直低头看屏幕。真机测试时我发现模拟器上一切正常但真机上Marker点击事件会偶发丢失。排查后定位为Marker被地图的默认点击事件消费了需要在SDK的OnMarkerClickListener回调里返回true表示事件已消费不再继续往下传递。3.3 景点列表、详情和评论RecyclerView的常规与不常规景点列表页就是一个标准的RecyclerView CardView列表图片加载用Glide。这里要提一个从实际测试中总结出来的经验景区封面图原图动不动3-5MB在弱网环境下加载慢得让人怀疑App崩了。Glide的override方法把图片尺寸限制到400dp宽后端接口同时返回大图和小图两个URL列表页用小图详情页再用大图图片流量能缩小80%以上。详情页除了景点介绍外还放了评论区列表和评分展示。评论列表同样使用RecyclerView内嵌了一个“写评论”的浮动按钮点击后弹出底部对话框。评分用Android的RatingBar组件默认星星颜色太淡在深色背景上几乎看不见需要设置自定义starDrawable这个也是实测后调整的。评论提交的接口有一个隐藏校验登录状态过期后后端会返回401。如果客户端没有全局处理401跳转用户会感觉“点了提交没反应”其实是请求已经失败但界面上没有任何提示。把401处理统一定义在网络层拦截器里非常必要现在几乎成了我自己所有项目的标配。收藏按钮在列表页和详情页各有一个状态同步是一个小坑。设计思路是列表页的is_favorite字段只作为初始展示点击收藏按钮时立即更新本地UI同时调用后端接口。如果接口失败再回滚按钮状态并弹出提示。这种做法牺牲了一点严格一致性换来的是操作响应快弱网环境下不会出现“点了半天按钮没反应”的挫败感。4. 前后端联调排查那些在机房试不出来、真机才暴露的问题4.1 明文流量限制、局域网IP与JWT过期的连环坑联调阶段遇到的第一堵墙是Android的明文流量限制。从Android 9开始应用默认禁止使用HTTP明文协议访问网络而后端开发环境跑在http://192.168.x.x:8080。第一次用真机调试时所有请求全部报CLEARTEXT communication not permitted接口一个都打不通。解决方案有两种直接设置android:usesCleartextTraffictrue允许所有明文流量或者配置networkSecurityConfig只允许特定域名的明文访问。我用了第二种只让debug环境指向的局域网IP走明文release环境默认走HTTPS。这样既解决了调试问题也不会在正式包上留下安全隐患。第二堵墙是局域网IP访问不到。开发机和手机连了同一个Wi-Fi但后端的Spring Boot只监听localhostAndroid端当然连不上。排查到这一步时需要在application.yml里修改server.address0.0.0.0让服务对外可见。同时Windows防火墙也要放行8080端口否则请求会卡在连接超时。第三堵墙是Token过期后的连锁反应。我的Token有效期设成2小时调试时触发一次过期后所有受保护接口都返回401但客户端全局拦截器没有做“跳转登录页”的动作而是把错误弹窗展示出来了体验非常糟糕。后来我实现了401统一处理拦截到401时清空本地Token发送一个登录失效的全局事件所有Activity通过观察者模式监听到事件后统一跳转登录页。4.2 山区弱网环境的四级缓存方案青屿山景区面积不小部分区域4G信号只有一格甚至完全断网。Android端如果不做缓存断网状态下App基本变成一块废铁。我最终实现了四级缓存方案。第一级是内存缓存。HashMap保存最近访问的景点详情数据LRU策略淘汰适合在同一会话中快速返回上一页再进入的场景。第二级是磁盘缓存。用文件缓存保存景点列表的JSON响应设置5分钟过期时间列表页优先读缓存同时后台请求最新数据并更新UI。这个策略让用户在信号不稳定区域依然能浏览到景点列表。第三级是SharedPreferences保存用户偏好类数据比如登录Token、主题模式、搜索历史。Preferences是在Android里最轻量的本地存储方式适合存少量键值对。第四级是离线地图包。这个项目因为时间关系只做了基础支持用户可以在Wi-Fi环境下提前下载青屿山景区的离线底图切片断网时地图底图依然可以显示但动态路线请求会失败并给出友好提示。实现缓存时有一个顺序问题网络请求返回成功后先写缓存再更新UI还是先更新UI再异步写缓存我最终选择后者因为先更新UI可以让用户感觉响应更快。缓存写入正常情况下耗时极小放在异步线程里执行即可。5. 打包交付前必须做的三件事签名、环境切换和文档沉淀5.1 签名打包与多渠道环境配置开发调试期间用的是Android默认的debug签名但交付时一定要配置独立的release签名文件。Android Studio里签名的生成路径很直观Build菜单下Generate Signed Bundle/APK按向导创建一个keystore文件配置好别名和密码即可。有一个建议是把签名文件信息写入gradle配置里的signingConfigs并设置storePassword和keyPassword通过环境变量获取不要把明文密码提交到代码仓库。环境切换是我在无数个项目里反复强调的问题。开发环境连局域网IP测试环境连测试服务器正式环境连线上服务器。如果每次打包前手动改代码里的BaseUrl改动成本低但容易漏改还会出现忘记切换导致正式包连了测试环境的情况。我用BuildConfig字段区分环境在build.gradle里根据buildType设置不同的BASE_URL代码里统一从BuildConfig读取。这样打包release版本时自动使用正式地址完全不需要人为干预。5.2 我所总结的十几条实用避坑清单最后这份清单算是这个项目最大的副产品每一条都是在真机或交付过程中真实踩过、修过的坑分享给你直接照着避雷。坐标体系必须统一。Android端地图SDK使用GCJ-02后端数据必须存GCJ-02不要混用WGS-84。权限动态申请要覆盖Android 12及以上版本只申请定位权限不够还要检查精确定位的权限声明。OkHttp超时时间不能统一设成默认值复杂网络环境下建议connectTimeout 10秒、readTimeout 15秒过短会误判失败过长会卡界面。列表图片务必做尺寸压缩和缩略图方案否则弱网环境直接劝退用户。结果集分页接口必须返回总条数否则客户端无法判断是否还有下一页。接口字段命名统一使用驼峰后端Java和Android端Java都是驼峰习惯JSON序列化保持默认即可。JWT的Token生成时加入客户端类型字段Android/iOS方便后续做多端区分和统计绕过率。注册接口要加验证码机制至少也要做请求频率限制否则容易被打接口刷垃圾账号。数据库所有表加create_time字段排查问题时没有这个字段真的会想哭。配置文件里的密码、密钥不要出现在代码里用环境变量或jasypt加密。Android的Gradle依赖版本要固定不要用动态版本号加号团队协作时版本冲突会让你怀疑人生。定时任务如果有自动刷新Token的能力心跳接口和Token刷新接口需要做好并发控制避免同时发起多个刷新请求。Swagger接口文档一定要标注每个字段的示例值给队友联调时可以少问十次“这个是什么”。我做完整交付时最大的体会是一个项目能不能用不看它堆了多少功能而是看它在不理想的环境下还能不能保持基本可用。青屿山旅游导航系统虽然只覆盖了核心链路但至少做到了打开App能看到景点、能定位不漂移、能断网浏览缓存、登录状态清晰可控。把这条路走通之后后面想扩展实时客流、语音播报或者其他功能都只是往这个稳定骨架上填肉的问题。