前前后后做了好几个类似的校园业务系统,说实话,校园兼职这个方向算是信息管理系统里比较典型的实战练手项目。前后端分离、权限控制、发布报名流程、列表检索,各个模块一应俱全,特别适合拿来验证 Vue 和 Python 这套技术栈的配合能力。这个项目标题写的是“基于 Vue 的校园兼职系统的设计与实现 Python django flask”,我相信很多人看到这里会有同感:这压根不是二选一的问题,而是需要在 Django 和 Flask 之间做一个合理取舍,再配合 Vue 把前后端串起来。这篇文章就把我完整做下来的过程、踩过的坑、以及最终沉淀下来的方案拆开讲清楚,给正准备做类似系统的同学一条尽可能顺的路径。
1. 整体设计与技术选型解析
1.1 技术栈选择的底层逻辑
先说结论:这个项目最终我选择了Django + Django REST Framework 作为后端主体,Flask 作为轻量级辅助服务。标题里同时出现 Django 和 Flask,看着像二选一,实际做的时候完全可以两条腿走路。
你要是自己去搜“Django 还是 Flask”,看到的无非是那句话:Django 是大而全的框架,Flask 是微框架。但真正动手做校园兼职系统之前,你得先搞清楚这两者的差异到底落在什么具体能力上。
| 对比项 | Django | Flask |
|---|---|---|
| ORM | 内置完整 ORM,迁移脚本自动生成 | 默认无 ORM,常配 SQLAlchemy |
| Admin 后台 | 自带 Admin 界面,管理数据超方便 | 需要自己写后台逻辑 |
| 认证体系 | 自带用户模型,配合扩展实现 JWT | 需要完全自己搭 |
| 开发速度 | 适合 CRUD 密集型的业务系统 | 适合原型验证和高自由度接口 |
| 学习曲线 | 稍陡,但套路统一 | 平坦,但后续功能要自己堆 |
我之前有个很直接的感受:校园兼职系统的核心就是用户、兼职信息、报名记录这三张表的增删改查,外加状态流转的管理逻辑。这种业务如果用 Flask 硬写,每个接口都要自己写数据库查询、序列化、参数校验,一个列表页就能写出一堆重复代码。而 Django 的 ORM 和 DRF(Django REST Framework)几乎就是为这种业务量身定做的。
那 Flask 在这个项目里有没有位置?有。我把系统中的信息推送和定时任务拆成了一个独立的 Flask 服务,比如每天晚上做一遍兼职信息过期自动下架、给学生推送报名状态变更提醒。Flask 启动快、干净,单独跑这类轻量服务很舒服。
1.2 前后端分离架构与项目结构规划
既然前端选了 Vue,那就必须走前后端分离的路子。后端不再返回 HTML 页面,只提供 JSON 接口;前端通过 Axios 请求这些接口,在浏览器里渲染页面。这样做的直接好处是:前端开发和后端开发可以完全并行,你后端还没写完接口,前端已经可以用 Mock 数据把页面画出来了。
实际项目目录结构我是这么组织的:
campus-parttime-system/ ├── backend/ # Django 后端 │ ├── manage.py │ ├── config/ # 配置目录(settings、urls、wsgi) │ ├── apps/ │ │ ├── users/ # 用户模块 │ │ ├── jobs/ # 兼职信息模块 │ │ └── applications/ # 报名与收藏模块 │ └── requirements.txt ├── notify-server/ # Flask 轻量服务 │ ├── app.py │ └── tasks.py └── frontend/ # Vue 前端 ├── src/ │ ├── api/ # 接口请求封装 │ ├── router/ # 路由配置 │ ├── views/ # 页面组件 │ └── store/ # 状态管理 └── package.jsonDjango 里我把业务拆成了users、jobs、applications三个 app,每个 app 只负责自己那一摊事。一开始你可能觉得这样拆麻烦,但等代码量上来以后就会发现,每个模块的 models、serializers、views 各归各的,改一个模块完全不影响另一个,排查问题的时候定位也快。
1.3 数据库设计与核心表结构
数据库设计这块,我直接用了 MySQL,但开发阶段建议先用 SQLite 顶着。原因很简单:SQLite 零配置,创建数据库就是本地文件,Django 默认就能跑;等系统上线再切换到 MySQL,只需要改 settings.py 里的数据库配置,然后跑一次迁移就行。
核心表结构有四张:
用户表(users_user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| student_id | varchar(20) | 学号,唯一 |
| username | varchar(50) | 用户名 |
| password | varchar(128) | 密码哈希值 |
| phone | varchar(11) | 手机号 |
| role | varchar(10) | 角色,student / admin |
兼职信息表(jobs_job)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| title | varchar(100) | 兼职标题 |
| description | text | 兼职描述 |
| salary | decimal(10,2) | 薪资 |
| location | varchar(100) | 工作地点 |
| work_time | varchar(100) | 工作时间段 |
| publisher | int | 发布人外键 |
| status | varchar(10) | published / closed / pending |
| created_at | datetime | 发布时间 |
报名记录表(applications_application)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| user | int | 报名学生外键 |
| job | int | 兼职外键 |
| status | varchar(10) | applied / accepted / rejected |
| apply_time | datetime | 报名时间 |
收藏表(applications_favorite)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| user | int | 用户外键 |
| job | int | 兼职外键 |
| created_at | datetime | 收藏时间 |
设计时的关键约束点:报名记录要把user和job组合起来设置为唯一约束,确保同一个学生不能对同一条兼职信息重复报名。
2. 后端接口开发与核心逻辑实现
2.1 Django 项目初始化和模型创建
搭环境这一步虽然基础,但值得认真走一遍。我习惯用虚拟环境隔离依赖,避免不同项目相互污染。
# 创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS / Linux) source venv/bin/activate # 安装核心依赖 pip install django djangorestframework django-cors-headers djangorestframework-simplejwt mysqlclient # 创建项目与模块 django-admin startproject config . python manage.py startapp users python manage.py startapp jobs python manage.py startapp applications模型创建这块,用户表我直接继承了 Django 自带的AbstractUser,这样省去了自己写密码加密和权限验证的麻烦:
# apps/users/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): student_id = models.CharField(max_length=20, unique=True, verbose_name="学号") phone = models.CharField(max_length=11, verbose_name="手机号") role = models.CharField( max_length=10, choices=[("student", "学生"), ("admin", "管理员")], default="student", verbose_name="角色", ) class Meta: db_table = "users_user" def __str__(self): return self.username兼职信息模型的核心在于状态字段。发布、下架、审核中,这三个状态决定了列表页展示哪些数据:
# apps/jobs/models.py from django.db import models from apps.users.models import User class Job(models.Model): STATUS_CHOICES = [ ("published", "已发布"), ("pending", "待审核"), ("closed", "已下架"), ] title = models.CharField(max_length=100, verbose_name="兼职标题") description = models.TextField(verbose_name="兼职描述") salary = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="薪资") location = models.CharField(max_length=100, verbose_name="工作地点") work_time = models.CharField(max_length=100, verbose_name="工作时间") publisher = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="发布人") status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="pending") created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间") class Meta: db_table = "jobs_job" ordering = ["-created_at"]这里有个小设计:发布兼职时默认走pending待审核状态,管理员审核通过后自动变成published,前端列表页只请求published的数据。这么做是防刷,校园场景下保不齐有人乱发内容,审核环节不能省。
2.2 认证体系与 JWT 鉴权
校园兼职系统涉及学生个人信息,登录认证是刚需。我没有用 Django 默认的 session 认证,因为前后端分离的项目里,前端是静态站点,session 这套 Cookie 机制通信起来很别扭。最终选了 JWT,无状态、能跨域、前端存 Token 也方便。
配置起来其实很清爽:
# config/settings.py 关键配置 INSTALLED_APPS = [ # ... 其他应用 'rest_framework', 'rest_framework_simplejwt', 'corsheaders', ] MIDDLEWARE = [ # ... 其他中间件 'corsheaders.middleware.CorsMiddleware', ] # 允许跨域 CORS_ALLOW_ALL_ORIGINS = True # DRF 配置 REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticatedOrReadOnly', ], }这里注意中间件顺序,CorsMiddleware要尽量往上放,放在CommonMiddleware前面,不然跨域预检请求可能会被拦掉,这个坑我踩过,后面问题排查小节细说。
登录接口用 simplejwt 自带的TokenObtainPairView就能直接跑:
# apps/users/urls.py from django.urls import path from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns = [ path('login/', TokenObtainPairView.as_view(), name='login'), path('refresh/', TokenRefreshView.as_view(), name='refresh'), ]前端每次请求都在 Header 带上Authorization: Bearer <token>,后端拿到 Token 后解析出用户 ID,再通过request.user识别当前是谁在操作。
2.3 兼职信息核心接口:发布、列表与检索
兼职信息的 CRUD 是整个系统最核心的接口组。我把发布、列表、检索三个需求合并到了一个视图里,用 DRF 的ListCreateAPIView处理。
# apps/jobs/views.py from rest_framework import generics from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly from .models import Job from .serializers import JobSerializer class JobListView(generics.ListCreateAPIView): queryset = Job.objects.all() serializer_class = JobSerializer permission_classes = [IsAuthenticatedOrReadOnly] def get_queryset(self): keyword = self.request.query_params.get("keyword", "") sort_by = self.request.query_params.get("sort", "-created_at") queryset = Job.objects.filter(status="published") if keyword: queryset = queryset.filter(title__icontains=keyword) # 排序白名单校验 allowed_sorts = ["-created_at", "salary", "-salary"] if sort_by in allowed_sorts: queryset = queryset.order_by(sort_by) return queryset def perform_create(self, serializer): # 发布人自动取当前登录用户 serializer.save(publisher=self.request.user, status="pending")列表接口一段代码同时搞定了查询全部、关键词检索、排序、分页(分页配置在 settings 里),发布接口还自动把 status 置为待审核。这里最关键的一手是排序白名单。直接把前端传的 sort 参数塞进order_by会造成任意字段排序,虽然不至于拖库,但属于潜在安全隐患,白名单校验成本低,顺手就做了。
分页配置也很简单:
# config/settings.py REST_FRAMEWORK = { # ... 其他配置 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, }一页十条,前端滚动翻页,体验正好。
2.4 报名与收藏的业务闭环
报名这个动作看似简单,但需求细节多:同一个学生不能重复报名同一兼职、报名后兼职的报名数要更新、兼职发布者登录自己的报名列表看有哪些学生、学生能取消报名。
# apps/applications/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from .models import Application from apps.jobs.models import Job class ApplyJobView(APIView): permission_classes = [IsAuthenticated] def post(self, request): job_id = request.data.get("job_id") try: job = Job.objects.get(id=job_id, status="published") except Job.DoesNotExist: return Response({"detail": "兼职不存在或已下架"}, status=400) # 防止重复报名 if Application.objects.filter(user=request.user, job=job).exists(): return Response({"detail": "你已经报名过这条兼职了"}, status=400) Application.objects.create(user=request.user, job=job) return Response({"detail": "报名成功"}, status=201)像这种需要做多个判断才能完成的操作,我用APIView自己写逻辑比 DRF 自动视图更清晰。实际做下来,报名成功之后给兼职发布者发通知这一步我是交给后面的 Flask 服务异步处理的,主 Django 进程里不阻塞请求,响应速度不受影响。
3. 前端 Vue 工程搭建与核心页面实现
3.1 Vue 环境配置与依赖安装
前端环境配置看似简单,其实是很多新手卡壳的第一道坎。Node.js 版本太老或者 npm 源太慢,都能让整个工程起不来。实测下来 Node 建议 16 以上,我用的是 18.19.0,全程没遇到版本兼容问题。
# 检查 Node 版本 node -v # 使用 Vite 创建 Vue 3 项目(比 vue-cli 快太多) npm create vite@latest frontend -- --template vue # 进入项目目录安装依赖 cd frontend npm install # 安装路由、状态管理、UI 库和 HTTP 库 npm install vue-router@4 pinia axios element-plus这里重点说下为什么选了 Vite 而不是 vue-cli。Vite 基于 ESModule,冷启动速度比 Webpack 快了不是一点点,开发阶段改代码热更新几乎是秒级。对校园项目这种需要反复调整页面细节的场景来讲,开发体验提升非常明显。
Element Plus 这个东西你要不装也行,自己手写样式也完全能实现一样的页面。但从零到一把表单项、弹窗、分页、表格全部手搓一遍,项目周期会拉得很难看,所以我直接用现成组件库,把精力花在业务逻辑上。
3.2 Axios 请求封装与路由守卫
前端所有接口请求都走同一个 Axios 实例,这是降低重复代码量的好办法。我更看重的是在拦截器里统一处理 Token 注入和错误提示:
// src/api/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000, }) // 请求拦截:自动携带 Token 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 => { const status = error.response?.status if (status === 401) { localStorage.removeItem('token') router.push('/login') } else if (status === 400) { ElMessage.error(error.response.data.detail || '请求参数有误') } else { ElMessage.error('服务器开小差了,请稍后再试') } return Promise.reject(error) } ) export default request这个封装的好处是业务组件里不用每次写一遍 Token 注入和错误处理。Token 失效跳转登录页这种事,前端用户是无感知的,拦截器里统一判断好,比每个页面单独判断简洁得多。
路由守卫的逻辑也是同样道理,页面访问前先判断该路由需不需要登录态:
// src/router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })3.3 核心页面实现要点:列表、发布、个人中心
前端页面我做下来最费心思的是兼职列表页。它要在同一个页面里同时支持关键词搜索、按薪资排序、分页加载,并且每条信息要展示标题、薪资、地点、状态这几个关键字段。
<!-- src/views/JobList.vue 核心模板 --> <template> <div class="job-list"> <div class="filter-bar"> <el-input v-model="keyword" placeholder="搜索兼职关键词" clearable @keyup.enter="fetchJobs(1)" /> <el-select v-model="sortBy" @change="fetchJobs(1)"> <el-option label="最新发布" value="-created_at" /> <el-option label="薪资最高" value="-salary" /> </el-select> <el-button type="primary" @click="fetchJobs(1)">搜索</el-button> </div> <el-table :data="jobs" v-loading="loading"> <el-table-column prop="title" label="兼职标题" min-width="200" /> <el-table-column prop="salary" label="薪资(元)" width="120" /> <el-table-column prop="location" label="地点" width="150" /> <el-table-column label="操作" width="200"> <template #default="{ row }"> <el-button type="primary" link @click="goDetail(row.id)">详情</el-button> <el-button type="success" link @click="applyJob(row.id)">报名</el-button> </template> </el-table-column> </el-table> <el-pagination layout="prev, pager, next" :total="total" :page-size="pageSize" @current-change="fetchJobs" /> </div> </template>对应的数据请求逻辑:
const fetchJobs = async (page = 1) => { loading.value = true try { const res = await request.get('/jobs/', { params: { keyword: keyword.value, sort: sortBy.value, page }, }) jobs.value = res.results total.value = res.count } finally { loading.value = false } }发布页的关键在于表单校验。兼职标题不能为空、薪资必须大于零、工作地点不能太宽泛,这些前端校验可以在用户填错的时候立刻提醒,减少后端接口被无效请求打到的次数。Element Plus 的表单校验规则非常好用:
const rules = { title: [ { required: true, message: '请输入兼职标题', trigger: 'blur' }, { min: 5, max: 50, message: '标题长度在 5 到 50 个字符', trigger: 'blur' }, ], salary: [ { required: true, message: '请输入薪资', trigger: 'blur' }, { validator: (rule, value, callback) => value > 0 ? callback() : callback(new Error('薪资必须大于0')), trigger: 'blur' }, ], }个人中心页相对简单,展示当前登录用户的信息,外加两个 Tab:我报名的兼职、我发布的兼职(如果是管理员还要加上审核入口)。后边那个“待审核”列表是管理员才有的操作,普通学生看不到,用一个role === 'admin'的判断就可以控制。
4. 前后端联调与常见问题排查
4.1 跨域配置与接口调试技巧
前后端分离项目联调时最常撞见的不是业务 bug,而是跨域问题。浏览器帮你把请求拦在了门外,前端控制台一片红色报错,后端接口单独用 Postman 测又是好的,这就非常让人困惑。
Django 端加上django-cors-headers之后,还需要确认中间件顺序正确:
MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # 放在最上面 'django.middleware.common.CommonMiddleware', # ... 其他中间件 ] CORS_ALLOW_ALL_ORIGINS = True # 开发阶段用,上线要换成具体域名这里有个非常容易踩的坑:中间件顺序不对,跨域请求直接 404。CorsMiddleware 必须放在 CommonMiddleware 之前,浏览器发送预检请求 OPTIONS 时才能被正确处理。我第一次没注意顺序,排错排了一下午。
联调的时候我用的是 Vite 的代理,把/api开头的请求转发到 Django 监听的 8000 端口,这样浏览器里看到的还是同源请求,开发阶段跨域问题直接被规避了。
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, }, }, }, })上线阶段直接把前端构建产物交给 Nginx,Nginx 再根据/api前缀把请求转发给后端,前端代码里还是那套/api开头的请求,不用做任何改动。
4.2 高频报错排查记录
做完整套系统,我把遇到的典型问题整理成了一张速查表,很多问题都是在不同项目里反复出现的类型。
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 前端请求一直 401 | Token 没传或者过期 | 检查 Axios 拦截器,确认请求头里带了 Authorization;过期则走刷新接口 |
| 接口返回 403 | 没有权限 | 检查视图的 permission_classes,确认登录用户有访问权限 |
| 跨域预检请求 404 | 中间件顺序错误 | 把 CorsMiddleware 移到 CommonMiddleware 之前 |
| 创建账号时密码明文存储 | 没使用 Django 加密 | 用 create_user 而非 create 创建用户 |
| 时间显示差了 8 小时 | 时区配置问题 | settings.py 设置 TIME_ZONE = 'Asia/Shanghai' |
| 前端拿到数据渲染不出表格 | 字段名不对 | 用 console.log 打印接口返回值,核对外键序列化字段名 |
其中时间显示这个问题尤其常见。Django 默认时区是 UTC,数据库里存的时间是带时区的 UTC 时间,前端显示出来就比本地时间少了 8 小时。最终我直接在 settings.py 里统一:
TIME_ZONE = 'Asia/Shanghai' USE_TZ = True后端序列化返回的就是东八区时间,前端不用再做时间格式转换,直接显示字符串就行。
4.3 Flask 辅助服务与部署经验
Flask 服务在这套系统里负责跑定期任务。比如每天晚上 12 点自动把过期兼职的状态改成closed,再比如报名状态变更后给学生推送站内信提醒。这类任务用 Django 也能写,但 Django 的启动本身就重,单独写一个 Flask 脚本定时执行会更轻巧。
# notify-server/tasks.py import requests from apscheduler.schedulers.blocking import BlockingScheduler scheduler = BlockingScheduler() def close_expired_jobs(): # 调用 Django 提供的管理接口 requests.post("http://127.0.0.1:8000/api/admin/close-expired-jobs/") def send_remind(): # 推送报名状态提醒 requests.post("http://127.0.0.1:8000/api/notify/push/") scheduler.add_job(close_expired_jobs, "cron", hour=0, minute=0) scheduler.add_job(send_remind, "cron", hour=9, minute=0) scheduler.start()这里要注意:Flask 服务和 Django 服务是通过 HTTP 接口通信的,所以 Django 那些管理接口得设置成只能用内部 Token 调用,防止被外部攻击。我用的是简单的X-Internal-Token请求头做校验,请求方带着固定 Token,服务端验证通过才执行。
部署方面,我的最终方案是:
- Django 用 Gunicorn 跑,4 个 worker,监听 8000 端口
- Flask 用
nohup python tasks.py &常驻后台 - Vue 构建产物
npm run build后丢到 Nginx 的 html 目录 - Nginx 配置里把
/api反向代理到 Django 的 8000 端口,静态资源直接走 Nginx
上线前务必把 Debug 关掉,ALLOWED_HOSTS 配置成真实域名,不然 Django 会报错,甚至可能暴露敏感配置信息。还有数据库连接信息、JWT 密钥、内部通信 Token 这类配置,务必放到环境变量里,别硬编码在代码中提交到仓库。
5. 做一个可用的系统,到底难在哪
抛开技术细节不谈,这个项目真正让我觉得有价值的地方在于:它把一个完整业务从需求到落地的流程完整逼真地跑了一遍。
做兼职发布页的时候,一开始我只写了标题、描述、薪资这三个字段,后来同学试用的时候反馈说没有 “工作时间” 信息,兼职列表一多根本不知道谁是谁。于是又加了work_time字段,发布表单也跟着改。这种需求演进的过程,恰恰是真实的开发状态——需求和实现永远在互相迭代,所谓的“可维护”不是设计得完美,而是改起来顺。
还有权限设计,最开始我是什么接口都能访问,测试的时候自己登录自己账号随便点。后来补了IsAuthenticatedOrReadOnly才对发布、报名这类操作加了门槛。管理员审核兼职信息这个功能也是后期加的,但因为是基于状态字段设计的,加权限控制只花了不到半小时。
我个人在实际操作中的体会是,这类学校业务系统,难的不是某个技术难点,而是把一堆简单功能可靠地串联起来。Vue 负责交互动效和页面状态,Django 负责数据管理和业务规则,Flask 在角落里做定时的辅助任务,三者配合好了,整个系统就像一条流水线,数据从表单流向数据库,再流向列表和详情页,每一步都有据可查。
如果你也准备做类似的项目,我的建议是先把数据库表结构设计好,再决定接口,最后才设计页面。表结构错了,改起来牵一发动全身;页面丑一点,后面随时可以优化。这条路我自己走了一遍,少踩了很多弯路。