简介:这是一套基于Python与Vue前后端分离的大学生社会实践申报系统毕业设计源码案例,面向计算机相关专业学生、毕设开发者及初级全栈学习者,可解决课程设计和毕业设计过程中从业务需求分析到系统编码落地的实际难题。压缩包共717个文件,大小35.82MB,内容包括39个Python源码文件、33个Vue组件、162个JavaScript脚本及51个CSS样式等前端资源,另有2个SQL数据库脚本、运行与安装批处理文件,整体结构清晰便于定位。已有91人学习下载。资源附带了数据库初始化脚本、一键启动脚本和多个备份文件,同时包含PPT、Word说明文档及演示视频,可帮助快速部署并理解社会实践申报系统的审核、统计等核心模块设计思路,适合需要参考完整全栈项目结构或前后端交互写法的开发者。
1. 基于Python的大学生社会实践申报系统,这套前后端分离的毕设到底在解决什么?
每年毕业设计选题里,大学生社会实践申报系统出现的频率都很高,核心原因在于它足够真实:纸质申报表、Excel汇总、导员逐级审核、团委统一认定学分,这套流程在不少高校仍然存在,状态靠人盯,统计靠手算。用Python写后端接口、用Vue写前端页面、按前后端分离的方式把申报闭环线上化,正好覆盖了毕业设计中数据库设计、接口开发、界面实现、权限控制、部署演示的全部考核点。对学生端是提交材料和查看进度,对导员端是审核和退回,对团委端是认定学分与导出报表,三方各有一个视图。适合选题偏管理系统方向、又不想在框架选型上过度冒险的同学,也适合想快速搭建一套企业内部申报小系统的在职开发者参考。
2. Python后端与Vue前端的选型理由,以及数据库表怎么设计
2.1 Django还是FastAPI,Vue2还是Vue3
标题只写了“基于Python”,没有限定具体框架。对申报系统这种以增删改查和审批流为主的管理系统,我一般推荐Django搭配Django REST Framework(DRF)。理由是Django自带ORM、Admin后台、迁移工具,DRF的ModelViewSet配合Router可以快速暴露标准REST接口,评分演示时Admin后台还能直接查看数据维护情况,等于白送一个加分点。
FastAPI的优势在异步性能和自动生成OpenAPI文档,如果课题偏向接口性能优化或需要WebSocket推送,选FastAPI更合适。但实践申报系统的并发量不会超过校园场景的日常访问,没必要为了异步特性引入额外的数据库会话管理复杂度。Flask则适合做原型,功能扩展需要自己集成一堆第三方库,毕设周期内容易陷入选择困难。
前端方面,Vue3是当前主流,配合Vite脚手架,依赖安装和热更新都明显快于Vue2的webpack方案。Vue2虽然还有大量存量项目,但新写的系统还选Vue2,答辩时大概率会被问“为什么不升级”。Vue3配Vue Router和Pinia,分别解决路由与状态管理,正好对应学生端、导员端、团委端的三套视图。
2.2 数据库模型:把状态和审批动作拆开
申报系统最核心的表不是用户表,而是申报单(Application)和审批记录(ApprovalRecord)两张表。很多毕设把申报、审批、学分认定全塞进一个status字段,状态一复杂就越改越乱。正确做法是主表只保留当前状态,历史审批动作单独落一张日志表,方便审计追溯。
-- application 表:当前状态 CREATE TABLE application ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, project_id BIGINT NOT NULL, semester VARCHAR(20) NOT NULL, -- 如 2026-2027-1 practice_type VARCHAR(50) NOT NULL, -- 志愿服务/社会调查/实习实践 report_url VARCHAR(255), -- 实践报告附件 status VARCHAR(20) DEFAULT 'pending', -- pending/approved/rejected/credited rating VARCHAR(10), -- 优秀/合格/不合格 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 审批记录表:审计日志 CREATE TABLE approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, application_id BIGINT NOT NULL, actor_id BIGINT NOT NULL, action VARCHAR(20) NOT NULL, -- submit/approve/reject/credit comment TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );| 应用模块 | 主要职责 | 对外核心接口 |
|---|---|---|
| accounts | 用户注册、登录、JWT签发 | /api/auth/login、/api/auth/me |
| practice | 实践类型、实践项目管理 | /api/projects、/api/practice-types |
| applications | 申报提交、审核、学分认定 | /api/applications、/api/review |
参数说明:status只保存当前状态,approval_record保存每一次动作和操作者,答辩时老师问“这个单子被谁退回、因为什么原因”,查一条记录就能说清楚。report_url存文件路径而不是二进制内容,文件放Django的MEDIA目录,数据库只留引用,以后做文件迁移也更方便。semester和status建议都加索引,列表页最常见的两个筛选条件就靠它们。
对应的Django模型可以这样写:
# applications/models.py from django.db import models class Application(models.Model): class Status(models.TextChoices): PENDING = 'pending', '待审核' APPROVED = 'approved', '已通过' REJECTED = 'rejected', '已退回' CREDITED = 'credited', '已认定' student = models.ForeignKey( 'accounts.User', on_delete=models.CASCADE, related_name='applications') project = models.ForeignKey( 'practice.Project', on_delete=models.CASCADE, related_name='applications') semester = models.CharField('学期', max_length=20, db_index=True) practice_type = models.CharField('实践类型', max_length=50) report_url = models.CharField('报告附件', max_length=255, blank=True) status = models.CharField( '状态', max_length=20, choices=Status.choices, default=Status.PENDING, db_index=True) created_at = models.DateTimeField(auto_now_add=True)这里把semester和status设置成db_index=True,是因为申报列表页最常见的查询就是“按学期”和“按状态”,数据量到几万条时有没有索引差别非常明显。Python代码里不需要手动维护created_at,Django的auto_now_add会自动写入创建时间。
2.3 用Django创建项目骨架的最小命令
新机器上建议先按Python 3.10或3.11版本安装,装完在终端确认python --version正常再继续。Web开发尽量用虚拟环境隔离依赖,避免和系统全局包冲突。
# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate # 安装依赖 pip install django djangorestframework django-cors-headers PyJWT # 创建项目与三个核心应用 django-admin startproject practice_system . python manage.py startapp accounts python manage.py startapp practice python manage.py startapp applications参数说明:django-cors-headers是前后端分离项目里的必装项,开发时Vue跑在localhost:5173,Django跑在localhost:8000,没有它浏览器会直接拦截跨域请求。accounts管用户角色,practice管实践项目和类型,applications管申报和审批,三个应用按业务边界拆开。创建完记得在settings.py的INSTALLED_APPS里注册这三个应用,再执行python manage.py migrate,Django会自动建好默认的用户表。
3. Python后端API设计:JWT登录认证、申报提交与审批流转
3.1 用PyJWT签发登录token,并把角色写进payload
系统有三种角色:学生、导员、团委秘书。学生只能提交和查看自己的申报单,导员只能审批本学院的申请,团委秘书负责认定学分并导出统计。前后端分离没有session机制,最稳妥的方案是JWT认证。
# apps/accounts/utils.py import jwt from datetime import datetime, timedelta from django.conf import settings def make_token(user): payload = { 'user_id': user.id, 'role': user.role, # student / advisor / secretary 'exp': datetime.utcnow() + timedelta(hours=settings.JWT_EXPIRES_HOURS), } return jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')逻辑说明:role写进payload后,视图层判断权限时不需要每次查数据库确认角色,减少一次磁盘I/O。exp控制token有效期,毕设项目一般设8到12小时——太短演示时频繁重登,太长答辩时容易被追问“token泄露怎么办”,可以回答用短有效期加定期轮换SECRET_KEY缓解。
解析请求头中token的通用写法:
# apps/accounts/utils.py def get_payload(request): header = request.META.get('HTTP_AUTHORIZATION', '') if not header.startswith('Bearer '): return None, 'missing_token' token = header[7:] try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256']) return payload, None except jwt.ExpiredSignatureError: return None, 'token_expired' except jwt.InvalidTokenError: return None, 'token_invalid'参数说明:header[7:]去掉Bearer前缀,只留token字符串。函数返回(payload, error)二元组,视图层遇到token_expired返回401,遇到token_invalid返回400,前端在axios响应拦截器里分别处理“跳转登录”和“提示重新登录”。
3.2 申报提交与撤回:提前用状态映射限制非法操作
申报接口不能只是简单插入一条数据,还要阻止不合理的状态动作。例如:已经导员审核通过的单子不能撤回;正在pending状态下的单子不能重复提交;被退回的单子必须走重新提交流程。这些规则放在后端状态映射里,比前端只控制按钮显隐更可靠。
# applications/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST class ApplicationActionView(APIView): allowed_actions = { 'submit': ('draft', 'rejected'), 'withdraw': ('pending',), 'resubmit': ('rejected',), } def post(self, request, pk, action): app = Application.objects.filter(pk=pk).first() if not app: return Response({'error': '申报单不存在'}, status=404) payload, error = get_payload(request) if error or app.student_id != payload['user_id']: return Response({'error': '只能操作自己的申报单'}, status=403) if action not in self.allowed_actions: return Response({'error': '非法动作'}, status=HTTP_400_BAD_REQUEST) if app.status not in self.allowed_actions[action]: return Response({'error': f'当前状态不允许{action}'}, status=HTTP_400_BAD_REQUEST) next_status = {'submit': 'pending', 'withdraw': 'draft', 'resubmit': 'pending'}[action] app.status = next_status app.save(update_fields=['status']) ApprovalRecord.objects.create( application=app, actor_id=payload['user_id'], action=action, comment=request.data.get('comment', '') ) return Response({'message': 'success', 'status': app.status})代码逻辑说明:allowed_actions字典定义了每个动作的合法前置状态:submit允许从draft和rejected进入,withdraw只允许pending,resubmit只允许rejected。更新状态时用update_fields=['status'],避免并发情况下其他字段被无意覆盖。每次动作都写一条ApprovalRecord,前端审核日志页就能完整渲染时间线。
3.3 导员审批与团委学分认定:用映射表统一控制角色和状态
审批接口最常见的错误是只改状态不校验角色,结果学生调接口把自己单子改成已通过。解决思路是把“谁能操作哪个动作、什么状态可以转换”集中定义成映射表,用同一个视图处理多个审批动作。
| 动作 | 操作角色 | 前置状态 | 后置状态 | 附加要求 |
|---|---|---|---|---|
| approve | advisor | pending | approved | 无 |
| reject | advisor | pending | rejected | comment必填 |
| credit | secretary | approved | credited | rating必填 |
# applications/views.py class ApplicationReviewView(APIView): ACTION_MAP = { 'approve': {'roles': ('advisor',), 'prev': 'pending', 'next': 'approved'}, 'reject': {'roles': ('advisor',), 'prev': 'pending', 'next': 'rejected'}, 'credit': {'roles': ('secretary',), 'prev': 'approved', 'next': 'credited'}, } def post(self, request, pk, action): rule = self.ACTION_MAP.get(action) if not rule: return Response({'error': '未知审批动作'}, status=400) payload, error = get_payload(request) if error or payload['role'] not in rule['roles']: return Response({'error': '当前角色无权执行'}, status=403) app = Application.objects.filter(pk=pk).first() if not app or app.status != rule['prev']: return Response({'error': f'当前状态无法执行{action}'}, status=400) comment = request.data.get('comment', '') if action == 'reject' and not comment.strip(): return Response({'error': '退回时必须填写原因'}, status=400) app.status = rule['next'] app.save(update_fields=['status']) ApprovalRecord.objects.create( application=app, actor_id=payload['user_id'], action=action, comment=comment ) return Response({'message': f'{action}成功', 'status': app.status})参数说明:rule['prev']和rule['roles']把状态机和权限放在一起管理,以后加“导出学分”之类的动作,只需往ACTION_MAP里补一项。comment对approve是可选,对reject必须校验非空——退回必须给出理由,否则学生不知道从哪里修改,这个细节也是评审老师常看的地方。
4. Vue3前端对接与前后端分离实践:依赖安装、axios请求封装与路由守卫
4.1 用Vite创建Vue3项目并安装依赖
前端环境准备的第一步是装好Node.js,然后执行npm create vite@latest生成工程。对申报系统这种表格加表单密集型页面,组件库能省下大量样式工作量,element-plus是Vue3生态里最成熟的选型。
npm create vite@latest practice-web -- --template vue cd practice-web npm install npm install axios vue-router pinia element-plus npm run dev参数说明:--template vue生成JavaScript版本工程,想用TypeScript可以改成vue-ts,但毕设项目用JS版本改动更快。npm run dev启动后,Vite默认在http://localhost:5173打开,这个端口号后面Django跨域白名单要精确匹配。
Vite的代理配置是前后端分离开发的关键。通过代理把/api请求转发到Django的8000端口,浏览器里就看不到跨域报错:
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })参数说明:rewrite把请求路径里的/api前缀去掉再转发给后端。如果Django的路由本身以/api开头注册,这里就不需要rewrite,两端保持一致即可。changeOrigin: true让后端收到的Host头变成后端地址,避免Django的ALLOWED_HOSTS校验失败。
4.2 axios请求拦截器统一携带token,响应拦截器处理401
前后端分离项目里,token处理是最容易绕晕的部分。把token注入、错误码映射统一封装在axios实例里,后续每个接口模块都不需要重复写认证逻辑。
// api/request.js import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` return config }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') localStorage.removeItem('user') window.location.href = '/login' } return Promise.reject(error.response?.data || { message: '网络异常' }) } ) export default request代码逻辑说明:请求拦截器在每次发请求前从localStorage取token并拼进Authorization头,和后端get_payload里解析的是同一个字段。响应拦截器遇到401时清理登录态并跳转登录页,对应后端返回的token_expired。注意403不要在这里处理,“未认证跳转登录、权限不足给出提示”两个分支要职责清晰,否则学生账号看到导员接口的403也会被莫名其妙踢出登录。
4.3 角色路由守卫与页面路由配置
前端路由先按功能拆页面,再用meta.roles标注该页面允许哪些角色访问。路由守卫在跳转前检查token和角色,未登录就跳登录页,角色不匹配就退回首页。这里也正好体现vue路由参数的两种使用场景:meta传递静态权限信息,query传递列表页的筛选条件。
// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', component: () => import('../views/Login.vue') }, { path: '/', component: () => import('../layout/MainLayout.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('../views/Dashboard.vue') }, { path: 'apply/list', component: () => import('../views/student/MyApplications.vue'), meta: { roles: ['student'] } }, { path: 'apply/new', component: () => import('../views/student/ApplyForm.vue'), meta: { roles: ['student'] } }, { path: 'review', component: () => import('../views/advisor/ReviewList.vue'), meta: { roles: ['advisor'] } }, { path: 'credit', component: () => import('../views/secretary/CreditList.vue'), meta: { roles: ['secretary'] } } ] } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) return next('/login') const user = JSON.parse(localStorage.getItem('user') || '{}') if (to.meta.roles && !to.meta.roles.includes(user.role)) return next('/dashboard') next() }) export default router| 前端路由 | 对应页面 | 允许访问角色 |
|---|---|---|
| /apply/list | 我的申报列表 | student |
| /apply/new | 新增申报表单 | student |
| /review | 待审核列表 | advisor |
| /credit | 学分认定列表 | secretary |
| /project/list | 实践项目列表 | 所有登录用户 |
参数说明:meta.roles是路由元信息,在全局前置守卫里通过to.meta.roles.includes(user.role)判断。前端守卫只能控制页面渲染,真正的权限校验必须同时依赖后端接口里的角色判断——这是答辩时常见的安全问题,需要在演示时主动说明前后端双层校验。
4.4 申报表单提交:附件上传与payload组装
实践报告、盖章扫描件这类附件单独走上传接口。用FormData方式把文件POST到后端,成功后拿到返回的url字段,再塞进申报payload调用创建接口。
// api/application.js import request from './request' export function uploadFile(file) { const formData = new FormData() formData.append('file', file) return request.post('/files/upload/', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) } export function createApplication(data) { return request.post('/applications/', data) }参数说明:FormData由浏览器自动生成multipart类型内容,请求头里不要手动写Content-Type,否则会丢失boundary导致后端无法解析文件。上传成功返回{ url: '/media/reports/xxx.pdf' },前端提交时把url放到report_url字段。后端Django配置好MEDIA_ROOT和MEDIA_URL后,浏览器可以直接访问这个附件地址。
5. Python与Vue联调:启动顺序、三个高频坑与演示验证
5.1 开发和部署时保持同一套启动顺序
先启动Django后端,再启动Vue前端。后端先跑起来,用curl或浏览器访问健康检查接口确认返回结构,再打开前端页面,可以少排查一半问题。
# 后端,项目根目录 python manage.py runserver 0.0.0.0:8000 # 前端,practice-web 目录 cd practice-web && npm run dev后端启动后访问http://localhost:8000/api/auth/me,看到{"error":"missing_token"}说明服务正常,再拿一个需要数据库的接口验证连接。都通了再进入前端页面操作。
5.2 三个高频坑及定位方法
接口404:先看浏览器Network面板里请求的完整URL。如果请求http://localhost:5173/api/applications/转发后变成http://localhost:8000/applications/,而后端路由注册的是api/applications/,那就是vite代理里rewrite把前缀删掉了,参照前端与后端实际URL是否带/api前缀来调整。
请求头带了token但后端一直认证失败:检查Django的SECRET_KEY在签发和解析时是否一致。开发环境如果每次重启进程都重新生成SECRET_KEY,旧token会全部失效,表现就是早上还能登录,重启后全部401。解决办法是把密钥写进环境变量或settings.py固定值,不要动态生成。
跨域报错:确认django-cors-headers已安装并注册到INSTALLED_APPS和MIDDLEWARE,CORS_ALLOWED_ORIGINS里必须写完整地址http://localhost:5173,尾部多一个斜杠或端口写错都会导致拦截。
5.3 答辩演示时值得走一遍的完整闭环
演示前准备两个浏览器账号:学生号和导员号,最好再准备一个团委账号。用学生账号提交一条志愿服务类型的申报,上传一份实际报告文件;切换到导员账号进入/review审核列表,点击通过;再切回学生账号确认状态变为已通过;最后团委账号进入/credit认定学分,填写评级并提交。过程中打开浏览器开发者工具切到Network面板,找到申报和审核两个请求,指出请求头里的Authorization: Bearer <token>,再切到后端控制台展示ApprovalRecord表新增的记录行,这样认证和审计两条链路都能直观看到,比口头描述系统功能更有说服力。如果有条件,进入Django Admin后台,从application表找到这条记录,把状态从pending到credited的变化指给评委看。
本文还有配套的精品资源,点击获取