SpringBoot与安卓实现唐诗三百首App:接口设计、数据建模与踩坑实录
发布时间:2026/10/18 13:05:03 锦皓数字建站

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?page1size20GET诗歌分页列表/api/poems/{id}GET诗歌详情/api/poems/search?keyword月GET按关键词搜索/api/poems/randomGET随机一诗/api/poems/dailyGET每日推荐一诗/api/authors/{id}GET作者信息及其作品/api/favoritesGET/POST/DELETE收藏的查询、新增、删除所有接口统一返回一个Result对象结构是code、message、data三个字段。这样做的好处是安卓端可以做统一的网络回调处理code为200时解析data非200时统一弹提示不需要每个接口单独写错误分支。这个类很简单但收益极大public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message ok; r.data data; return r; } public static T ResultT error(int code, String message) { ResultT 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加LiveDataActivity只负责渲染和事件数据请求全部放在ViewModel里。这样做的直接好处是屏幕旋转时数据不会丢间接好处是代码结构清晰哪个方法负责什么一目了然。答辩的时候能讲清楚这套MVVM思想本身就是加分项。很多同学喜欢把网络请求直接写在Activity的onCreate里图省事但项目一旦加收藏、加缓存代码很快就乱成一团回头维护成本反而更高。4.2 列表页与详情页的排版细节列表页用RecyclerView加卡片式布局每张卡片显示诗名、作者、朝代和正文第一句。正文第一句的截取逻辑要注意直接从content字段按\n切分取第一行就行但有些清洗不干净的数据第一行会是空行所以客户端要加个过滤去掉空行再取。这个小细节看起来不起眼但如果数据库里偶尔混入空行列表页就会出现一排排空白的卡片体验非常糟糕。详情页是整个App的体验重点古诗的排版和普通文本完全不一样。我当时做了三件事第一设置TextView的行间距为1.6倍五言和七言在这种行高下都有比较好的呼吸感第二标题用专门的字体样式或者直接调用系统楷体字型正文保持常规字体形成层次对比第三把正文按行拆分后居中展示译文和赏析用灰色小字放在下方与正文拉开视觉距离。这些细节不涉及复杂技术但对App主观体验的影响非常大。长文本滚动方面一首诗的正文加赏析大概几千字直接放TextView里完全够用不需要WebView也不需要分页加载。我实测在低端手机上大号字加1.6倍行距的滚动流畅性依然很好因为诗歌文本量实在太小。如果你加的赏析内容特别长只要不超过几十KBTextView都没有压力不要在这个点上过度设计。4.3 收藏与本地缓存收藏功能通过后端接口实现传入用户标识和诗歌id数据库表里用这两个字段做唯一索引防止重复收藏。但实际使用中用户会希望离线也能看收藏所以我加了一层本地缓存收藏列表请求成功后写入Room数据库详情页也缓存最近浏览过的诗。断网时打开App至少能看到缓存过的内容这个体验在很多类似项目中是被忽略的但恰恰是用户最在意的点之一。缓存的策略不需要复杂。列表页每次请求成功后把JSON写入SharedPreferences或Room以接口路径加页码作为key下次进入时先展示上次的缓存再发起网络请求刷新。详情页按诗id做key命中直接显示。这套方案的缺点是数据可能和服务器不同步但在这个项目场景里完全够用而且实现成本很低。如果你以后做新闻类App再考虑加缓存过期策略也不迟。5. 联调与上线前检查那些容易翻车的隐蔽问题代码写完之后真正消耗时间的不是功能开发而是联调阶段一个接一个的隐蔽问题。我把在这个项目里实际踩过的坑整理成一个清单每个都配有定位思路希望你能少走弯路。5.1 中文编码和JSON字段映射第一个坑是编码。SpringBoot默认UTF-8但MySQL连接串如果没加characterEncodingutf8或者数据库表本身是latin1就会出现中文乱码。我的排查经验是先在数据库客户端直接执行SELECT看看数据在库里是否正常如果库里正常问题就在传输层用Postman看HTTP响应头是不是charsetUTF-8基本就能定位。这个排查顺序能帮你快速把问题圈定到存储层还是接口层而不是瞎猜。第二个坑是字段映射。我一开始在诗歌表里定义了author_idJava实体类里也定义了authorIdGson默认映射没问题但后来加了一个字段下划线命名部分接口返回的数据就出现了null。排查半天发现是JSON字段风格不统一导致的。我的建议是实体类字段统一用驼峰命名接口返回的JSON也统一用驼峰序列化库的命名策略设置好之后就不要再混用如果一定要混用就在实体类字段上显式加SerializedName注解不要靠默认规则碰运气。5.2 服务器时间与每日推荐的日期错位每日推荐用日期作为随机种子理论上没有问题但联调时发现了一个有趣的现象安卓端显示的今日推荐在晚上过了零点之后服务器返回的诗有时候还是昨天的。原因是服务器运行的时区和客户端所在时区不一致服务器取LocalDate.now()默认用的是系统时区一旦部署环境是UTC日期就差了八个小时。解决办法很直接在代码里指定时区取日期就是我前面那一节展示的写法。这一步很不起眼但不处理的话每天更新的功能在部分时间窗口就是错的。做完之后我还特意在零点前后各测了一次确认推荐确实在零点准时切换。这里也引出一个通用经验所有涉及日期和时间的业务逻辑都要显式指定时区不要依赖服务器的系统默认值否则换一台机器部署就可能出问题。5.3 安卓明文HTTP访问与打包部署安卓9之后默认禁止明文HTTP请求开发阶段如果用http加IP地址访问本地后端请求会被直接拦下来报错。需要在AndroidManifest里配置networkSecurityConfig允许特定域名的明文流量。这个坑几乎每个做本地联调的人都会遇到网上资料也很多但第一次碰到时确实容易发懵我在这里提醒一下让你至少知道去搜什么关键词。部署方面SpringBoot打包成jar后用java -jar启动配合一个简单的服务管理工具或者Docker都可以。数据库初始化建议做成启动时自动执行脚本避免人工导库。安卓端的正式包要把调试日志关掉用正式签名打包。这些虽然是收尾工作但做不好会给人留下项目不稳定的印象。还有一个容易被忽视的点后端服务启动前先确认端口没有被占用特别是多人共用一台开发机的时候我遇到过两次因为端口冲突导致接口时通时不通的诡异现象。最后再分享一个我在这类项目里积累的实操心得做全栈小项目最重要的不是某个炫酷的技术点而是一条完整的数据链路是否真正跑通。从数据库里的三百首诗到后端接口正确返回再到安卓端渲染出排版舒服的详情页每一步都有容易忽略的细节。我建议每完成一个环节就手动验证一次不要等全部写完再联调否则一次冒出七八个问题的时候排查成本会翻好几倍。我的习惯是先在Postman里把后端接口全部验证通过再开始写安卓端的网络层这样客户端开发时遇到的每一个问题都能确定是客户端的问题不用在前后端之间来回摇摆。希望这篇能帮你把这条路走得顺畅一些。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。