基于Uniapp+SpringBoot的全栈实战:公考刷题平台从0到1开发详解
做公考学习平台这个选题起初是因为一个备考中的朋友跟我抱怨市面上的刷题App要么弹窗广告满天飞要么功能堆得乱七八糟找个“打开就能做一套行测题”的工具都费劲。我索性动了念头——自己动手做一套只围绕学习场景的移动平台。技术栈定得很干脆移动端用Uniapp保证一套代码跑微信小程序和App后端用SpringBoot提供稳定的服务接口管理后台用Vue做内容运营的“中控台”。项目从需求梳理到完成核心功能前后花了大约两个月下面把整个设计过程中的关键决策、踩过的坑和实现细节完整记录下来希望能给正在做类似全栈练手项目或毕业设计的朋友一些参考。1. 项目定位与整体架构拆解1.1 一个“刷题工具”背后的真实需求公考学习场景和普通的知识付费产品不太一样用户的诉求非常纯粹练题、批改、记录错题、看解析可能还要配合少量视频课程和时政资料。它不像K12或者职业教育那样需要复杂的课程体系、排课逻辑和作业点评流程核心闭环就是“刷题—判分—收集错题—针对性重练—统计进步”。所以我第一件事不是写代码而是把产品功能画成一个闭环首页入口负责分发每日一练、模拟套卷、章节练习刷题页负责沉浸式答题提交后立即显示结果和解析答错的题自动进入错题本错题本可以支持按考点筛选和重练个人中心则展示正确率曲线和薄弱考点统计。整个平台的用户目标也非常清晰移动端面向考生管理后台只服务内容运营者也就是出题、传课、看数据的人。这个定位决定了后续的技术选型系统不需要高并发的直播能力不需要复杂的推荐算法但是需要有稳定的题库管理、准确的判分逻辑和流畅的多端刷题体验。这也解释了为什么SpringBoot这种成熟稳重的后端方案反而是最合适的选择——不追求花哨但求可靠。1.2 为什么是Uniapp SpringBoot Vue这套组合技术选型时我当然也考虑过纯原生开发、Flutter或者React Native但最终还是定了Uniapp原因很现实让我用一套代码库同时交付微信小程序和Android/iOS App一个人力能撑住的项目就绝不能拆成两套代码。Uniapp基于Vue语法写起来顺手uni-app框架对小程序原生能力和App原生插件都有封装比如uni.login()、uni.requestPayment()这些API是跨端统一的业务代码写一遍就能跑多端。后端选SpringBoot没有太多犹豫。一方面Java生态里的MyBatis-Plus、Spring Security这些轮子非常成熟做权限和数据库操作几乎不用从零造另一方面SpringBoot的自动配置特性让项目在几分钟内就能跑起来部署时打一个jar包丢服务器上就完事省心。管理后台用Vue则是顺理成章因为Uniapp本身就是基于Vue语法前端团队或者说我本人对Vue的思维模型已经很熟悉从移动端切到管理端几乎零学习成本。Vue配合Element UI组件库做表格、表单、弹窗这类后台界面效率极高特别是题库管理这种大量表格操作的页面组件库能省下一大半开发时间。从工程维护的角度看三端移动端、后端、后台技术栈同构还有一个隐形的好处数据结构和接口文档可以复用同一套心智比如后端返回的题目JSON结构移动端和管理后台解析逻辑完全一致出问题排查起来特别快。1.3 整体技术架构与部署形态整个系统的物理拓扑大概是这样的用户通过微信小程序或App访问请求打到Nginx反代的后端服务SpringBoot应用通过MyBatis-Plus操作MySQL数据库Redis负责缓存热点题目和保存登录态管理后台是独立的Vue项目打包成静态文件由Nginx托管管理端通过同样的后端接口进行内容管理。这里有一个很多人忽略的设计点题库数据一定要做缓存。公考题目的特点是读取频繁、更新低频每次刷题都从MySQL里随机捞题目在高并发时很容易把数据库拖垮。我当时的方案是题目列表的查询结果按“考点难度题型”缓存到Redis缓存过期时间设为10分钟保证考生连续刷题时读取的是内存数据数据库只承担题目新增和修改的写入压力。部署形态上本地开发我用Docker Compose统一管理MySQL和Redis避免本机装环境到处踩坑。生产环境则更朴素一台2核4G的云服务器跑jar包和Nginx开发初期一个月不到一百元的成本完全够用。2. 移动端Uniapp实战刷题主流程怎么搭2.1 页面架构从“每日一练”到“错题重练”的闭环移动端的页面结构我设计成四个Tab首页、刷题、错题本、我的。Tab页比较常规比较容易出问题的地方在于刷题页和错题本页的页面栈管理——因为这两个页面有交叉跳转的需求比如从错题本点“重练”会跳转到答题页如果直接用uni.navigateTo页面栈会越堆越深用户连续跳转几次之后再返回可能就直接退回首页了。我的解决办法是答题页统一用uni.redirectTo跳转替代navigateTo。这样每次进入刷题都会替换当前页面栈深度始终保持在可控范围内。返回逻辑则通过自定义的“退出答题”按钮处理点击后先判断当前模式如果是章节练习返回章节列表页如果是错题重练返回错题本页如果是模拟考试返回提交确认页。用一个枚举变量控制返回目标比让用户反复点页面左上角返回要清晰得多。页面清单里还有一个容易被忽视的角色“每日一练”并不是一个独立页面而是通过参数进入通用刷题页。首页上用户点击“每日一练”时跳转路径是 /pages/exam/do?typedailynum10刷题页根据type参数的不同调用不同的出题接口。这种设计让刷题页成为整个App的核心复用单元后续扩展“考前押题”“专题突破”等新模式时只需要新增一个type值而不用重写页面。2.2 刷题引擎的实现状态管理、进度记录与异常处理刷题页的核心是一个“状态机”初始状态是“加载题目”加载成功后进入“答题中”用户点击交卷后进入“判分中”判分结束展示结果后进入“已完成”。这套状态我用Vuex全局管理而不是页面本地data原因在于用户可能会在答题中途切到后台、锁屏甚至被系统杀掉进程如果状态只存在组件里重新进入页面时进度就丢了。Vuex的state里我维护了这些核心字段当前试卷的题目数组questionList用户答案映射表answerMapkey是题目IDvalue是选项下标当前题目索引currentIndex答题模式mode和剩余时间remainTime这里特别想提一下“答题中途退出”的处理。小程序场景下用户习惯随手切走所以我在onHide生命周期里把当前进度序列化到uni.setStorageSync在onShow恢复。刷新页面时如果发现有未提交的进行中答卷弹出确认框询问“是否继续上次练习”。这个功能虽然代码只写了三四十行但对用户体验的提升非常明显也是我后来在真实使用中觉得“这个App像个正经产品”的关键节点。计时逻辑上我没有用全局setInterval而是只在答题开始时记录一个startTime时间戳每次点击选项时用当前时间减startTime计算已用时间页面展示用每分钟一次的低频setInterval更新。这样做的好处是省电省资源而且不受程序切后台的影响——因为时间戳不会暂停而setInterval在后台是真的会“停摆”的。2.3 多端适配微信小程序与App的差异处理Uniapp的最大卖点是跨端但跨端不等于零差异。我在开发中至少遇到了三类需要差异化处理的场景。第一类登录授权。微信小程序的登录靠uni.login拿到code然后由后端拿着code去微信接口换openidApp端则是同一个API但Uniapp换成uni.getProvider和uni.login时行为有差异有些安卓机甚至要在manifest.json里配置OAuth相关的key才能正常拉起微信授权。我统一封装了一个loginUtil.js内部判断运行环境如果编译端是MP-WEIXIN就走code换token流程APP端则拉取微信客户端授权H5端直接走账号密码登录。第二类分享功能。小程序的分享是一个很自然的行为用户点右上角菜单就能调起onShareAppMessage把当前的每日一练分享给朋友。而App端要分享只能自己集成分享SDK或者引导截图体验完全不一样。我的处理方案是在onShareAppMessage里配置标题和路径并带上来自分享的userId参数作为渠道追踪App端则不提供分享入口转而设计“学习成就卡片”让用户主动保存图片分享到朋友圈——这反而是更符合App用户心智的做法。第三类titleNView和返回行为。小程序有自带的导航栏App端却可以用plus API动态修改导航栏按钮。我在题目页的App端版本里加了一个“收藏”按钮通过uni.setNavigationBarTitle和plus.navigator的API实现。代码用条件编译包起来// #ifdef APP-PLUS plus.navigator.setStatusBarStyle(dark); // #endif // #ifdef MP-WEIXIN wx.setNavigationBarColor({ backgroundColor: #ffffff, frontColor: #000000 }); // #endif需要说明的是条件编译一定要早做等到后期再补会因为页面已经写死而非常痛苦。我一开始偷懒没管状态栏颜色结果真机上发现小程序顶部是黑字白底、App却是白字透明整整排查了一个下午。2.4 课程视频与学习资料的播放方案公考平台上除了刷题必不可少的是视频课程和一些PDF资料。视频播放这一块Uniapp提供了uni.createVideoContext这个API基础的video组件在微信小程序和App上都能直接播放mp4格式但如果课程源是m3u8格式的直播流或切片视频事情就会麻烦一些——小程序端对m3u8的支持还好App端却依赖封装在原生层的播放器直接用video组件挂m3u8链接在部分Android机型上会黑屏。我当时是采用了“双轨方案”如果后端返回的视频地址是.mp4直接用video组件播放如果是.m3u8链接则在小程序端继续用video组件因为微信底层支持HLSApp端则标记为“请在小程序中观看”并隐藏播放器。同时管理后台在上传课程时强制转码输出一份mp4版本作为App端的兜底。这个方案谈不上优雅但确实解决了实际问题而且实现成本很低。PDF资料的处理相对简单后端返回文件URLApp端用uni.downloadFile下载后用plus.runtime.openFile调用系统应用打开小程序端则用uni.openDocument预览。唯一要注意的是文件名如果包含中文或特殊字符下载时容易导致乱码我统一在生成URL时使用encodeURIComponent处理文件名。3. SpringBoot后端题库与核心业务逻辑的实现3.1 数据库设计五张核心表的结构与字段要点后端数据库我设计了五张核心业务表分别是用户表(user)、题目表(question)、答题记录表(answer_record)、错题表(wrong_question)、学习统计表(study_statistics)。题目表是最重要的字段设计直接决定刷题功能的灵活性。题目表的字段并不是简单的“题干答案”就完事我拆得比较细id主键type题目类型1单选、2多选、3判断category一级分类行测、申论、常识sub_category二级分类如判断推理、数量关系、言语理解difficulty难度1-5级content题干内容options选项JSON字符串如 {A:苹果,B:香蕉}answer正确答案单选存A多选存ABDanalysis答案解析status上下架状态需要提醒的是options字段用JSON存储极大简化了表结构但也会带来一个问题——在多选判分时解析JSON的开销。我当时的做法是全量查出来后用fastjson在Java内存里解析因为单次刷题的题量通常只有10-30道内存解析完全没有性能压力。如果未来题库量级上去了可以考虑把选项拆成子表但当前单机场景完全没必要。答题记录表answer_record记录了每次作答明细userId、questionId、userAnswer、isCorrect、createTime。错题表wrong_question实际上是一个基于答题记录的冗余表字段包括userId、questionId、wrongCount、lastWrongTime。为什么表和答题记录会冗余因为查询“我的错题列表”时如果直接从answer_record里筛“答错的记录”会因为多次练习同一道题而产生大量重复行。错题表按“用户题目”去重后展示和管理都舒服得多。3.2 JWT鉴权与用户体系设计用户体系我最终选择了JWT Token方案配合拦截器做统一鉴权。登录接口校验账号密码成功后后端生成一个包含userId和expireTime的Token并同时写入Rediskey为tokenvalue为userId过期时间设为一周每次请求拦截器会先解析Token、再查Redis确认Token是否有效。JWT Redis双校验解决了一个纯JWT方案的痛点——无法主动让Token失效。如果管理后台发现某用户的账号被盗或刷题异常可以直接删除Redis里的token用户下一次请求就会被拦截这比单纯依赖JWT的过期时间要可控得多。拦截器我用Spring Boot的HandlerInterceptor实现在preHandle里完成Token校验并往ThreadLocal里写入当前用户信息。这样后续的Service层代码不需要在每个方法里重复拿userId参数写起来清爽很多。需要注意的一个细节是文件下载和微信回调这类接口必须配置为匿名放行否则会出现线上回调失败但本地联调正常的情况——我就是因为漏配了微信支付的回调地址白名单整整排查了两天才发现后端一直在拦截回调请求。3.3 核心接口设计随机出题、提交判分、错题收集刷题类平台的核心接口其实很好设计。我这里分享三个最重要的接口逻辑。出题接口的请求参数是modedaily/exam/practice、category、num后端逻辑分三步先从Redis读取该分类下题目ID列表的缓存如果不存在则从MySQL查询并写入缓存然后从ID列表中随机抽num个最后从MySQL按ID批量查询题目完整信息返回给前端。随机抽题我是在Java内存里用Collections.shuffle实现的10万以下的数据量性能都非常好。提交判分接口的参数是answerMap和mode后端遍历每道题判断用户答案是否与标准答案一致。单选直接比较字符串多选则要把用户提交的选项拆开后排序再比较。这里有个让不少初学者翻车的坑用户提交的顺序和标准答案顺序不一致时直接字符串相等判断会误判。我当时用了一个很稳的办法把选项字符串转成字符数组排序后再拼接比较比如用户提交DB和标准答案BD排序后都是BD结果就正确了。判分完成之后后端统一处理错题入账如果本题答错了插入一条错题记录如果之前已经存在同题的错题记录则wrongCount加1。这里我用了INSERT INTO ... ON DUPLICATE KEY UPDATE的SQL语句通过(user_id, question_id)唯一索引实现一行SQL完成“新增或更新”的意图省掉了先查询再更新的两段式逻辑。学习统计表的研究价值在于它需要聚合用户每天刷了多少题、正确率多少、各考点的刷题数和正确率。我是在每次提交判分接口之后进行增量更新而不是每天跑定时任务全量统计。增量更新虽然让接口多了一次写入但数据实时性极好个人中心页打开就能看到当天的数据和正确率曲线。3.4 接口性能优化的一个实战案例开发到后期我做了一次性能测试发现“模拟考试试卷加载”接口有点慢单次耗时经常超过800毫秒。排查之后发现瓶颈主要在两步第一步考试试卷需要从每个分类常识、言语、数量、判断、资料随机抽固定数量的题目我原来用了五次数据库查询第二步每道题查询时我的Mapper里selectById和SELECT *没有指定列把analysis这种大字段也查了出来。优化方案很简单五个分类的随机抽题合并到一个方法里用一次查询查出该分类所有题目ID然后在内存里分组后随机抽取查询题目列表时指定需要的列字段把大体积的analysis字段在列表阶段排除到考生提交完题目或查看解析时再单独查询。优化之后接口响应直接降到了150毫秒左右用户体验提升了一个量级。这个案例也说明很多时候性能优化不是引入多高级的中间件而是减少不必要的查询和字段传输。4. Vue管理后台内容运营的“中控台”4.1 功能划分与页面规划管理后台的功能我用一句话概括让运营人员不写一行代码就能完成平台的内容维护。基于这个目标我把后台分为四个模块题库管理、试卷管理、用户管理和数据看板。题库管理是最核心的部分提供题目的新增、编辑、批量导入导出、上下架切换、按分类筛选和关键字搜索。试卷管理则偏向“组卷”场景运营人员可以创建一套模拟卷设置各分类的题目数量然后从题库中手动添加或按规则抽题。用户管理相对简单就是查看用户列表、封禁异常账号、重置密码。数据看板我用ECharts展示每日新增用户、每日刷题量、整体正确率和各考点正确率排行。页面结构上我参考了常见的后台管理设计左侧是sidebar菜单右侧是主体内容区顶部是用户信息和退出按钮。Vue Router使用嵌套路由每个一级菜单对应一个布局组件二级页面放在children里。4.2 Excel批量导入题目从需求到实现的完整方案题库初始化阶段运营手里往往是几百道甚至上千道Excel格式的真题。为了不让人工一条条录入我做了Excel批量导入功能。前端使用Element UI的Upload上传组件把Excel文件提交给后端后端用EasyExcel解析文件把每一行映射成Question实体插入数据库。具体流程是上传文件→后端校验文件格式→EasyExcel逐行解析→数据校验必填字段、选项格式、答案合法性→批量插入。数据校验这一块最容易被忽视如果题目数据本身有脏数据比如选项缺了D、答案填了Z就会导致导入后用户刷题时碰到异常选项。我的做法是先全量校验把所有错误行收集到汇总里再一次性返回给前端展示而不是第一行出错就中断整个导入。这样运营人员能一次性看到所有需要修正的行。有一个印象很深的坑EasyExcel默认会把Excel里的数字列转成Double比如题号“1001”会变成“1001.0”插入数据库时必须做类型转换。我写了一个Integer类型的转换器在item转为实体时处理这个问题。这类零碎bug排查起来特别耗时但只要遇到一次后面就终身免疫了。4.3 权限控制与安全校验管理后台的接口不可能跟移动端接口共用一套鉴权逻辑但又不能完全分开我采用的办法是同一套SpringBoot应用但通过URL前缀区分/api/app/**走JWT用户鉴权/api/admin/**走管理员鉴权。管理员登录时后端生成一个独立的管理员Token并校验用户的role字段是否为ADMIN。后端用拦截器对/api/admin/**路径统一进行管理员身份校验前端则配合Vue Router的导航守卫做路由级拦截未登录或已过期的管理员会被redirect到登录页。这里我特别想把一个安全管理细节讲一下管理后台的接口请求必须校验Referer或者自定义header防止直接被人通过构造请求绕过页面调用后台接口。虽然这类项目本身价值不高不会有人刻意攻击但养成交互安全的习惯是值得的。5. 部署上线与常见问题避坑实录5.1 本地联调环境的搭建与启动顺序联调环境我推荐用Docker Compose一次性启动MySQL和Redis避免在Windows或macOS上装数据库踩环境变量、端口占用等一堆坑。docker-compose.yml的核心配置大概是这样的version: 3 services: mysql: image: mysql:8.0 environment: MYSQL_DATABASE: exam MYSQL_ROOT_PASSWORD: root123 ports: - 3306:3306 redis: image: redis:6.2 ports: - 6379:6379后端启动前我习惯先检查数据库初始化脚本是否已执行这也是常见的“服务起来但接口报错”的原因。第一次使用SpringBoot的自动建表时配置好ddl-auto: update只是能力兜底正式项目里所有表结构变更必须手动维护SQL脚本否则上线时容易出大乱子。启动顺序上我的建议是先启动MySQL和Redis→再启动SpringBoot后端→最后启动Uniapp或H5的前端。原因是前端开发时需要实时访问后端接口如果依赖的服务没就绪页面会一直转圈并提示接口超时容易让人误以为是前端代码问题。5.2 微信小程序发布与App上架的注意事项微信小程序发布是我在这个项目里踩坑最多的环节。首先小程序必须配置域名白名单且必须是已备案的HTTPS域名本地IP的请求一律拦。这导致我在开发期间要么在微信开发者工具里勾选“不校验合法域名”的选项要么先把后端提前部署到测试服务器。我建议是立项第一天就让后端同事或自己先把测试域名准备好、HTTPS证书配置好提前用测试环境联调小程序能省掉后期大量重复验证。小程序审核时还有一个高频拒绝项类目选择。公考学习平台在人家的类目里属于“教育-在线视频课程”或“教育-教育信息服务”需要提供对应的资质证明。如果个人主体的小程序未开通这些类目功能再完整也上不了架。我当时的一个变通方案是暂时把“课程视频”功能隐藏只保留刷题和文章阅读审核通过后再通过版本更新逐步开放这也是个人开发者绕开资质限制的一种常见做法。App上架安卓应用市场则完全是另一套流程需要软著、隐私政策、隐私合规检测报告每个市场审核周期和标准都不同。审核中最常被提及的隐私违规点是未向用户明示收集哪些信息、未提供注销账号入口、拒绝用户访问或删除个人信息。我最后是把“账号注销”功能加进了“我的”页面并做了告知弹窗后才顺利通过几个头部应用市场的上架。5.3 打包运行中常见的报错与解决方案开发过程中很多人问过我Uniapp的打包细节问题我把自己遇到过的坑汇总成一张速查表方便后面的人直接索引。问题现象可能原因解决方案小程序运行后request报URL错误域名未配置到微信白名单登录mp.weixin.qq.com配置request合法域名App真机调试时请求不到后端请求地址误填localhost改为局域网IP或公网测试地址确认同一网段H5打包后接口404baseURL使用了相对路径使用完整URL或配置Nginx反向代理安卓端video播放m3u8黑屏原生播放器兼容性不足转码为mp4或用H5的video组件渲染小程序分享卡片打不开页面分享路径少写.html结尾检查withShareTicket和path参数格式华为鸿蒙系统上App闪退插件库版本过低或不兼容检查HBuilderX版本和原生插件更新到最新打包后页面白屏路由模式history路径问题改成hash模式或配置history fallback小程序白屏的问题值得单拎出来再说一句如果是用Vue Router的history模式会出现这种情况因为服务器上没有对应的路由文件会返回404。小程序端则通常是因为onLoad里异步请求数据失败导致页面空渲染我一般会在onLoad和onShow里都做一次数据加载保护并在页面模板中用v-if控制空数据时的占位提示。还有一点很多初学者会问“为什么我用uniapp开发的App在鸿蒙系统上跑不了”。目前Uniapp官方对鸿蒙原生App的支持还在迭代中大部分情况下“鸿蒙兼容”指的是能通过H5或现有App包在鸿蒙上运行也就是说你正常打一个APK包在鸿蒙上装好也能跑只是不是原生鸿蒙包。如果要真做鸿蒙原生就得用ArkTS重写这不是一个量级的工程量。对公考平台这种MVP项目先用APK兼容鸿蒙完全够用。6. 最终的小结与心得做这个公考移动学习平台回过头来看更像是一次“全栈基本功的大练兵”。Uniapp这边我彻底搞懂了条件编译的本质明白了跨端开发并不只是“一套代码走天下”而要在框架封装的边界上做谨慎的取舍和兜底SpringBoot这边我对数据库表设计、接口分层、权限拦截、性能优化的理解从“知道概念”变成了“能在真实场景做出权衡”Vue管理后台则教会了我如何从运营者视角设计一套高效的内容管理工具。最后分享一个我个人的体会这类全栈项目最怕的不是某个技术难点搞不定而是被“看着啥都想做”的心态拖垮。把产品核心闭环刷题—判分—错题—统计做扎实比堆十个花哨功能重要一百倍。前期多花点时间在表结构设计和接口契约上后期能避免近乎灾难的返工。如果你也准备做类似的项目建议给自己划定一个清晰的版本边界MVP阶段只做刷题闭环课程、分享、数据看板全部放下一版。稳住核心链路剩下的慢慢补整套系统才会真正走得更稳。