尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于Laravel与uniapp的毕业论文选题系统设计与实现

发布时间:2026/9/26 8:02:42

资讯中心
01
ARTICLE

基于Laravel与uniapp的毕业论文选题系统设计与实现

基于Laravel与uniapp的毕业论文选题系统设计与实现
毕业设计选题真的是每年教务老师最头疼的环节。去年那版用 Excel 收课题、微信群盯选题的操作一到高峰期就乱课题重复、名额超了、没选上的学生反复问。后来我跟团队给学校做了一套毕业论文选题系统后端以 Laravel 为主线最早原型用 ThinkPHP 快速验证的前端改成 uniapp 编译成微信小程序。整套系统把教师申报、管理员审核、学生选题、双向确认这串流程全部线上化连最终的选题名单都可以一键导出。如果你也在做类似的选题管理系统或者学校学院正好需要一套工具这篇实战记录可以直接参考。里面会讲清楚状态机怎么设计、Laravel 接口怎么写、uniapp 端怎么跟小程序打交道以及我在联调过程中踩过的那些文档上查不到的坑。1. 整体设计从业务痛点梳理到状态机1.1 三种角色与六条核心链路开始动手写代码之前我先把业务角色捋了一遍。用户只有三类管理员、教师、学生。这三类人的诉求完全不同系统要守住的核心链路也就六条教师申报课题。管理员审核课题通过后进入选题池。学生浏览课题池选择导师与题目。教师确认或拒绝学生。管理员统计选题结果处理未选上的学生。系统按学期隔离数据每一届毕业生的选题互不干扰。很多新手拿到这种题目上来就建表写接口结果做到一半发现老师出题审核和学生选题确认之间的状态完全对不上。我的建议是先画一张状态流转图把每个业务节点可能出现的分支都标出来再动数据库。课题状态我用四个关键词待审核、已通过、未通过、已截止。选题状态稍微复杂涉及学生提交、教师确认、退选三个分支我最终简化为已提交待确认、已确认、教师已拒绝、已退选。整套系统的复杂逻辑都围绕这几条状态展开状态定义清楚了接口和界面的设计就是顺着往下走。1.2 为什么我从 ThinkPHP 快速原型迁到 Laravel有朋友看到项目名里的ThinkPHP-Laravel觉得奇怪是不是两个框架混着用其实不是。我在早期验证流程时用 ThinkPHP 写过一版原型因为它建 CRUD 最快一个命令生成控制器和模型半小时能把后台搭起来。但后来发现这个系统需要处理小程序端大量的 JSON 接口、中间件权限校验、并发选课控制ThinkPHP 写起来不慢但代码组织上不如 Laravel 的 Eloquent 和中间件机制清晰。所以我最终架构是整个后台和 API 用 Laravel 重写ThinkPHP 原型里的表结构设计、字段命名习惯、状态枚举全部保留这样业务上不用重新推倒。如果你是从零开始我会直接建议你上 Laravel没必要先去 ThinkPHP 绕一圈。它的route:list、ORM 关联、Sanctum 登录认证写这类小程序后台天然匹配。Laravel 的另一个优势是代码的可读性对做毕业设计的同学来说尤其重要。答辩的时候老师会翻源码Laravel 的 Controller、Service、Model 分层一眼就能看懂比把所有逻辑堆在 index.php 里强得多。2. 数据库设计五张核心表如何撑起整个选题流程2.1 每张表的关键字段与设计意图选题系统不是一个复杂的电商系统主要数据也就是人员、课题、选题关系但字段设计必须提前考虑清楚。我最终保留了这五张核心表。表名核心字段设计意图usersusername, password, role, student_no, major_id, class_name三种角色共存一张表角色用枚举区分避免冗余的人员表categoriesname, semester课题分类比如软件工程、网络安全、大数据方向topicsuser_id, title, description, requirements, category_id, students_limit, selected_count, audit_status, semester选题池所有课题状态集中在这里管理selectionstopic_id, student_id, status, remark, confirmed_at学生与课题的中间表承载双向确认信息operation_logsuser_id, action, detail, ip关键操作留痕方便老师和管理员回溯users 表我在设计时加了一个role字段值是 admin、teacher、student 三个字符串。有人喜欢建 role 关联表但对这种规模的项目没必要一个字段加查询索引就够了。topics 表里最重要的是一对兄弟字段students_limit和selected_count。前者是教师限制的最大选课人数后者是实际已选/已确认人数。每次学生选题成功selected_count加一退选或教师拒绝后减一。这样统计名额只需要读一行数据不用count()去现算。selections 表要注意status的语义默认提交后是待确认教师通过后改成已确认学生可退选教师可拒绝。主键使用自增 id同时给topic_id和student_id联立唯一索引避免同一学生重复提交同一课题。2.2 并发抢题事务加锁防止超额选课选题系统最容易出事故的就是最后几分钟大家都去抢同一位老师的课题。如果不做并发控制两个学生同时把selected_count从 0 改到 1最后一个字段就错了。我在 Laravel 里用事务加锁的方案处理这段代码。核心是使用lockForUpdate()对 topic 行加排他锁让同一时刻只有一个请求能读到最新名额其他请求必须等锁释放。use Illuminate\Support\Facades\DB; use App\Models\Topic; use App\Models\Selection; public function submit(Request $request) { $request-validate([ topic_id required|exists:topics,id, ]); $studentId $request-user()-id; try { $selection DB::transaction(function () use ($topicId, $studentId) { $topic Topic::where(id, $topicId) -where(audit_status, Topic::STATUS_APPROVED) -lockForUpdate() -firstOrFail(); if ($topic-selected_count $topic-students_limit) { throw new \Exception(该课题名额已满); } $exists Selection::where(student_id, $studentId) -whereIn(status, [Selection::STATUS_PENDING, Selection::STATUS_CONFIRMED]) -exists(); if ($exists) { throw new \Exception(你已经有一个进行中的选题); } $topic-increment(selected_count); return Selection::create([ topic_id $topic-id, student_id $studentId, status Selection::STATUS_PENDING, ]); }); return response()-json($selection); } catch (\Exception $e) { return response()-json([message $e-getMessage()], 422); } }有人会问为什么不用 Redis 原子操作因为选题的高峰并发通常也就是每秒几十次数据库锁完全扛得住而且不需要额外维护 Redis 服务对小型项目更实在。真正要注意的是事务里的顺序先锁行再做校验最后更新这是一个不变的顺序。3. 后端接口实现路由、登录、选题 API 的完整方案3.1 小程序登录与权限中间件微信小程序登录不是传统的账号密码登录而是用小程序的 wx.login 拿到临时 code在后端换 openid。用户在绑定过一次学生号之后后续登录直接通过 openid 换取 token。我在 Laravel 里用 Sanctum 做登录态管理auth 表用的是 API token不走 session。这一步非常关键小程序端没有 Cookie 概念如果沿用 Laravel 默认的 session 方式会遇到明明登录了接口还是 401的问题。登录接口的关键逻辑public function login(Request $request) { $request-validate([ code required|string, username required_without:password|string, password required_without:username|string, ]); $user User::where(student_no, $request-username)-first(); if (!$user || !Hash::check($request-password, $user-password)) { $user $this-wechatLogin($request-code); } $token $user-createToken(wechat-mini-program)-plainTextToken; return response()-json([ token $token, user $user-only([id, username, role, student_no]), ]); }路由分组里加上角色校验中间件比如教师端出题接口只允许 teacher 访问管理员接口只允许 admin 访问Route::middleware(auth:sanctum)-group(function () { Route::get(/topics, [TopicController::class, index]); Route::post(/topics/{id}/submit, [SelectionController::class, submit]); Route::middleware(role:teacher)-group(function () { Route::post(/topics, [TopicController::class, store]); Route::post(/selections/{id}/confirm, [SelectionController::class, confirm]); }); Route::middleware(role:admin)-group(function () { Route::post(/topics/{id}/audit, [TopicController::class, audit]); Route::get(/statistics, [StatisticsController::class, index]); }); });中间件代码很简单判断当前用户 role 是否在允许列表里面public function handle($request, Closure $next, string $role) { $user $request-user(); if ($user-role ! $role) { return response()-json([message 没有权限操作], 403); } return $next($request); }3.2 路由设计接口路径与小程序请求地址必须一一对应许多新手在联调的时候发现Laravel 接口地址在 Postman 里能访问小程序却一直报 404。绝大多数原因是路由配置里的地址跳转没对齐比如小程序请求的是/api/v1/topics后端路由写的是/topics中间少了一层v1。我的做法是在RouteServiceProvider里为所有接口统一加/api/v1前缀然后在小程序端封装请求时baseURL 直接指向该前缀。这样将来要升级 v2 版本只需要把前缀改掉小程序端不用大改。Route::prefix(api/v1)-middleware(api)-group(base_path(routes/student.php));还有一个重点是路由必须放在 auth 中间件之外比如登录接口和微信小程序换取 session 的接口。拿 code 换 openid 的接口如果误加了登录校验就会互相锁死前端调登录后端要求先登录等于进死胡同。3.3 汇总统计与名单导出管理员最常看的是选题进度统计我直接用一个聚合查询搞定$stats Topic::selectRaw(audit_status, count(*) as total) -where(semester, $semester) -groupBy(audit_status) -get();导出名单我用 Laravel 的 streamedResponse 输出 CSV不需要额外安装 Excel 库。文件头加上 UTF-8 BOM否则用 Excel 打开中文会乱码return response()-streamDownload(function () use ($selections) { $handle fopen(php://output, w); fwrite($handle, \xEF\xBB\xBF); fputcsv($handle, [学号, 姓名, 课题, 导师, 状态]); foreach ($selections as $item) { fputcsv($handle, [$item-student_no, $item-name, $item-topic_title, $item-teacher_name, config(status.text)[$item-status]]); } fclose($handle); }, selection-list.csv);权限中间件、接口路由、数据统计这三个部分做完后端的骨架已经立起来了。下一步主要精力会放在微信小程序端毕竟学生和老师日常用的都是小程序体验不顺会前功尽弃。4. 微信小程序端uniapp 从构建到发布的全过程4.1 manifest 配置与环境切换uniapp 项目创建后第一件事是改manifest.json。微信小程序模块里填入真实 appid基础库版本选一个比较稳妥的比如 2.30.0 以上否则一堆 API 用不了。项目开发时要注意区分环境我配置了三个环境变量开发环境、测试环境、生产环境。小程序端请求地址切换我之前遇到过最头痛的问题。小程序里request的域名必须是 HTTPS 并且在微信公众平台配置过合法域名但开发时本地后端往往是http://192.168.x.x。我采取的办法是封装一层config.js根据process.env.NODE_ENV自动切换 baseURLconst env { development: http://localhost:8000/api/v1, testing: https://test-api.example.com/api/v1, production: https://api.example.com/api/v1 } export const BASE_URL env[process.env.NODE_ENV] || env.production开发时在微信开发者工具中勾选不校验合法域名就能本地联调。但真机预览必须用测试环境的 HTTPS 域名这是很多朋友卡住的地方。4.2 请求封装与 token 过期处理小程序里我封装了一个request.js所有接口统一走这个入口。它负责三件事拼接 baseURL、自动携带 token、统一处理 401 和错误提示。const request (options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: Bearer ${uni.getStorageSync(token)}, Content-Type: application/json }, success: (res) { if (res.statusCode 401) { uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) reject(res) return } if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else { uni.showToast({ title: res.data.message || 请求失败, icon: none }) reject(res) } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) }uniapp 要求所有接口调用都走 Promise不要直接在业务页面里写uni.request不然后期改 token 刷新、改请求日志都要逐个页面翻非常痛苦。4.3 导航栏高度适配与自定义分享微信小程序顶部导航栏高度计算是个老问题。不同机型、不同微信版本的胶囊按钮位置不一样iPhone 刘海屏跟安卓的 statusBar 高度也不同。我封装了一个获取导航栏信息的函数export const getNavBarHeight () { const { statusBarHeight, platform } uni.getSystemInfoSync() const menu uni.getMenuButtonBoundingClientRect() const navBarHeight (menu.top - statusBarHeight) * 2 menu.height return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight navBarHeight } }页面自定义导航栏时把占位的高度设为totalHeight内容往下挪就不会出现顶部与胶囊重叠的问题。自定义分享好友很简单写在每个页面的onShareAppMessage里返回标题和路径即可。我做课题详情页时把课题 id 带进了分享路径onShareAppMessage() { return { title: this.topic.title, path: /pages/detail/detail?id${this.topic.id} } }4.4 扫码参数解析与缓存处理教师端我做了扫码进入页面生成的小程序码带scene参数。小程序码升级后scene需要通过decodeURIComponent解码再手动拆分参数onLoad(options) { if (options.scene) { const scene decodeURIComponent(options.scene) const params scene.split() const obj {} params.forEach(item { const [key, value] item.split() obj[key] value }) this.topicId obj.topicId } else { this.topicId options.id } }缓存方面选题系统里课题列表数据实时性要求不高但也不能永远不更新。我做了带过期时间的缓存封装比如列表缓存 5 分钟详情缓存 1 分钟。这样快速切页面时响应快也避免每次都打后端。export const setCache (key, value, expire 300) { const data { value, expire: Date.now() expire * 1000 } uni.setStorageSync(key, data) } export const getCache (key) { const data uni.getStorageSync(key) if (!data) return null if (Date.now() data.expire) { uni.removeStorageSync(key) return null } return data.value }小程序端的开发基本可以告一段落但真正折磨人的是上架和联调阶段各种零零碎碎的问题。接下来我把最常遇到的几个集中整理一下。5. 常见问题与避坑实录症状原因解决方案手机预览请求失败未配置 request 合法域名微信公众平台配置 HTTPS 域名开发时可忽略校验接口返回 401登录态丢失token 过期或未携带请求封装统一携带 Authorization401 时跳登录页后端返回 500日志显示 SQL 死锁并发选课未加锁使用事务lockForUpdate 控制 topic 行页面下拉刷新后列表重复分页参数未重置刷新时重置 page1重新请求安卓端 canvas 导出白图绘制未完成就调用 toDataURL绘制完成后延迟 200ms 再导出iOS 静音模式下视频/音乐没声音微信默认跟随静音开关uni.setInnerAudioOption({ obeyMuteSwitch: false })开发工具正常真机扫码打开白屏基础库版本过低manifest 中提高基础库最低版本导出 CSV 用 Excel 打开中文乱码缺少 UTF-8 BOM输出前写入\xEF\xBB\xBF后端还有两个容易被忽略的点。第一是 Laravel 的 CORS 中间件如果未来要支持 H5 端跨域配置必须提前写好第二是 Laravel 的 session 在 API 模式下默认不启用如果沿用某些教程里的session()方法取数据会一直拿到 null。我在项目里全部用 token 机制Session 只在后台管理页的 Web 登录里使用两边彻底隔离。微信小程序虚拟支付相关的问题值得单独提醒一句选题系统如果不涉及虚拟商品不要随意接入支付组件。苹果端对小程序虚拟支付审核很严格我见过不少项目因为误接支付被拒审。还有个小细节是关于 uniapp 生命周期。小程序页面切换时onShow和onLoad的执行顺序容易被忽略。比如从详情页返回列表页列表页的onShow会被调用但onLoad不会。所以刷新列表数据要放在onShow而不是onLoad否则教师确认学生之后学生端列表数据还是旧的。最后说两个我自己印象特别深的细节。第一个是操作日志最初版本我没做结果出现纠纷时根本没法追溯是谁在什么时候批准的课题。加上operation_logs表之后所有审核、确认、退选操作都记录了后端排查问题方便非常多。第二个是课题的semester字段一定要在每一处查询条件里带上第二年开始跑的时候如果没有这个字段所有接口都会把上一届的数据漏出来谁也说不清。整套系统从表结构设计到小程序上线前后花了两周。最耗时间的不是写代码而是把选题流程的异常分支一个个理清楚。你把状态机和权限骨架理清之后剩下的 CRUD 只是体力活。这是我做这个项目最大的体会也建议所有做类似毕业设计的朋友先画流程再写代码能少走很多弯路。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。