1. 唐诗三百首App的需求拆解:这真不是"列表加详情"这么简单
"springboot安卓唐诗三百首app"这个题目,我第一次看到的时候心里其实是有点轻视的。一个后端接口暴露诗歌列表,一个安卓端发请求渲染出来,怎么看都是课程设计里的标准配置。但真正把项目做完,把三百首诗全部落库,把每日推荐和收藏功能都跑通之后,我才发现这个题目卡人的地方根本不在增删改查,而在几个特别隐蔽的细节上:诗歌原始数据怎么清洗入库、中文内容在接口和客户端之间传输时的编码问题、每日推荐一诗背后的时间和随机策略、安卓端对古文长文本的排版体验。这篇就把我完整实现这套系统时的设计思路和踩坑过程梳理一遍,适合正在做课程设计或毕业设计的同学参考,也适合想用一个小而全的项目把"后端接口设计+安卓网络请求+数据建模"这条链路串起来的开发者。
1.1 用户真正需要的是什么
先把需求想清楚再动手。唐诗三百首App听起来是纯粹的展示类应用,但用户的使用场景里其实藏着不少隐含需求:想按作者浏览某位诗人的作品、想搜一句记不全的诗、想把喜欢的诗存下来反复看、想每天打开App看到一首"今日推荐"作为仪式感内容。如果只做一个列表加详情,这个项目交作业是没问题,但体验上非常单薄。
所以我当时把功能划分成了三层。基础层是诗歌分页列表和详情展示,这决定App能不能用;增强层是作者聚合和关键词搜索,这决定App好不好找内容;增值层是收藏、随机一诗、每日推荐,这决定用户愿不愿意打开第二次。这样的划分直接影响后面的数据库设计和接口拆分,强烈建议做同类项目时先按这个思路画一张功能清单,再开始建表写代码,而不是拿到题目就直接建工程。
1.2 为什么选SpringBoot加安卓原生
技术选型上,SpringBoot几乎是当前Java后端做这类项目的默认答案,理由很实际:内置Tomcat,一条命令就能跑起来;Spring Data JPA和MyBatis都有成熟的脚手架,写CRUD效率很高;更重要的是社区资料极其丰富,遇到问题搜索一下基本都有现成答案。对于课程设计这种时间紧、需要快速出东西的场景,SpringBoot的收益是立竿见影的。有同学纠结要不要用Spring Cloud或者微服务,我的建议很直接:三百首诗的体量用微服务是给自己找麻烦,单体应用加良好分层才是这个项目的最优解。
安卓端我选择了原生开发而不是WebView套壳,原因有两方面。一是原生RecyclerView的分页加载、收藏状态、本地缓存这些体验细节更容易控制,这也是项目"设计"部分的加分点;二是大多数课程设计对安卓端的要求就是Java或Kotlin原生实现,用混合开发虽然在技术上没问题,但答辩环节很容易被追问"为什么不用原生",解释成本远高于实际收益。客户端语言我选的Java配Retrofit,一方面是因为团队更熟,另一方面是Java的排查资料在安卓领域还是最多的,遇到诡异问题不至于卡住。
2. 诗库数据建模:三百首诗的存储与检索没你想得那么简单
数据库设计是整个项目的地基。唐诗三百首的数据量虽然不大,但诗的文本结构有一定特殊性,如果表设计得不好,后面做搜索、做作者聚合都会很别扭。这一节我把表结构、字段取舍和数据导入这三件事讲透。
2.1 表结构:作者表和诗歌表怎么拆
我设计了两张核心表,作者表保存诗人基本信息,诗歌表保存诗的内容和扩展字段。建表语句大致如下:
CREATE TABLE author ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '作者名', dynasty VARCHAR(20) NOT NULL COMMENT '朝代', intro TEXT COMMENT '作者简介' ); CREATE TABLE poem ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, author_id BIGINT NOT NULL, author_name VARCHAR(50) NOT NULL, dynasty VARCHAR(20), content TEXT NOT NULL COMMENT '正文,多行用\n分隔', translation TEXT COMMENT '译文', annotation TEXT COMMENT '注释', appreciation TEXT COMMENT '赏析', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );这里有一个当时纠结过的点:诗歌表里要不要冗余一个author_name字段。按数据库范式来说,作者名应该通过author_id关联查询,但实际做接口的时候会发现,列表页每一行都要显示作者名,如果每个条目都去join一次作者表,虽然数据量小影响不大,但代码会多出一堆联表逻辑。我最终选择了冗余author_name,理由是三百首诗的体量下,冗余带来的数据一致性问题几乎不存在,而换来的查询简洁性非常值。做小体量项目时,这种"适度违反范式"的取舍其实很实用,答辩时也能讲出你自己的思考,比一句"我按范式设计的"更有说服力。
content字段我用文本格式保存全文,行与行之间用\n分隔,没有用JSON数组或者拆成多行记录。原因是诗的正文是整体展示单元,客户端拿到整段文本后自行按换行拆分渲染,远比在数据库里拆成多行再组装简单。这里有个容易被忽略的坑:MySQL建库时一定要用utf8mb4字符集,否则遇到生僻字或者特殊标点有可能出现乱码或者插入报错。建库语句我统一写成CREATE DATABASE xxx DEFAULT CHARACTER SET utf8mb4,一劳永逸。
2.2 数据初始化:三百首诗的导入方案
这是很多同学卡住的第一步:三百首诗的原始数据从哪来、怎么导进数据库。网络上可以搜到不少txt格式的唐诗三百首全集,但这些文本质量参差不齐,常见问题有全角半角标点混用、诗名和作者排版不一致、正文里夹带注释、繁简体混杂。直接拿原始文本往数据库里灌,后面做搜索和展示时一定会出各种奇怪的现象。
我当时写了一个Python清洗脚本,把txt按"诗名、作者、正文"的规律切分,剔除空行和多余的换行符,统一转换成UTF-8编码,再生成SQL脚本批量插入。如果你不想自己写脚本,也可以把整理好的数据做成.sql文件,在SpringBoot项目启动时用schema.sql配合data.sql自动初始化,或者用MyBatis的ScriptRunner手动执行。清洗脚本的核心逻辑不复杂,大致就是正则匹配诗名和作者,把这两项摘出来,剩下部分按行拼接成正文。值得花功夫的是对正文行首行尾空白字符的处理,以及把中文标点统一成全角、英文标点统一成半角,这些细节决定了客户端排版时是否会突然冒出奇怪的空格。
这里给一个非常实用的建议:导入完成后一定要抽样检查。我当时的做法是随机取十首诗,人工对照原始文本,逐首核对标题、作者、正文是否对应。因为原始文本的排版错误是清洗脚本很难全覆盖的,比如有的诗集版本会把诗的前两句和注解混在同一行,这种只能靠人工抽查发现。抽样检查看起来多花了半小时,但能避免后面所有功能都建立在错误数据上,这笔时间花得非常值。
3. SpringBoot后端:接口设计的取舍与踩坑记录
后端部分的核心就是接口设计,接口做得好不好,直接决定安卓端的开发难度和使用体验。这一节我会把接口清单、统一返回格式、随机一诗和每日推荐的实现差异、中文搜索的方案选择全部交代清楚。
3.1 接口清单与统一返回格式
我最终定义的接口如下表所示,覆盖了需求阶段划分的所有功能:
| 接口路径 | 请求方式 | 说明 |
|---|---|---|
| /api/poems?page=1&size=20 | GET | 诗歌分页列表 |
| /api/poems/{id} | GET | 诗歌详情 |
| /api/poems/search?keyword=月 | GET | 按关键词搜索 |
| /api/poems/random | GET | 随机一诗 |
| /api/poems/daily | GET | 每日推荐一诗 |
| /api/authors/{id} | GET | 作者信息及其作品 |
| /api/favorites | GET/POST/DELETE | 收藏的查询、新增、删除 |
所有接口统一返回一个Result对象,结构是code、message、data三个字段。这样做的好处是安卓端可以做统一的网络回调处理,code为200时解析data,非200时统一弹提示,不需要每个接口单独写错误分支。这个类很简单但收益极大:
public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.message = "ok"; r.data = data; return r; } public static <T> Result<T> error(int code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }异常处理我用@RestControllerAdvice做全局捕获,业务异常返回业务错误码,未知异常统一返回500,同时在日志里记录堆栈。这套组合拳写起来不难,但能让安卓端省掉大量判空和异常分支的代码。很多新人觉得异常处理是后端自己的事,跟客户端无关,其实恰恰相反:一个结构稳定的错误返回格式,能让客户端把网络错误、业务错误、数据为空这三种情况统一处理,体验差异是肉眼可见的。
3.2 随机一诗和每日推荐:两种完全不同的实现思路
随机一诗容易理解,就是从数据库里随机取一条返回。在小数据量下直接用ORDER BY RAND() LIMIT 1完全没问题,三百条记录实测响应在毫秒级。但我在代码注释里专门标了一句:如果将来数据量增大,这个写法会全表扫描,优化思路是先查出总数,再随机一个偏移量,用LIMIT offset, 1去取。大家做同类项目时可以提前按这个思路写,免得将来改。
每日推荐则是另一个思路。它和随机的核心区别在于:所有用户在同一天要看到同一首诗,第二天再统一换一首,所以不能用随机数,必须用日期作为确定性种子。我当时用的是把当天日期转成字符串,再计算哈希值对诗歌总数取模,得到一个稳定的索引。实现代码大致是这样:
ZoneId zoneId = ZoneId.of("Asia/Shanghai"); LocalDate today = LocalDate.now(zoneId); int seed = Math.abs(today.toString().hashCode()); int index = seed % poemCount; Poem dailyPoem = poemMapper.selectByOffset(index);这样设计的好处是服务端不需要任何定时任务,用户每次请求时实时计算即可,逻辑简单、天然支持水平扩展,而且同一天内不管谁请求都拿到同一首。日期哈希取模可能会有极小概率产生相邻日期推荐同一首诗的重复感,实际跑下来概率很低,可以不做处理;如果追求极致体验,也可以把这个映射关系固化到一张推荐表里,每天定时生成,那是后话。
3.3 中文搜索的三种方案与选择
搜索功能是关键词里的重点,"月"能搜出所有带"月"字的诗,听起来简单,但中文搜索在技术上有几条路线可选。
第一种是MySQL的LIKE模糊查询,写法最直接,三百条数据量下性能完全够,缺点是SQL里要手动拼接%关键字%,而且需要处理下划线、百分号的转义。第二种是给content字段加全文索引,MySQL的全文索引对中文支持不算好,需要ngram分词插件,配置成本偏高。第三种是引入Elasticsearch,对这个小项目来说属于明显的过度设计。
我选了第一种LIKE方案,加了一层简单的转义处理。很多教程会告诉你数据量大了不要用LIKE,但脱离数据量谈优化都是耍流氓,三百条记录就算裸查也是秒回。这背后的原则值得多说一句:技术选型一定要匹配数据规模和业务阶段,不要为了"技术正确"给自己增加不必要的复杂度。如果你以后做的项目数据量到百万级,再迁移到全文索引也不迟,但在这个阶段,最朴素的办法就是最优解。
4. 安卓端实现:从书架到原诗的体验还原
后端接口稳定之后,安卓端的工作量其实更大一些,因为用户体验的所有细节都堆在客户端。这一节讲网络层搭建、列表和详情页排版、收藏与本地缓存三个重点。
4.1 网络层与数据层的搭建
安卓端我用Retrofit加OkHttp加Gson这套组合,几乎是安卓网络请求的事实标准。Retrofit负责把接口定义转成可调用的Java方法,Gson负责JSON到实体类的映射。依赖配置大致如下:
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'关键配置有两个。一个是超时时间,连接超时和读取超时我都设了10秒,避免弱网环境下请求长时间挂起导致界面卡死的错觉。另一个是日志拦截器,开发阶段开启HttpLoggingInterceptor,联调时能在Logcat里看到完整的请求响应报文,定位问题效率高很多。正式发布包要把日志级别调成NONE,既是性能考虑,也是避免敏感信息泄露。
数据层我用了ViewModel加LiveData,Activity只负责渲染和事件,数据请求全部放在ViewModel里。这样做的直接好处是屏幕旋转时数据不会丢,间接好处是代码结构清晰,哪个方法负责什么一目了然。答辩的时候能讲清楚这套MVVM思想,本身就是加分项。很多同学喜欢把网络请求直接写在Activity的onCreate里,图省事,但项目一旦加收藏、加缓存,代码很快就乱成一团,回头维护成本反而更高。
4.2 列表页与详情页的排版细节
列表页用RecyclerView加卡片式布局,每张卡片显示诗名、作者、朝代和正文第一句。正文第一句的截取逻辑要注意,直接从content字段按\n切分取第一行就行,但有些清洗不干净的数据第一行会是空行,所以客户端要加个过滤,去掉空行再取。这个小细节看起来不起眼,但如果数据库里偶尔混入空行,列表页就会出现一排排空白的卡片,体验非常糟糕。
详情页是整个App的体验重点,古诗的排版和普通文本完全不一样。我当时做了三件事:第一,设置TextView的行间距为1.6倍,五言和七言在这种行高下都有比较好的呼吸感;第二,标题用专门的字体样式,或者直接调用系统楷体字型,正文保持常规字体,形成层次对比;第三,把正文按行拆分后居中展示,译文和赏析用灰色小字放在下方,与正文拉开视觉距离。这些细节不涉及复杂技术,但对App主观体验的影响非常大。
长文本滚动方面,一首诗的正文加赏析大概几千字,直接放TextView里完全够用,不需要WebView也不需要分页加载。我实测在低端手机上大号字加1.6倍行距的滚动流畅性依然很好,因为诗歌文本量实在太小。如果你加的赏析内容特别长,只要不超过几十KB,TextView都没有压力,不要在这个点上过度设计。
4.3 收藏与本地缓存
收藏功能通过后端接口实现,传入用户标识和诗歌id,数据库表里用这两个字段做唯一索引防止重复收藏。但实际使用中用户会希望离线也能看收藏,所以我加了一层本地缓存:收藏列表请求成功后写入Room数据库,详情页也缓存最近浏览过的诗。断网时打开App,至少能看到缓存过的内容,这个体验在很多类似项目中是被忽略的,但恰恰是用户最在意的点之一。
缓存的策略不需要复杂。列表页每次请求成功后把JSON写入SharedPreferences或Room,以"接口路径加页码"作为key;下次进入时先展示上次的缓存,再发起网络请求刷新。详情页按诗id做key,命中直接显示。这套方案的缺点是数据可能和服务器不同步,但在这个项目场景里完全够用,而且实现成本很低。如果你以后做新闻类App,再考虑加缓存过期策略也不迟。
5. 联调与上线前检查:那些容易翻车的隐蔽问题
代码写完之后真正消耗时间的不是功能开发,而是联调阶段一个接一个的隐蔽问题。我把在这个项目里实际踩过的坑整理成一个清单,每个都配有定位思路,希望你能少走弯路。
5.1 中文编码和JSON字段映射
第一个坑是编码。SpringBoot默认UTF-8,但MySQL连接串如果没加characterEncoding=utf8,或者数据库表本身是latin1,就会出现中文乱码。我的排查经验是:先在数据库客户端直接执行SELECT,看看数据在库里是否正常;如果库里正常,问题就在传输层,用Postman看HTTP响应头是不是charset=UTF-8,基本就能定位。这个排查顺序能帮你快速把问题圈定到存储层还是接口层,而不是瞎猜。
第二个坑是字段映射。我一开始在诗歌表里定义了author_id,Java实体类里也定义了authorId,Gson默认映射没问题,但后来加了一个字段下划线命名,部分接口返回的数据就出现了null。排查半天发现是JSON字段风格不统一导致的。我的建议是:实体类字段统一用驼峰命名,接口返回的JSON也统一用驼峰,序列化库的命名策略设置好之后就不要再混用;如果一定要混用,就在实体类字段上显式加@SerializedName注解,不要靠默认规则碰运气。
5.2 服务器时间与每日推荐的日期错位
每日推荐用日期作为随机种子,理论上没有问题,但联调时发现了一个有趣的现象:安卓端显示的"今日推荐"在晚上过了零点之后,服务器返回的诗有时候还是昨天的。原因是服务器运行的时区和客户端所在时区不一致,服务器取LocalDate.now()默认用的是系统时区,一旦部署环境是UTC,日期就差了八个小时。
解决办法很直接,在代码里指定时区取日期,就是我前面那一节展示的写法。这一步很不起眼,但不处理的话,每天更新的功能在部分时间窗口就是错的。做完之后我还特意在零点前后各测了一次,确认推荐确实在零点准时切换。这里也引出一个通用经验:所有涉及日期和时间的业务逻辑,都要显式指定时区,不要依赖服务器的系统默认值,否则换一台机器部署就可能出问题。
5.3 安卓明文HTTP访问与打包部署
安卓9之后默认禁止明文HTTP请求,开发阶段如果用http加IP地址访问本地后端,请求会被直接拦下来报错。需要在AndroidManifest里配置networkSecurityConfig,允许特定域名的明文流量。这个坑几乎每个做本地联调的人都会遇到,网上资料也很多,但第一次碰到时确实容易发懵,我在这里提醒一下,让你至少知道去搜什么关键词。
部署方面,SpringBoot打包成jar后用java -jar启动,配合一个简单的服务管理工具或者Docker都可以。数据库初始化建议做成启动时自动执行脚本,避免人工导库。安卓端的正式包要把调试日志关掉,用正式签名打包。这些虽然是收尾工作,但做不好会给人留下"项目不稳定"的印象。还有一个容易被忽视的点:后端服务启动前先确认端口没有被占用,特别是多人共用一台开发机的时候,我遇到过两次因为端口冲突导致接口时通时不通的诡异现象。
最后再分享一个我在这类项目里积累的实操心得:做全栈小项目,最重要的不是某个炫酷的技术点,而是一条完整的数据链路是否真正跑通。从数据库里的三百首诗,到后端接口正确返回,再到安卓端渲染出排版舒服的详情页,每一步都有容易忽略的细节。我建议每完成一个环节就手动验证一次,不要等全部写完再联调,否则一次冒出七八个问题的时候,排查成本会翻好几倍。我的习惯是先在Postman里把后端接口全部验证通过,再开始写安卓端的网络层,这样客户端开发时遇到的每一个问题,都能确定是客户端的问题,不用在前后端之间来回摇摆。希望这篇能帮你把这条路走得顺畅一些。