资讯详情

校园论文选题系统开发实战:Laravel+uniapp+微信小程序

📅 2026/9/25 22:10:45 | 华诺云谱 👁 阅读
校园论文选题系统开发实战:Laravel+uniapp+微信小程序
毕业论文选题每年春季都是高校信息部门最头疼的环节。纸质表格传阅、Excel来回汇总、学生线下找老师签字协调一套流程走下来少说两周还免不了各种重复和错漏。后来我接手了一个校园团队的项目用 Thinkphp/Laravel 作为后端、uniapp 开发前端、再编译成微信小程序把“教师出题 → 管理员审核 → 学生选题 → 师生互认”整条链路搬到了线上。学生用微信扫码就能查看题目、提交选题教师在后台维护题目池管理员一键导出冲突名单。系统上线之后选题周期从两周压缩到三天最明显的变化是再也没有人抱着 Excel 在各个办公室来回跑了。这篇博文适合两类人看一是准备搭建校园业务系统的开发者不管是毕设选题、选课还是报修这套架构都能直接套二是想练手“前后端分离 小程序端”完整项目的朋友整条链路不复杂有一点点 PHP 和 Vue 基础就能跟上。下面我会把需求设计、数据库结构、后端接口、uniapp 适配小程序的坑、打包上架需要注意的事以及我在并发选题时遇到的“超选”问题全部摊开讲清楚。1. 需求分析与整体设计思路1.1 选题场景的痛点在哪很多人以为选题系统就是个“学生选题目”的简单应用真正做下去才发现线下流程的痛点远比想象中多。信息不透明是最先暴露的问题。以往题目名额靠老师在群里喊、靠纸质清单传阅学生根本不知道哪个题目还有几个名额经常出现几十个人同时看中同一个题目最后全靠线下协调。然后是流程冗长学生要打印选题表、找指导老师签字、交到教务办公室材料稍微填错一点就要重跑一轮。学校层面更是头疼每学期结束后要统计师生比、热门方向分布、跨专业选题情况如果原始数据都在纸质材料和 Excel 里做一次教学评估统计能熬掉好几个通宵。所以这套系统的核心角色其实很明确学生要快速看到题目、提交选题、查看审核结果教师要发布题目、审核学生申请、确认带教关系管理员要审核题目是否合规、处理冲突、汇总统计。三者的操作界面完全不同但数据全部围绕同一张“题目池”转。我在设计流程时把选题生命周期拆成了几个节点教师维护题目题目先进入“待审核”状态管理员审核通过后才对学生可见学生提交选题申请后该选题记录是“待确认”教师确认后学生正式锁定选题如果教师拒绝学生可以立即改选其他题目。这个状态机是整个系统的核心逻辑后面我专门用一节细讲。1.2 为什么选 Thinkphp/Laravel uniapp 这样的技术组合后端在 Thinkphp 和 Laravel 之间怎么选是我一开始就遇到的问题。这两个框架在国内 PHP 圈都属于“主流中的主流”网上争论也有很多我最后给出的结论是看团队维护能力和项目体量。Thinkphp 的优势是门槛低、中文文档全、上手快传统后台管理系统的开发效率很高。Laravel 的优势在生态和工程化Eloquent ORM 处理模型关系非常舒服中间件机制对权限控制很友好Composer 管理器让第三方扩展的接入成本低很多。考虑到这个系统要做 RESTful API、要处理多角色权限、后续还想加消息推送和统计报表我最终选了 Laravel。但如果你更熟 Thinkphp完全可以用 ThinkPHP 8 照着做本文提到的路由分组、权限中间件、事务防冲突这些设计都是通用的。前端用 uniapp 基本没什么悬念。这个项目的最终交付物是微信小程序但后台管理页面如果也单独开发一套 Web 就太浪费了。uniapp 一套代码可以同时编译到微信小程序、H5 和 App我只需要写好一套页面小程序端给老师学生用H5 端作为管理后台入口后续想上安卓端也不用重写。开发时配合 uni-ui 或者 uView 组件库页面搭建效率会高不少。整体架构就是标准的前后端分离uniapp 编译出来的小程序负责界面交互后端只输出 JSON 接口数据落在 MySQL。小程序端通过 HTTPS 请求访问后端域名登录态用 Token 管理后台管理端也可以用同样的接口链只是页面布局不同。模块技术选型选型理由后端框架Laravel / ThinkPHP 8前后端分离模式下接口开发效率高权限中间件成熟前端框架uniapp Vue3一套代码编译小程序、H5、App降低多端维护成本UI 组件uni-ui / uView表单、弹窗、列表组件开箱即用贴近移动端交互数据库MySQL 8事务支持完善配合 InnoDB 行锁解决并发选题冲突缓存RedisToken 存储、接口限流、高频题目列表缓存2. 后端框架搭建与核心业务模块实现2.1 数据表设计与角色权限模型数据库设计是这套系统的地基我建议不要一上来就写代码先把表和约束理清楚。第一张是用户表 users字段包含 id、姓名、学号/工号、密码哈希、角色、所属学院。角色我不用字符串散存而是单独建 roles 表做映射因为后期很可能出现“辅导员可以审核题目但不能管理教师”这类细分权限。用户表与角色表用多对多关联但这个项目里一人通常只有一个角色所以我直接加了 role_id 外键简化查询。第二张是题目表 topics这是系统的核心资产。字段包括 id、教师 ID、标题、描述、所属方向、选题人数上限、当前已选人数、审核状态、发布时间。这里有两个字段要特别注意max_students 是名额上限selected_count 是已选人数两者配合用于选题时的名额校验。审核状态我单独用 audit_status 字段区分与“题目是否已满”完全解耦避免逻辑搅在一起。第三张是选题记录表 selections记录学生和题目的关系。字段是 id、学生 ID、题目 ID、申请状态、申请时间、确认时间。这里有个非常关键的设计给学生 ID 加唯一索引。这意味着每个学生在同一批次里只能产生一条选题记录从数据库层面杜绝“一个学生提交多个申请”的情况。状态字段我设了 pending、approved、rejected 三个值对应待确认、已确认、已拒绝。第四张是消息通知表 messages当教师审核、管理员驳回、题目状态变化时写入记录小程序端“我的消息”直接查这张表不用对接第三方推送服务。我们可以直接在 Laravel 里用迁移文件维护这些表迁移文件进版本库团队成员拉下来执行php artisan migrate就能同步库结构。ThinkPHP 的话建议用自带的 migration 工具别用 PHPMyAdmin 手工改表否则上线之后环境不一致会非常痛苦。2.2 选题流程状态机与防冲突设计状态机是整个业务逻辑里最容易被新手写乱的部分。我把它拆成了两条独立的状态线一条是“题目生命周期”另一条是“选题申请生命周期”。题目生命周期的状态是待审核 → 已通过 / 已驳回 → 已满员 / 已下线。教师新建题目时状态为待审核只有管理员审核通过的题目才出现在学生的题目列表里。已通过状态下的题目如果被选满自动变成已满员学生端要隐藏或者置灰。已驳回的题目教师可以修改后重新提交。选题申请生命周期稍微复杂一点。学生提交申请时状态为 pending教师收到通知后可以选择通过或拒绝。通过后该学生的选题流程结束学生不能再选其他题拒绝后学生可以马上改选其他题目。这里最需要注意的是“申请通过”和“名额占用”之间的时序问题。最典型的场景是一个题目只剩最后一个名额两百个学生同时点击选题如果代码是“先查名额是否够再插入记录”并发情况下会有大量请求同时通过名额校验最后把已选人数冲到远超上限。这就是超选和电商秒杀超卖是同一个道理。解决超选的标准做法是使用数据库事务配合行锁。以 Laravel 为例伪代码如下DB::transaction(function () use ($studentId, $topicId) { $topic Topic::where(id, $topicId) -lockForUpdate() -first(); if ($topic-selected_count $topic-max_students) { throw new \Exception(该题目已满员); } $topic-increment(selected_count); Selection::create([ student_id $studentId, topic_id $topicId, status pending, ]); });lockForUpdate()会对命中行加排他锁事务提交前其他事务只能等待这样就把“检查名额”和“占用名额”变成了原子操作。加上 selection 表的学生 ID 唯一索引即便上面逻辑漏掉了一处数据库层面也会兜底拦住重复申请。我踩过这个坑后最深切的体会是业务代码可以加各种判断但最终兜底一定得靠数据库约束。2.3 路由配置与用户登录态管理接口路由按模块分组是所有后端框架的共同做法。在 Laravel 里我写在 routes/api.phpRoute::prefix(auth)-group(function () { Route::post(login, [AuthController::class, login]); Route::post(wechat-login, [AuthController::class, wechatLogin]); }); Route::middleware(auth:sanctum)-group(function () { Route::get(topics, [TopicController::class, index]); Route::post(topics/{id}/select, [SelectionController::class, apply]); Route::get(my-selections, [SelectionController::class, mySelections]); }); Route::middleware([auth:sanctum, role:teacher])-group(function () { Route::post(topics, [TopicController::class, store]); Route::patch(topics/{id}, [TopicController::class, update]); });如果用 ThinkPHP对应的 route/app.php 里写法类似只是把语法换成 TP 的Route::group()。这里要重点提醒一个 ThinkPHP 下很容易踩的坑域名或伪静态配置不对会出现访问接口地址跳转异常、404 或者直接下载文件的情况。Nginx 下 ThinkPHP 必须配置好 URL 重写把请求转发到入口文件同时注意重启 Nginx 和 PHP 进程不然你改了路由规则却始终看到旧结果会非常浪费时间。再说登录态。很多从传统 PHP 转过来的朋友习惯用 Session 存用户信息这对前后端分离的小程序场景其实是反向的。小程序端没有 Cookie 机制跨端也不方便维护 Session 文件所以我用 Laravel Sanctum 生成 Token存到 Redis 里设置过期时间小程序每次请求在 Header 里带Authorization: Bearer token。ThinkPHP 可以接入 firebase/php-jwt 生成 JWT原理一样。中后台 H5 场景如果确实要用 Session有几点必须注意Session 驱动不要落默认文件并发一高文件锁就是瓶颈Cookie 要设置 HttpOnly 和 Secure生产环境要定期清理过期 Session。现在框架社区经常爆出老版本的安全漏洞很多都跟 Session、加密解密这些底层组件有关所以依赖一定要及时小版本升级别停留在两年没更新的老版本上这种坑属于“平时没事、一上线就出事”的类型。3. uniapp 前端设计与微信小程序适配3.1 页面框架与导航栏适配前端页面我分了四个 Tab首页题目列表、消息、我的选题、个人中心。题目列表页是学生进入系统后第一个看到的页面顶部放筛选条件题目方向、是否可选下面用卡片展示题目卡片上直接显示剩余名额和“选题”按钮。这里要说一个微信小程序特有的适配问题导航栏高度。部分页面我用的是自定义导航栏因为要在右上角放“扫码选课”的入口但小程序机型不同状态栏高度和胶囊按钮位置都不一样写死高度就会出现页面顶到刘海屏或者按钮错位的情况。正确做法是动态获取const systemInfo uni.getSystemInfoSync(); const menuButton uni.getMenuButtonBoundingClientRect(); this.statusBarHeight systemInfo.statusBarHeight; this.navBarHeight menuButton.height (menuButton.top - systemInfo.statusBarHeight) * 2;这段代码在 onLoad 里执行然后把算出来的高度赋给自定义导航栏容器。实测下来不管是全面屏还是带实体返回键的老机型都不会出现错位。页面生命周期也要注意。题目列表用 onShow 做数据刷新比 onLoad 更合理因为学生从详情页返回列表后名额可能已经被别人抢走了需要在页面重新展示时同步最新状态。onPullDownRefresh 配合开启页面配置的 enablePullDownRefresh下拉时重新拉接口。列表滚动到底部用 onReachBottom 做分页加载一次拉 10 条左右体验比较流畅。3.2 请求封装与多域名指向处理小程序的网络请求不能直接用原生的wx.request到处写一定要统一封装。我在 utils/request.js 里做了一件事统一拼接 baseURL、添加 Token、统一处理 401 跳转、统一拦截业务错误码弹 toast 提示。封装之后业务代码里只需要关注成功逻辑代码量能少三分之一。这里涉及一个很多人在网上搜的问题——uniapp 封装 H5 如何指向两个域名。实际场景是这样的H5 端调试时要连本地开发环境上线时要连测试环境或者生产环境如果 baseURL 写死每次切换都要改代码。我用条件编译解决const config { // #ifdef H5 baseURL: process.env.NODE_ENV development ? http://localhost:8001/api : https://api.example.com/api, // #endif // #ifdef MP-WEIXIN baseURL: https://api.example.com/api, // #endif };小程序端的域名必须在微信公众平台配置 request 合法域名而且必须是 HTTPS否则真机调试直接报错。H5 端如果涉及跨域在 manifest.json 里配置 h5.devServer.proxy把 /api 代理到后端地址这样浏览器就不会有跨域拦截。这样一个 baseURL 配置块就能同时覆盖两个域名的场景。3.3 自定义分享、扫码与缓存时间控制毕业论文选题系统里“分享给好友”是个高频操作。学生看到一个合适的题目想发给同学参谋一下这时候微信默认分享的页面标题太简单我重写了 onShareAppMessageonShareAppMessage() { return { title: 题目推荐${this.topic.title} · 还剩${this.topic.remaining}个名额, path: /pages/topic/detail?id${this.topic.id}, imageUrl: this.topic.cover || , }; }注意这里的 path 一定要带上题目 id好友点进分享卡片后就能直接定位到详情页。分享出去后也不要忘记在详情页 onLoad 里解析参数判断是普通打开还是分享卡片进入。扫码功能是另一个便捷入口。教师可以在后台打印题目二维码学生用小程序扫一扫直接进选题页。uniapp 里调用uni.scanCode就行拿到的 result 是一个约定好的内容比如topic:123前端解析出 id 后跳详情。实际体验下来核心点是二维码别做得太小太密白边至少留 10px否则低端安卓手机经常识别失败。小程序缓存这块我用uni.setStorageSync存一些不敏感数据比如开屏公告、题目分类字典。原生 storage 没有过期时间所以我自己包了一层function setItem(key, value, expireSeconds 86400) { const data { value, expire: Date.now() expireSeconds * 1000 }; uni.setStorageSync(key, data); } function getItem(key) { const data uni.getStorageSync(key); if (!data) return null; if (data.expire Date.now() data.expire) { uni.removeStorageSync(key); return null; } return data.value; }把分类缓存设成一天过期列表数据不缓存保证学生每次进来看到的是最新的名额信息。这里我建议的原则是只缓存静态字典绝不缓存动态数据。3.4 移动端特殊处理视频展示与图表导出有些教师在发布题目时会附一个短视频介绍比如实验设备的操作演示。小程序端我用 video 组件做播放但切记小程序原生 video 组件层级很高普通弹窗和浮层可能盖不住需要封面蒙层时用 cover-view 而不是 view 去覆盖。苹果手机如果开了静音视频默认没有声音需要在 video 上设置playSilent或者引导用户关闭静音不然学生会以为视频坏了。另一个容易翻车的场景是图表导出。系统里有个统计页用 echarts 做各方向题目热度图在 uniapp 的 vue3 项目里要用 renderjs 方案引入 echarts。我遇到过 canvas 导出白图的问题后来排查到原因是 uni.canvasToTempFilePath 调用时 canvas 还没完成绘图。解决办法是等 echarts 的 setOption 回调执行完成后再调用导出方法并且在 setOption 里不要将渲染模式设成 SVG否则二维码导出也会异常。这些在真机调试时最容易暴露模拟器上反而不明显。4. 打包发布与真实踩坑记录4.1 manifest 配置与上架审核要点uniapp 项目最终要正式发布到微信小程序很多配置在 manifest.json 里统一管理。这里最容易忽略的有三块。第一是基础配置里的微信小程序 AppID必须填真实的小程序 AppID不能留测试号否则真机预览和上传都会被限制。第二是权限配置小程序端如果涉及选择图片上传身份证、拍照等操作需要在 manifest 里声明对应的隐私接口。第三是微信平台的隐私保护指引2023 年之后新提交审核的小程序必须配置用户隐私保护指引声明会收集哪些信息、用途是什么否则审核会被打回。审核阶段要格外注意“虚拟支付”的红线。如果系统后续加入了论文查重费、报告打印费这类虚拟商品购买直接在小程序内实现微信支付大概率会被拒原因就是苹果对虚拟支付有严格限制。我的处理方案是小程序只做“提交订单”和“复制订单编号”实际付费跳转到公众号 H5 或者线下扫码支付这样既合规又能完成业务闭环。另外小程序内不能出现诱导分享的文案比如“分享后解锁选题名额”这类审核团队查得很严。我第一版就栽在“分享得次数”功能上被驳回后只能乖乖删除。manifest 里的 H5 设置也别忘了小程序和 H5 是同一套代码但 H5 需要单独配置路由模式和静态资源路径否则部署到服务器会出现刷新 404 或者资源加载失败的问题。4.2 选题高峰期高频问题排查速查表系统上线后我整理过一张运维排障表这里把最高频的几个问题原样放出来问题现象可能原因解决办法微信开发者工具真机预览报“request 合法域名 not configured”小程序后台未配置 request 合法域名或域名未过 ICP 备案登录微信公众平台添加 HTTPS 域名并等待生效同时检查后端服务器有没有返回跨域头小程序不需要跨域头H5 需要学生分享题目给好友好友点进来是空白页path 上带的中文参数未编码或者 detail 页 onLoad 解析参数时机不对分享时对参数做 encodeURIComponent详情页 onLoad 里先 uni.$off 再 uni.$on 处理跨页面事件下拉刷新不生效页面的 json 配置里没有设置 enablePullDownRefresh: true开启页面配置并在 onPullDownRefresh 回调结束后调用 uni.stopPullDownRefresh()图片上传提示“上传域名不合法”微信公众平台的上传合法域名没配和 request 合法域名是两套在公众平台配置 uploadFile 合法域名服务器同时确认 nginx client_max_body_size 已调大Canvas 导出统计图是白图导出时机早于 canvas 绘制完成或渲染模式设置为 SVG等待 setOption 回调使用 canvasToTempFilePathecharts 配置更改为 canvas 渲染苹果手机静音模式下视频有声/无声表现异常video 组件受系统静音键影响设置 playSilent 属性或页面内增加“音量提示”引导用户扫码一直提示识别失败二维码尺寸太小或对比度不足二维码生成时至少 300x300 像素白边留 10px 以上避免深色背景这张表我建议直接保存下来上线前对着过一遍能省掉不少远程帮同事排查的时间。有些问题不是代码 bug而是平台配置项和项目配置项不一致这类问题最坑因为本地调试永远复现不了。5. 性能、安全与后续扩展5.1 并发选题防超选的关键设计前文讲状态机时提到过事务和行锁这里我再展开说说为什么必须这么做。选题系统的并发请求高峰非常集中管理员上午十点统一开放选题学生基本在同一秒涌入。如果不做任何并发控制一个剩余 1 个名额的题目可能被 50 个学生同时选中数据库里 selected_count 会变成 51前 50 个学生都以为自己选上了后续的申诉和人工协调能把管理员逼疯。单纯在 PHP 代码里加 if 判断解决不了问题因为多个请求可能同时读到旧数据。正确姿势是数据库层面的锁机制开启事务用SELECT ... FOR UPDATE锁定目标题目行在事务内检查已选人数是否达到上限未满则更新人数、插入选题记录提交事务。如果觉得行锁对数据库压力大可以加一层 Redis 预扣先DECR剩余名额小于 0 就拒绝再把选中的请求异步写入数据库。这种方案性能更高但引入了最终一致性问题一旦数据库写入失败Redis 的名额就对不上了。对这个项目的体量来说MySQL 行锁方案最可靠也最简单完全是“杀鸡用牛刀”的稳妥做法。上线前我用 200 个并发请求做过压测最终记录数稳定等于名额数没有一条超卖数据落到库里。5.2 安全加固与合规建议校园系统的用户信息非常敏感安全底线必须拉高。我做的第一件事是密码存储后端只存密码哈希绝不存明文第二件事是接口访问控制学生角色只能访问自己的选题数据即使临时改接口参数也不能越权查看别人的信息第三件事是内容安全题目描述里不能出现不当言论后端接入微信的内容安全检测接口提交时实时校验审核不通过直接拦截。接口防刷也要做。选题接口一旦被脚本批量请求就算有事务兜底也会给数据库带来很大压力。我按学生维度做了简单限流比如一分钟内最多请求 30 次超过就返回“操作太频繁”前端配合弹窗提示。Laravel 自带 throttle 中间件配置一下就能用ThinkPHP 可以自己写个 Redis 计数器实现同样的效果。还有个容易被忽视的合规细节小程序端收集学生的学号、姓名属于个人信息前端页面要展示隐私协议并让用户主动勾选同意。隐私协议不是走过场内容里要写清楚数据用途和存储方式否则审核阶段会被打回。密码凭据和 Token 也不要存放在前端 storage 的可被读取区域Token 尽量通过请求头传递避免泄露。5.3 后续还能怎么扩展这套系统目前的形态是“选题 审核 统计”但底层的角色权限和消息通知机制完全可以延伸到整个论文过程管理。最直接的扩展是开题报告、中期检查、查重结果、答辩评分这些模块都是典型的“教师提交、学生查看、管理员审核”结构新增几张表就能跑通。消息模块可以对接微信订阅消息把“选题结果已出”“论文被退回修改”这类通知主动推送到学生微信比单纯站内信体验好很多。数据层面我建议给统计模块单独建表用定时任务每天聚合一次师生比、选题方向分布、学院完成率后台展示的时候直接查聚合表不会因为刷统计页把主业务拖慢。导出功能做成异步任务管理员点击导出后生成 Excel 文件下载链接通过消息中心推送避免大列表导出时请求超时。如果将来学校要做多学院、多学期的选题管理只需要在 topics 表和 selections 表上增加 term_year 和 college_id 两个维度字段路由里带上筛选条件系统骨架基本不用动。这套设计的核心收益就是当初花了心思把状态机和权限模型做干净后面加任何功能都只是在这个骨架上填充血肉。6. 个人经验与项目复盘项目做完复盘我最想强调的一点是技术选型真的比大多数人想象中重要但更重要的是把状态机、并发、权限边界这些“看不见的骨架”先想明白。我第一版后端用的是另一个轻量框架当时觉得能跑就行结果做到消息通知和权限分级时越来越吃力最后花了两天时间迁到 Laravel数据表结构完全没动只改了模型层和路由层就平滑过渡了。这件事之后我养成了习惯新项目不管大小先把数据关系和状态流转画出来再决定怎么写代码。另一个体会是 uniapp 的条件编译真是好东西。同一个页面在小程序端和 H5 端的行为差异比想象中大得多比如分享在小程序端是一个 API在 H5 端就是复制链接扫码在小程序端能直接调相机H5 端走的是外部适配。用#ifdef MP-WEIXIN这种注释块把差异代码隔离起来维护成本会低很多。最后分享一个小技巧微信小程序后台有个“真机调试”入口我在开发阶段经常遇到模拟器正常但真机白屏的问题多数出在 ES6 兼容性和uni内置 API 的真机差异上。遇到这类问题别反复改代码直接扫码看 console 报错比瞎猜高效十倍。这套系统前后加起来开发周期大概六周真正花时间的不是写业务代码而是处理边界情况和线上部署。把这套方案记录下来的原因是选题、选课、报名这类“名额有限、并发集中、角色复杂”的业务场景在校园里实在太常见了希望这份踩坑实录能帮后面做类似项目的人少走几步弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑