做旅游类App的毕业设计,每年都有一大批人选,但真正能在答辩现场把"设计"两个字讲明白的并不多。我见过太多项目,源码能跑、页面能点,一问到"你这个推荐逻辑为什么这么排""这个缓存为什么放这一层",就卡住了。这篇东西想聊的是西藏旅游App设计与实现这条完整链路:从开题报告怎么立题、功能模块怎么划边界、技术栈怎么选、数据库怎么落地,一直到源码联调阶段那些文档里不会写的故障。关键词里带了"源码"和"开题",说明读者多半是要自己动手做一套能交付的东西,所以我尽量把每一步的取舍理由写出来,而不是只给一份功能清单。不管你是刚开始找选题,还是代码写了一半发现架构撑不住,下面这些内容应该都能对得上。
1. 这个题目真正的难点不在"西藏",而在"旅游App"这四个字
很多人拿到这个题目,第一反应是"地域特色"——觉得难点在于怎么把高原风光、民俗元素做进界面里。我一开始也这么想,后来发现完全想偏了。界面上的地域元素是最容易做的部分,换几张图、调一套配色、写几段介绍文案,两三天就能出效果。真正卡人的是"旅游App"这个品类本身的复杂度:它是一个多角色、多状态、强时空关联的系统,比你做的图书管理、学生成绩管理要难一个量级。
1.1 为什么同类题目扎堆却少有能讲清楚设计意图的
图书管理系统的本质是增删改查,业务状态是线性的:借出、归还、逾期,三条分支就覆盖完了。旅游App不是。一个用户从"想出去玩"到"玩完回来",中间要经历目的地筛选、行程编排、票务预订、导航到达、现场消费、回来评价六个阶段,每个阶段的数据形态都不一样。行程编排阶段是"计划数据",预订阶段是"交易数据",导航阶段是"位置数据",评价阶段是"内容数据"。这四类数据的生命周期、一致性要求、查询模式完全不同,硬塞进一套CRUD接口里,后期一定会崩。
我见过一个比较典型的做法:把所有东西都做成"信息表 + 增删改查",景点是表、酒店是表、订单是表、评论是表,接口一律select * from xxx。演示的时候没问题,因为数据量只有二三十条。但答辩老师只要问一句"用户按距离排序附近的景点,你怎么实现",就露馅了——因为经纬度根本没建索引,排序靠的是全表扫描后在内存里算。
所以这个题目的第一个分水岭是:你有没有意识到自己在做一个空间数据系统,而不是一个内容展示系统。意识到这一点,后面的表结构、接口设计、缓存策略才会跟着变。
1.2 从"能演示"到"有工作量"的分界线在哪
毕业设计有个很现实的问题:老师要看工作量,但你的时间只有两三个月。这两者之间的平衡点,我总结成一句话——功能数量控制在十二个以内,但至少有三个功能要做出技术纵深。
什么叫技术纵深?举几个例子对比:
| 功能 | 浅做法(工作量低) | 深做法(有技术含量) |
|---|---|---|
| 景点列表 | 分页查全表返回 | 按地理距离排序 + 分类筛选 + 图片懒加载 + 本地缓存 |
| 行程规划 | 用户手动一条条添加 | 按天分配、按距离就近排序、冲突检测、拖拽调整顺序 |
| 地图导航 | 点击跳转第三方地图 | 应用内展示、地理围栏到达提醒、路线概览 |
| 订单 | 提交后改状态字段 | 库存扣减 + 超时自动取消 + 状态机流转 |
| 天气 | 静态写死的文案 | 按景点坐标请求、缓存有效期控制、异常降级 |
左边一列是"能演示",右边一列是"有工作量"。做完右边一列的三到四个,你的项目在同类里就站得住了,而且答辩时有东西可讲。剩下的功能用浅做法补数量,别贪多。
这里有个我踩过的坑要提醒:不要一上来就把功能清单列到二十几个。我当时列了二十四项,写到最后发现光是把菜单页做出来就花了两周,剩下的功能全靠"占位页 + 敬请期待"糊过去。后来重做的时候砍到十一个功能,每个都跑通了,整体完成度反而高得多。功能清单是给自己看的排期依据,不是给老师看的炫技清单。
2. 功能清单不是越长越好:把旅游场景拆成三条主线
功能怎么划,我的建议是别按"模块名"划,按"用户旅程"划。模块名(景点模块、订单模块、用户模块)是给数据库看的,用户旅程才是给产品看的。这两者的区别在于:按模块划,你写出来的接口是散的;按旅程划,接口天然有先后顺序,联调时能一条线走通。
2.1 行前、行中、行后:用户旅程决定模块边界
行前阶段的核心诉求是"降低决策成本"。用户面对一个陌生目的地,最想知道三件事:有什么值得去、怎么安排顺序、大概花多少钱。对应到功能上就是:目的地/景点浏览与筛选、行程编排、费用预估。
行中阶段的核心诉求是"降低操作成本"。人在路上,手不方便,网络还不一定好。这个阶段的功能设计原则是一屏解决一件事:当前在哪、下一站去哪、怎么过去。对应功能是:附近景点(按距离排序)、地图与到达提醒、离线缓存。
行后阶段的核心诉求是"沉淀内容"。用户可以发评价、发照片、收藏行程。这部分工作量不大,但对项目的完整性很重要,因为它是"数据闭环"的最后一环——你的数据库里终于有了用户产生的内容,而不全是自己录入的假数据。
按这三条线一切,你会发现之前想做的"积分商城""会员等级""社交关注"全被自然淘汰了——它们不属于任何一条主线,属于锦上添花,砍掉不心疼。这就是按旅程划分的好处:它自带优先级。
2.2 一份可落地的模块清单与优先级排序
下面这份清单是我实际做完之后总结的,P0 是必须做,P1 是做完了 P0 有余力再加,P2 是答不出来也不要紧的。你看这个分布就知道,P0 只有七个,是能压在两三个月里做完的量。
| 优先级 | 模块 | 关键能力 | 预估工时 |
|---|---|---|---|
| P0 | 账号体系 | 注册登录、Token 鉴权、资料编辑 | 5 天 |
| P0 | 景点浏览 | 列表分页、分类筛选、距离排序、详情页 | 8 天 |
| P0 | 行程规划 | 多天分组、添加移除、排序调整、费用合计 | 10 天 |
| P0 | 地图与定位 | 应用内地图、当前定位、景点标注 | 6 天 |
| P0 | 订单 | 下单、库存扣减、状态流转、超时取消 | 8 天 |
| P0 | 评价 | 图文发布、列表展示、评分聚合 | 4 天 |
| P0 | 离线缓存 | 景点数据本地化、图片缓存 | 4 天 |
| P1 | 天气提示 | 按坐标拉取、缓存降级 | 3 天 |
| P1 | 收藏 | 收藏行程与景点 | 2 天 |
| P2 | 智能推荐 | 基于浏览历史的简单加权 | 视情况 |
这里面有个容易被忽略的点:行程规划和订单是仅有的两个"有状态"模块,也是答辩老师最爱问的地方。行程规划涉及数据结构的组织(怎么存多天、怎么保证顺序),订单涉及并发与一致性(库存扣减)。这两个模块做好了,你的项目就从"页面堆砌"升级成"系统设计"。
我在做行程规划的时候第一次用的是"一个大 JSON 塞进一个字段"的方案,好处是简单,坏处是不能排序查询、不能单独删某一条、不能做统计。后来改成"行程主表 + 行程明细表"的结构,明细表里带day_index和sort_order两个字段,问题全解决了。这个改动花了我差不多一天,但省下了后面无数麻烦。
3. 技术选型:开题报告里写的栈,和你能写完的栈往往不是同一个
开题报告阶段有个很普遍的现象:为了显得"技术先进",写上一堆流行框架,什么微服务、消息队列、容器编排全堆上去。结果真动手的时候发现,光是让服务端跑起来就卡了一周。我的建议很直接:开题报告里写的每一个技术名词,你都得能说清楚它在你的项目里解决了什么具体问题,否则就删掉。
3.1 客户端三种路线的取舍
客户端这块,常见三条路:原生 Android 开发、跨端框架、小程序。三条路各有各的适用场景,不是"哪个先进选哪个"。
| 路线 | 优势 | 劣势 | 适合什么情况 |
|---|---|---|---|
| 原生 Android | 地图、定位、相机等设备能力调用最直接,性能最好 | 只能跑一个平台,代码量大 | 你只做 Android,且地图交互是核心 |
| 跨端框架 | 一套代码跑两端,UI 开发效率高,热重载体验好 | 部分原生能力需要插件桥接,包体积偏大 | 想做双端,且对设备能力依赖不深 |
| 小程序 | 免安装、有现成的定位与地图组件、演示方便 | 需要额外的主体资质,审核周期不可控 | 只做课程演示,不追求独立 App 形态 |
我自己选的是跨端方案,原因很实在:地图、定位、相机这些能力都有成熟的官方插件,不需要自己写原生桥接;而 UI 部分的开发速度是原生的两倍以上。对于一个人两三个月要交付的项目,开发效率的权重远高于极致性能。
选跨端要注意一个细节:状态管理别搞太复杂。我一开始学着大项目用了一整套状态管理方案,写到一个页面要跨三个状态容器,调试的时候根本不知道数据是从哪个环节丢的。后来退回到"页面级状态 + 一个全局用户态",代码量少了一半,bug 也少了。小项目用小方案,这是经验。
3.2 服务端与数据层的最小可用组合
服务端选型的原则是"少组件、少中间件、少运维"。一个人开发,你既是前端又是后端还是运维,多引入一个组件就多一个可能出故障的地方。
我的组合是:单体服务端 + 关系型数据库 + 一个内存缓存 + 对象存储。就这四个。没有消息队列,没有服务注册,没有网关,没有容器编排。所有需要"异步"的地方,用数据库的状态字段 + 定时任务轮询来实现,比如订单超时取消,就是一个每分钟跑一次的定时任务扫超时订单。这个方案在数据量小的时候完全够用,而且排查问题极其直观——出问题就看数据库,一眼就知道卡在哪。
数据层用关系型数据库而不是文档型,理由是旅游业务的数据关联性很强:订单关联用户和产品,行程明细关联行程主表和景点,评价关联订单和用户。这种多对多、一对多的关系,用关系型数据库的外键和连接查询最省心。文档型数据库在嵌套结构上是舒服,但一旦要"查某个用户所有订单里涉及的景点"这种跨集合查询,就很别扭。
缓存这块只做一件事:把首页和景点列表这类高频读接口的返回结果缓存起来,设置较短的有效期。别一上来就设计多级缓存、缓存穿透布隆过滤器这些东西,用不上,还容易写出数据不一致的 bug。我踩过的最典型的一个:更新了景点信息但忘了清缓存,前端一直显示旧数据,查了两个小时才想起来是缓存没过期。
4. 数据库设计:五张表撑起整个业务闭环
数据库设计是这个项目里最能体现功力的部分,也是答辩时最容易被追问的部分。我建议你在动手写代码之前,先把表画出来,反复问自己三个问题:这张表的主键是什么、它和谁关联、它会被怎么查。
4.1 核心实体关系与字段设计
整个系统拆到最小,五张核心表就能撑住:用户表、景点表、行程表、行程明细表、订单表,再加一张评价表做闭环。
景点表是整个系统的数据底座,字段设计上最需要花心思的是位置信息。经纬度必须单独存成两个字段,而且必须建索引——因为"按距离排序"是这个 App 最高频的查询之一。别把经纬度拼成一个字符串存,那样既不能算距离也不能排序。
CREATE TABLE spot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(80) NOT NULL COMMENT '景点名称', category VARCHAR(32) NOT NULL COMMENT '分类:自然/人文/城镇', latitude DECIMAL(10,6) NOT NULL COMMENT '纬度', longitude DECIMAL(10,6) NOT NULL COMMENT '经度', altitude INT DEFAULT NULL COMMENT '海拔,米', cover_url VARCHAR(255) DEFAULT NULL, intro TEXT, ticket_price DECIMAL(8,2) DEFAULT 0.00, open_time VARCHAR(64) DEFAULT NULL, score_avg DECIMAL(3,2) DEFAULT 5.00 COMMENT '评分聚合值', status TINYINT DEFAULT 1, INDEX idx_geo (latitude, longitude), INDEX idx_category (category, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;行程明细表是另一个重点。它承担"多天分组"和"顺序调整"两个职责,所以必须带day_index和sort_order:
CREATE TABLE trip_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, trip_id BIGINT NOT NULL, spot_id BIGINT NOT NULL, day_index INT NOT NULL DEFAULT 1 COMMENT '第几天', sort_order INT NOT NULL DEFAULT 0 COMMENT '当天内顺序', cost DECIMAL(8,2) DEFAULT 0.00, note VARCHAR(255) DEFAULT NULL, INDEX idx_trip_day (trip_id, day_index, sort_order) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表的关键是状态字段要设计成状态机,而不是随便几个数字。我用的是 0 待支付、1 已支付、2 已使用、3 已取消、4 已退款。每一个状态转换都必须有明确的触发条件,比如"待支付"超过 30 分钟自动变"已取消",这个转换由定时任务触发。把状态画成图贴在文档里,答辩的时候拿出来讲,非常有说服力。
4.2 索引、冗余与那些后期一定会改的地方
有几个设计细节,我当时纠结了很久,最后的选择供你参考。
第一,评分要不要冗余。评价表里每条有分数,景点表里我另存了一个score_avg聚合值。这是典型的空间换时间——查询景点列表时不用去连接评价表做聚合,直接读冗余字段。代价是每次新增评价都要更新这个字段。考虑到评价是低频写、景点列表是高频读,这个冗余是划算的。判断冗余的标准就是读写比例,读远多于写就冗余,反之别冗余。
第二,图片存哪里。千万别把图片二进制存进数据库。做法是图片传到对象存储,数据库里只存 URL。这个决定能省掉后面一堆麻烦:备份快、查询快、迁移容易。
第三,软删除还是硬删除。我给景点表加了status字段做软删除,订单和行程用的是硬删除。区别在于:景点是基础数据,被其他地方引用,删了会造成历史数据展示异常;订单和行程是用户自己的数据,用户想删就真删。这个区分在答辩时是个加分项,说明你想过数据引用的问题。
后期一定会改的地方,我提前告诉你:分类字段。一开始我想的是"自然景观/人文景观"两类,做到一半发现用户会按"是否适合带小孩""是否需要长时间徒步"来筛,于是又加了一组标签字段。所以景点表建议从一开始就预留一个tags字段(逗号分隔或者 JSON 都行),把不确定的分类维度先放进去,别急着建关联表。
5. 几个真正值得写进论文的技术点
论文最怕的是"流水账"——第一章需求分析,第二章概要设计,第三章详细设计,第四章测试,全是"我做了个列表页""我做了个详情页"。这类内容写再多也显不出水平。真正能加分的,是把几个有技术含量的小问题讲透,讲清楚你遇到了什么约束、尝试了哪些方案、为什么最终选了这个。
5.1 弱网环境下的图片与数据缓存策略
旅游场景有个绕不开的约束:用户在景区里,网络很可能时好时坏。这不是假设,是常态。所以缓存不是"优化项",是"必需项"。
我的做法分两层。图片层用的是客户端本地磁盘缓存:图片 URL 作为 key,加载时先查本地,命中就直接用,没命中才发起网络请求,成功后再落盘。这里有个细节要注意——必须给缓存设容量上限和淘汰策略,否则用户刷几百张图,App 的体积就膨胀到几个 G。我用的是按最近最少使用淘汰,上限设 100MB。这个数字是估算出来的:单张缩略图压到 80KB 左右,100MB 大约能存 1200 张,覆盖一次完整旅行的浏览量绰绰有余。
数据层用的是两层缓存:服务端把景点列表接口的结果缓存 5 分钟,客户端把最近一次成功返回的列表存进本地数据库。这样即使断网,用户打开列表页看到的也是上次的数据,而不是一片空白。降级提示我放在了页面顶部,一条很轻的横条:"当前为离线数据"。
提示:缓存一定要设过期时间和清理入口。我在设置页加了一个"清除缓存"按钮,看起来是个小功能,但答辩演示的时候很有用——老师看到你考虑了这个,会觉得你在认真做产品而不是交作业。
服务端缓存的伪代码大概是这样,重点是先查缓存、未命中再查库、查完回填这个顺序不能乱:
public List<SpotVO> listByCategory(String category, int page) { String key = "spot:list:" + category + ":" + page; Object cached = cache.get(key); if (cached != null) { return (List<SpotVO>) cached; } List<SpotVO> list = spotMapper.selectByCategory(category, page); cache.set(key, list, 300); // 5 分钟 return list; }看着简单,但坑在于写操作之后必须删缓存。我在新增评价、更新景点信息的两处逻辑里都加了删除对应 key 的代码。漏一处,就会出现"数据更新了但页面不变"的诡异现象。
5.2 基于地理围栏的到达提醒怎么算
"到达景点附近时提醒用户"这个功能,听起来很高级,实现起来其实不难,关键是想清楚计算方式。
最朴素的做法是定时轮询位置:每隔一段时间取一次当前坐标,和所有景点的坐标算距离,小于阈值就提醒。这个方案的缺点是耗电、耗流量,而且位置更新频率低的时候反应慢。
我实际采用的是客户端本地计算 + 距离预筛:先把景点坐标一次性拉到本地(数据量不大,几百个点),定位回调触发时,在本地做一次距离计算。距离计算不用精确的大地测量公式,用等距圆柱投影的近似算法就够了,误差在几百米以内,对于"是否进入 1 公里范围"这个判断完全够用:
// 近似距离计算,单位:米 private double roughDistance(double lat1, double lng1, double lat2, double lng2) { double x = Math.toRadians(lng2 - lng1) * Math.cos(Math.toRadians((lat1 + lat2) / 2)); double y = Math.toRadians(lat2 - lat1); return Math.sqrt(x * x + y * y) * 6371000; }为什么要用近似而不是精确公式?因为这是一个高频调用的函数,在定位回调里每次都要对几百个点跑一遍。精确公式里有大量三角函数,开销大约是近似算法的三倍。而我们的判断阈值是公里级的,近似算法几十米的误差完全可以忽略。这就是典型的"精度换性能",而且是安全的换法。
再补一个细节:提醒要做去重。用户在一个景点附近待半小时,定位回调可能触发几十次,不能每次都弹提醒。我的做法是给每个景点记一个"最近提醒时间",同一个景点在两小时内只提醒一次。
5.3 行程规划的数据结构与排序算法
行程规划是这个小系统里逻辑最绕的地方,值得单独说。
用户的操作有这么几种:往某一天加一个景点、从某一天删一个景点、把景点从第二天拖到第三天、调整某一天内的顺序、查看某一天的总费用和总耗时。每一种操作背后都是对trip_item表的增删和sort_order的重排。
顺序调整的经典坑是"断层"。假设某天有 A、B、C 三条,sort_order分别是 1、2、3。现在删掉 B,剩下 A(1)、C(3),中间断了。下次插入新项时如果取max(sort_order)+1,得到 4,排序结果是 A、C、新项,逻辑上没问题。但如果按"取当前条数+1"来算,得到 3,和 C 冲突了。所以统一用max+1,永远不用"条数+1",这是最简单也最不容易出错的规则。
跨天拖动需要同时改day_index和重排目标天的sort_order。我的实现是把目标天的所有项重新编号(1、2、3……),一次性批量更新。虽然写起来多几行,但保证了不会出现重复序号。
还有一个容易忽略的点是费用合计。每个行程项有cost字段,行程详情页要显示"第 N 天合计"和"全程合计"。这两个值不要每次都去数据库聚合,而是在返回行程详情时由服务端算好一起返回。因为前端拿到列表后自己算也可以,但一旦分页加载就会算错,统一在服务端算更稳妥。
6. 开题报告怎么写才不像填空
开题报告这块我得多说几句,因为"源码+开题"这个组合说明读者里有一大部分是在准备选题材料。开题报告最容易犯的毛病是空话密度太高:研究背景写三段宏观形势,技术路线写五个框架名字,进度安排画个甘特图,创新点写"界面美观、操作便捷"。老师一眼就能看出来这是模板。
6.1 研究背景与意义的写法陷阱
研究背景别从"旅游业的蓬勃发展"写起,这个开头已经被用了成千上万次,而且和你这个项目没有任何关系——你做一个 App,跟行业大盘增长没有因果关系。背景应该写"具体场景下的具体问题"。
比如可以这样切入:某类旅游目的地的信息获取存在明显的不便利——景点分布在广阔的地理范围内,用户在做行前规划时需要在多个信息源之间来回切换,且已有的行程编排工具大多是通用型的,不针对这类目的地的空间分布特征做优化。这就是一个具体问题,而且它直接对应到你要做的"按距离排序 + 行程编排"这两个功能。
研究意义也一样,别写"促进旅游业发展",写你自己的项目能带来的直接改变:把分散的景点信息、距离关系、行程编排整合到一个入口,减少用户在多来源之间切换的成本。这句话就是你的功能目标,答辩时也能拿它当"设计依据"来讲。
我个人的经验是:背景和意义加起来不要超过八百字,但每一句都要能对应到后面的某个功能或某个设计决策。写完之后做个自检——随便挑一句话,问自己"这句话对应我哪个功能",对应不上的就删。
6.2 技术路线、进度安排与创新点的可信度
技术路线部分最忌讳"名词罗列"。不要写"采用前后端分离架构,前端采用跨端框架,后端采用主流服务端框架,数据库采用关系型数据库"——这是选型结果,不是技术路线。技术路线要写的是数据怎么流、模块怎么分、关键问题怎么解。
比较可信的写法是这样组织:客户端负责界面渲染与设备能力调用,服务端提供 RESTful 接口,数据层负责持久化,缓存层负责高频读接口加速;客户端与地图能力之间通过官方插件桥接;行程、订单等有状态业务的数据一致性由服务端保证。这样写,每一句都能在代码里找到对应物。
进度安排要避免"平均分配"。很多人的安排是每周做一行,看起来整齐,实际上完全不真实。真实的开发节奏是前松后紧、中间有返工。我的建议是按阶段划分,并且明确标出风险阶段:需求与原型 2 周、数据层与核心接口 3 周、客户端页面 4 周、联调与修复 3 周、文档与答辩准备 2 周。其中"联调与修复"标注为风险最高,因为这一阶段最容易超期。主动写出风险,反而显得你有经验。
创新点这个词有点大,我建议改成"本项目的差异化设计",写两到三条具体的、能被验证的:
- 针对景点空间分布分散的特点,在列表与行程编排中引入距离因素,支持就近排序与就近推荐;
- 针对户外场景网络不稳定的情况,设计了客户端数据与图片的两级缓存以及断网降级展示;
- 将行程按天分组并支持顺序调整,形成可保存、可复用的行程数据,而不是一次性浏览。
这三条都来自实际功能,都能在演示时点出来,比"界面友好、体验流畅"这种话有用一百倍。
7. 联调阶段的典型故障与排查链路
前面都是设计层面的东西,这一节讲讲真刀真枪的阶段。联调是这类项目最容易让人崩溃的环节,因为问题往往不在代码逻辑,而在环境、配置、协议这些"边角"上。我把自己遇到的几个典型故障和排查过程写下来,你能少走不少弯路。
7.1 接口通了但页面空白
这个现象我第一次遇到时折腾了大半天。表现是:用接口调试工具请求接口,返回的数据完全正常,JSON 结构也没问题;但 App 里的列表页就是一片空白,也不报错。
排查链路是这样的,建议按顺序走:
第一步,先确认请求有没有真的发出去。在客户端的网络拦截器里打一行日志,看请求有没有到达。结果是请求发了,但响应体是空字符串。
第二步,看响应状态码。状态码是 200,但内容长度是 0。这说明服务端返回了空内容,但状态码正常。
第三步,看服务端的日志。服务端日志显示查询语句执行了,返回了 12 条记录。问题出在序列化环节——接口返回的是一个包装对象,里面的data字段是List<SpotVO>,但SpotVO里的日期字段在序列化时抛了异常,被全局异常处理器吞掉了,返回了一个空体。
第四步,定位到具体字段。加上详细日志后确认是时间字段的格式问题:数据库里是datetime,实体类里是LocalDateTime,序列化配置里没有注册对应的格式化模块。
这个问题的根因是全局异常处理器把异常静默处理了。这是个很常见的坏习惯——为了"不让用户看到报错",把所有异常都捕获后返回统一提示。结果是问题被藏起来了。我的改法是:全局处理器只处理业务异常,系统异常必须打完整堆栈;同时在开发环境把详细错误信息返回给客户端。上线前再关掉详细信息的开关。
| 现象 | 可能原因 | 验证方式 |
|---|---|---|
| 页面空白无报错 | 返回体为空、字段解析失败 | 看响应体长度、看序列化日志 |
| 列表有数据但不显示 | 字段名大小写不一致 | 对比接口返回字段名与实体字段名 |
| 图片全部裂开 | URL 拼接规则错误、存储桶权限 | 浏览器直接访问图片 URL |
| 点击跳转无反应 | 路由参数类型不匹配 | 打印路由参数 |
另一个高频问题是字段名不一致。服务端返回的是下划线命名,客户端按驼峰解析,结果全是 null。这类问题排查起来很快,但定位之前很折磨人。解决办法是从第一天起就统一命名规范,别指望后面统一改。
7.2 真机与模拟器的行为差异
模拟器跑得好好的,一上真机就出问题,这是移动端开发的经典场景。我遇到的两个:
第一个是定位。模拟器可以随意设置一个坐标,真机上的定位需要权限授权,而且首次定位往往有延迟。表现是页面打开时地图中心点跳到默认位置,而不是用户当前位置。原因是代码里默认认为"定位回调一定会及时返回",没有处理超时。改法是加一个超时兜底:3 秒内没有定位结果,就用景点的中心坐标作为初始中心点,同时提示用户"未获取到定位,已使用默认位置"。
第二个是图片加载。模拟器网络快,图片几乎瞬间加载完成,所以我的加载占位处理写得很随意。真机上网络慢,占位图尺寸和实际图片比例不一致,页面会剧烈跳动。解决办法是服务端返回图片宽高信息,客户端按宽高比预留空间。如果服务端拿不到宽高,就在上传图片时就算好存进数据库。这个改动看似麻烦,但解决了列表滚动时的抖动问题,体验提升非常明显。
还有一个真机特有的问题:应用切到后台再回来,页面状态丢失。原因是后台时系统回收了部分内存。检查一下是不是某个全局状态存在了内存里而没有持久化,比如登录 Token 存的是变量而不是本地存储。改成持久化存储后问题消失。
8. 答辩与源码交付:把工作量讲清楚的几个细节
代码写完之后,还有两件事要处理:怎么讲,怎么交。这两件事做得好,能把你的实际工作量放大;做得不好,做得再多也显得单薄。
演示路线的设计。别从首页开始一个个点菜单,那是"功能罗列"。设计一条真实的使用路径:打开 App 看到附近景点(讲距离排序)→ 选两个景点加进行程(讲行程数据结构)→ 第二天打开看到行程详情和费用合计(讲数据聚合)→ 断网后数据仍在(讲缓存降级)→ 提交一个订单(讲状态机)。一条路线走完,你的核心技术点全讲到了,而且是在真实场景里讲的,比对着 PPT 念有说服力得多。
源码的组织方式。交付的代码结构要能一眼看出分层,客户端按页面、组件、数据访问分层,服务端按接口层、业务层、数据访问层分层。命名要规范,别出现test1、aaa、TestController2。可以写一个简单的说明文档,列出目录结构和每个目录的职责,再附一份"从零启动"的步骤——数据库脚本在哪、配置文件要改哪几项、依赖怎么装。这份文档花不了两小时,但能体现你的工程素养。
被问到"哪些是你自己写的"怎么办。这个问题的正确回答方式不是罗列代码行数,而是讲决策:为什么选这个架构、为什么这张表要这么设计、为什么这个功能用这种算法。决策是你独有的,代码可能是抄的,但决策思路抄不来。所以准备答辩的时候,重点准备"为什么",而不是"是什么"。
最后分享一个我个人在做这类项目时体会最深的事:把时间花在"想清楚"上,比花在"写代码"上更划算。我第一版是边想边写,写到第三个功能时发现表结构不对,返工花了三天;第二版我花了整整两天时间只画图和写接口文档,一行代码没写,然后再动手,十天就把核心功能全跑通了。这个比例大概是二八开——两成时间想,八成时间写,但想的那两成决定了后面八成顺不顺。
如果你正在做这个题目,建议先别急着搭环境,把第 2 节的模块清单和第 4 节的表结构对着自己的需求过一遍,看看哪些该砍、哪些字段该加。这一步做完,后面的路会顺很多。