每年到了毕设季和课设末期,总有一批人会被同一个题目卡住:Python + Vue 的高校学生实习综合服务平台。这类项目在网上被翻来覆去地讨论,但真上手时,问题往往出在“知道大概要做什么,却不知道从哪个文件开始写”。这篇文章我就按自己实际做过的同类项目,把设计思路、技术选型、核心模块、数据库建模、前后端联调、常见坑位完整梳理一遍,python、django、flask、vue、pycharm 这些关键词都会落到具体的实现上。无论你是准备做毕设、课程设计,还是想完整练一次全栈开发,照着这条线走,能少走很多弯路。
先交代一下背景。高校学生实习综合服务平台,说白了要解决三件事:第一,学生能浏览实习岗位、投递简历、写实习日报;第二,企业能发布岗位、筛选简历、评估学生;第三,学校教师或辅导员能审核实习申请、查看学生动态、最终给出鉴定。这三条线串起来,就是一个典型的“多角色信息管理系统”。它既有用户权限,又有业务流程,还有文件上传、状态流转、统计报表,用来做毕设或课设非常合适,复杂度也刚好够展示水平。
1. 项目整体设计与技术选型
1.1 这个平台到底要解决什么问题
先别急着写代码,把业务边界划清楚比什么都重要。实习管理平台和普通的企业招聘网站最大的区别在于:它必须有“学校”这个角色参与全流程审核,而且要沉淀过程性材料。
以我做的版本为例,核心角色分成四类:学生、企业、教师、系统管理员。学生的操作路径是:完善个人信息 → 浏览岗位 → 投递简历 → 查看录取结果 → 填写日报 → 提交实习鉴定。企业的路径是:注册并完善公司资料 → 发布岗位 → 审核学生申请 → 为学生打分 → 出具实习评价。教师负责审核学生提交的实习申请、查看日报、确认鉴定材料。管理员负责维护基础数据,比如专业列表、班级信息、系统公告。
这里有个容易被忽略的需求:实习往往不是一次“投递简历就结束”的事情,而是从申请、确认、到岗、写日报、最终鉴定的一整条链路。所以业务设计上一定要有状态机,不能把“岗位发布”和“实习过程”混在一块。我当时的第一版设计就是只做了岗位和投递,结果做到一半发现“实习日报”这一块根本挂不上去,被迫重构了数据模型。建议你一开始就把这条链路设计出来,至少包含申请、录用、到岗、日报、鉴定五个节点。
1.2 Django还是Flask:我的选型逻辑
标题里同时出现了 django 和 flask,这确实是新手最容易纠结的地方。我需要把两者的差异说清楚,并且给出一个可以直接抄的结论。
Django 是“全家桶”路线:自带 ORM、Admin 后台、表单校验、认证系统、模板引擎。做这种多角色的管理系统,Django 的 Admin 后台几乎是白送的福利,可以快速给管理员做一个数据管理界面,省掉大量 CRUD 页面的开发时间。Flask 是“微框架”路线,只有路由和模板,数据库、登录、权限都要自己选组件搭。好处是灵活,坏处是“选择太多”对新手反而是负担。
单说这个实习平台项目,我更推荐 Django。原因有三个:一是用户认证这块,Django 自带的 AbstractUser 可以直接扩展成学生、企业、教师模型,省很多事;二是 ORM 的查询和迁移工具很成熟,建表、改字段都不用手写 SQL;三是 DRF(Django REST Framework)把序列化、视图、路由、分页都安排好了,后端接口开发效率非常高。
那 Flask 还有没有存在感?有。如果你只想做一个几十个接口的轻量 API,或者想展示自己对组件的掌控力,用 Flask + Flask-SQLAlchemy + Flask-JWT-Extended 也能做出来,代码量会比 Django 少一些,但权限和后台管理需要自己一点点补。另外提一句 FastAPI,它性能更好、自带接口文档,适合高并发场景,但生态和资料相对少一点,做课设反而不如 Django 稳。我的建议是:除非你的项目要求里明确写了 Flask,否则优先选 Django。
1.3 为什么是Vue + PyCharm这套组合
前端选 Vue 基本没有悬念。Vue 在国内社区活跃,中文资料多,组件库 Element Plus 写后台管理系统非常顺手,更重要的是 Vite 脚手架建项目快,对新手友好。很多网上的完整项目源码也都是 Vue 写的,遇到问题容易找到参考。
PyCharm 则是 Python 开发的第一梯队 IDE。专业版对 Django 有特殊照顾,比如模板语法高亮、manage.py 命令快捷入口、ORM 模型跳转,用起来确实舒服。如果你用的是社区版,也完全够用,区别只在一些 Django 专属的辅助功能。安装 PyCharm 之后,最重要的不是去折腾那些花哨的主题,而是把 Python 解释器和虚拟环境配置对。我的习惯是在项目根目录建一个 .venv,所有依赖都装在里面,避免和系统 Python 环境冲突。这一步弄明白了,后面 pip install 什么都很干净。
2. 核心模块拆解与数据库建模
2.1 用户角色与权限体系
这部分的第一个决定就是要不要用 Django 自带的 User 模型。我的做法是继承 AbstractUser,新增一个 user_type 字段,把学生、企业、教师、管理员四种身份放进同一个表里。这样做的好处是登录逻辑统一,认证体系不用做多套;坏处是字段会比较杂,学生需要学号、专业、班级,企业需要公司名称、信用代码、简介,教师需要工号和院系。
针对这些差异化字段,有两种处理思路:第一种是在 User 模型上加一个 JSON 字段存扩展信息,简单但不利于查询和校验;第二种是建独立的 Profile 表,比如 StudentProfile、CompanyProfile,通过 OneToOneField 关联到 User。我更推荐第二种,因为后续写查询时可以直接按 profile 里的字段筛选,比如“查找所有计算机专业的学生”,用关联查询就能完成,不用解析 JSON 字符串。
权限控制层面,DRF 里可以用自定义 Permission 类,也可以直接在视图里判断 request.user.user_type。我的建议是写几个基础权限类,比如 IsStudent、IsCompany、IsTeacher,然后在视图集中指定 permission_classes。这样做权限逻辑集中在代码开头,后面接业务功能时思路非常清楚。
2.2 实习业务的状态流转
状态机是整个项目里最考验设计能力的地方,也是答辩时最容易讲出彩的点。我把它分为两条线:一条是“岗位状态”,一条是“实习申请状态”。
岗位状态比较简单:草稿 → 发布中 → 已下线。企业只能编辑自己创建的岗位,学生只能看到发布中的岗位。
实习申请状态复杂一些,我最终定了六种:待审核、已通过、已拒绝、进行中、已完成、已终止。学生投递简历后是“待审核”;企业审核通过后变成“已通过”,此时可以约定到岗时间;学生确认到岗后状态为“进行中”;实习结束后企业提交评价,状态改为“已完成”;如果中途退出或企业取消了,则进入“已终止”。
为什么要单独设计状态而非用一个布尔字段?因为每一个状态都对应一组可执行的操作,比如“进行中”状态才能提交日报,“已完成”状态才能生成鉴定表。状态不清晰,权限判断就容易出现漏洞。另外,每个状态变更我都建议记录一次操作日志,哪怕只是简单的“谁在什么时间把申请从待审核改成了已通过”。这个日志在后期排查问题和答辩时都非常有用。
2.3 数据库表结构设计
下面是我实际用 Django models 实现的核心表结构,直接列出来供参考。
用户表在 2.1 里已经说了,继承 AbstractUser 后扩展 user_type、phone、avatar 字段。这里特别提醒一下,Django 的项目只要定义了自定义 User 模型,就必须在 settings.py 里设置 AUTH_USER_MODEL = "accounts.User",顺序错一步,迁移就会报错。
企业表 CompanyProfile 关联 User,包含 company_name、credit_code、industry、description、license 等字段。岗位表 Internship 关联 CompanyProfile,包含 title、description、location、salary_range、headcount、status、created_at。学生表 StudentProfile 关联 User,包含 student_no、major、grade、class_name。实习申请表 Application 关联 StudentProfile 和 Internship,包含 status、cover_letter、apply_time、approve_time。日报表 DailyReport 关联 Application,包含 content、work_date、created_at。鉴定表 Evaluation 关联 Application,包含 company_comment、score、teacher_comment、is_confirmed。
还有一个细节:如果企业需要上传营业执照,岗位需要展示公司 Logo,建议把文件上传的字段统一放到 resources 表或者用独立的 media 目录处理。Django 的 ImageField 和 FileField 能省很多事,但要注意配置 MEDIA_URL 和 MEDIA_ROOT,否则本地能跑,部署到服务器后图片全部 404。
2.4 设计阶段容易踩的三个坑
第一个坑是外键删除策略。学生如果注销账号,关联的申请记录怎么办?日报怎么办?我建议用 PROTECT 或者 SET_NULL,不要用默认的 CASCADE 到处级联删除。清空一条用户数据结果把整个实习记录全删掉,这种事故在演示时一旦发生,基本就崩了。
第二个坑是时间字段。实习这种业务天然需要展示时间轴,比如“已投递 3 天”“日报已提交 5 篇”。如果你只存一个 DateTimeField,后续统计会比较吃力。建议把创建时间、更新时间、审核时间都独立存字段,虽然冗余,但查询统计很直接。
第三个坑是数量统计。首页通常要展示“多少家企业发布了岗位”“多少学生在实习”,这些数据有两种做法:一是实时 count,数据少时没问题;二是单独做统计表,每次状态变更时更新计数器。我建议先做实时 count,等数据量真的上来了再优化,避免第一版就被统计逻辑拖住。
3. 后端实现:Django核心开发实录
3.1 PyCharm中从零搭建项目
这一节我用命令把环境搭起来,你跟着操作就行。先建虚拟环境并激活,然后安装依赖包。我列一份最小依赖清单:django、djangorestframework、django-cors-headers、djangorestframework-simplejwt、pillow、mysqlclient(如果用 MySQL)。
python -m venv venv source venv/bin/activate pip install django djangorestframework django-cors-headers djangorestframework-simplejwt pillow django-admin startproject internship_platform cd internship_platform python manage.py startapp accounts python manage.py startapp internships python manage.py startapp reports项目创建完成后,记得在 settings.py 里注册应用,一般的顺序是先写自研 app,再写第三方库:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', 'accounts', 'internships', 'reports', ]这里有个顺序问题:corsheaders 这个中间件要尽量放在 MIDDLEWARE 列表的前面,否则跨域请求的响应头可能加不上。我当时因为这个顺序问题排查了半天,浏览器控制台一直报 CORS 错误,最后才发现是中间件位置不对。
3.2 自定义用户模型与JWT登录
用户模型是登录认证的基础。我写的 User 模型长这样:
from django.contrib.auth.models import AbstractUser class User(AbstractUser): USER_TYPE_CHOICES = ( ('student', '学生'), ('company', '企业'), ('teacher', '教师'), ('admin', '管理员'), ) user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES, default='student') phone = models.CharField(max_length=20, blank=True) avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) class Meta: db_table = 'auth_user'然后去 settings.py 写一行关键配置:
AUTH_USER_MODEL = 'accounts.User'登录认证我选了 JWT,对应 Django 里最常用的 simplejwt 库。配置方式是在 settings.py 的 REST_FRAMEWORK 里声明认证类:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], }这两行配置的作用是:全局接口默认必须登录才能访问,认证方式从 Session 换成 JWT。完成之后,登录接口可以用 simplejwt 自带的 TokenObtainPairView,也可以自己写一个视图,在返回 access token 的同时带上用户信息。我比较推荐后者,因为前端一登录就要知道当前用户的角色和姓名,这个信息放在 token 解析里会很别扭,直接在登录接口返回给前端,体验好很多。
3.3 核心业务接口:报名、审核、日报
接口设计遵循一个原则:一个业务动作对应一个接口。下面是我整理的核心接口清单。
| 模块 | 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|---|
| 认证 | POST /api/auth/register | 注册 | 游客 | |
| 认证 | POST /api/auth/login | 登录 | 游客 | |
| 岗位 | GET /api/internships | 岗位列表 | 登录用户 | |
| 岗位 | POST /api/internships | 发布岗位 | 企业 | |
| 岗位 | POST /api/internships/{id}/apply | 投递申请 | 学生 | |
| 申请 | GET /api/applications | 我的申请 | 学生/企业 | |
| 申请 | POST /api/applications/{id}/approve | 通过申请 | 企业 | |
| 申请 | POST /api/applications/{id}/reject | 驳回申请 | 企业 | |
| 日报 | POST /api/applications/{id}/reports | 提交日报 | 学生 | |
| 日报 | GET /api/applications/{id}/reports | 查看日报 | 学生/企业/教师 | |
| 鉴定 | POST /api/applications/{id}/evaluate | 企业评分 | 企业 | |
| 鉴定 | POST /api/evaluations/{id}/confirm | 教师确认 | 教师 |
以“发布岗位”为例,视图写起来基本是标准 DRF 风格:
class InternshipViewSet(viewsets.ModelViewSet): queryset = Internship.objects.all() serializer_class = InternshipSerializer permission_classes = [IsAuthenticated, IsCompany] def get_queryset(self): user = self.request.user if user.user_type == 'company': return Internship.objects.filter(company__user=user) if user.user_type == 'student': return Internship.objects.filter(status='open') return Internship.objects.all() def perform_create(self, serializer): serializer.save(company=self.request.user.company_profile)这里的 get_queryset 用了角色判断,让企业只能看自己的岗位,学生只能看开放的岗位。还有一个细节:视图集默认会有 PUT / DELETE 方法,业务上只允许企业编辑和下线岗位,学生是不能改岗位的。我建议在视图集会显式指定 http_method_names,减少越权操作面。
3.4 高频操作:查询、删除的Django写法
“Django 执行查询-删除对象”是热搜里的高频词,很多新手都会卡在这。删除对象最简单的方式是:
obj = Application.objects.get(id=1) obj.delete()也可以用 queryset 批量删除:
Application.objects.filter(status='rejected').delete()但这里有一个性能方面的坑:Django 的 delete 是逐条删除而不是一条 SQL 搞定,处理外键时还会触发信号和收集被关联对象。数据量小时无所谓,如果一次删除几万条推荐数据,建议用 ORM 的 _raw_delete 或者直接执行 SQL,否则会非常慢。另外,业务上很多“删除”应该是“标记删除”,我建议在核心表上统一加 is_active 字段,删除时只置为 False,界面上过滤掉就行。这样哪怕误删也能一键恢复,答辩时也能讲“我们用了软删除保证数据安全”。
查询方面,最容易出问题的是 N+1 查询。比如查申请列表时迟早要关联学生和企业,如果序列化器里又写了嵌套字段,每条申请都会额外查一次数据库,页面一打开就慢了。解决办法是用 select_related 或 prefetch_related:
apps = Application.objects.select_related('student', 'internship').filter(status='pending')这个写法会一次性 JOIN 出学生和岗位信息,数据库请求次数从 N+1 降成 1。
做批量数据导入或初始化测试数据时,推荐 bulk_create,它能一次插入多条记录而不是一条条插:
Application.objects.bulk_create([ Application(student_id=1, internship_id=1, status='pending'), Application(student_id=2, internship_id=1, status='pending'), ])4. 前端Vue实现与前后端联调
4.1 Vue环境准备与依赖安装
Vue 的前端环境很简单,但在 PyCharm 里直接跑 Vue 项目之前,必须先把 Node.js 装好。建议选 LTS 版本,装了之后在命令行执行 node -v 确认生效。然后我用 Vite 脚手架创建项目:
npm create vite@latest frontend -- --template vue cd frontend npm install npm install vue-router@4 pinia axios element-plusVite 创建出来的项目结构非常清晰,src 下面自然分出了 components、views、router、stores 等目录,对于“vue 项目源码怎么发给别人”这个问题,只需要把整个项目文件夹压缩发给对方,对方执行 npm install 和 npm run dev 就能跑起来。不过要提醒一点:node_modules 目录不要发给别人,那是本地安装的依赖;我见过很多人把整个含 node_modules 的文件夹复制过去,不仅慢,还容易因为平台差异报错。
如果你下载的 Vue 模板项目跑起来后报 tsconfig 找不到的错,多半是项目里有没有用的 TypeScript 配置残留,或者依赖版本不对。解决方式是删除 tsconfig 相关引用,或者重新生成模板,不建议硬着头皮去逐行配 TS 环境。
4.2 路由与权限控制
Vue 的路由是页面跳转的核心。我的习惯是把路由分成两部分:一部分是公开页面(登录、注册、首页),另一部分是需要登录才能访问的业务页面。在 main.js 里注册路由和状态管理:
import { createApp } from 'vue' import App from './App.vue' import router from './router' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' const app = createApp(App) app.use(router) app.use(createPinia()) app.use(ElementPlus) app.mount('#app')路由守卫是实现登录校验的关键,这里用最基础的 beforeEach:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })这个做法的思路是:进入任何带 requiresAuth 标记的页面,都先查本地有没有 token,没有就跳登录。更复杂一点的动态菜单可以根据用户角色控制,比如企业用户才显示“岗位管理”,学生用户才显示“我的投递”。实现方式一般是从登录接口拿到的 user_type 存起来,然后在菜单组件里用 v-if 判断。这样做的目的是减少前端越权入口,和后端权限类形成双重防护。
4.3 复用组件实践:插槽与m3u8播放
Element Plus 是后台管理系统的利器,但它提供的组件不可能覆盖全部业务需求,所以组件复用能力很重要。Vue 插槽这里必须得会,因为表格操作列、卡片内容区、弹窗底部按钮这些场景都需要插槽来塞入自定义内容。
最简单的例子是表格操作列:
<el-table :data="applications"> <el-table-column label="状态" prop="status" /> <el-table-column label="操作"> <template #default="{ row }"> <el-button type="success" size="small" @click="approve(row)">通过</el-button> <el-button type="danger" size="small" @click="reject(row)">驳回</el-button> </template> </el-table-column> </el-table>这里 #default="{ row }" 是作用域插槽,它能把当前行的数据暴露给插槽内容,所有基于行数据的操作按钮都在这里写。
关于热词里反复出现的 m3u8 播放,我顺手把方案也说一下。m3u8 本质是视频分片索引文件,浏览器原生不支持直放,需要借助 hls.js。插件安装命令是 npm install hls.js,然后在对应组件里写:
import Hls from 'hls.js' function playM3u8(url, videoElement) { if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(url) hls.attachMedia(videoElement) } }这个场景在实习平台里可以用在企业宣传视频或者面试视频回放上,属于加分项,掌握之后面试时也能顺手提一句“我处理过 HLS 直播流和视频分片播放”。
4.4 axios封装与跨域配置
后端接口和前端页面要连通,第一件事是封装 axios。统一封装的好处是:请求头不用每个页面都写一遍 token,401 错误也能集中处理。
import axios from 'axios' import router from './router' const service = axios.create({ baseURL: '/api', timeout: 10000, }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) service.interceptors.response.use( res => res.data, err => { if (err.response && err.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(err) } ) export default service跨域这里我选了最省事的方案:后端直接启用 django-cors-headers,在 settings.py 里配置允许来源:
CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", ]前端开发服务器跑在 5173 端口,后端跑在 8000 端口,这个配置一下去,浏览器就不会再报跨域错误。如果你用 Vite 的 baseURL 写成 /api,还需要配合 Vite 开发环境做路径转发,否则请求会打到前端服务器上。为了减少复杂度,我建议前端统一写成 http://localhost:8000/api 这样的完整地址,或者在生产环境改成域名地址,这样调试时看得一清二楚,不会出现“路径到底转发到哪去了”的迷惑问题。
5. 常见问题与排查实录
5.1 环境安装类:Python版本、依赖装不上、PyCharm配置
第一个高频问题是 Python 版本。这个项目本身对 Python 版本要求不苛刻,3.8 到 3.10 都能跑。我建议你装 Python 3.8 或 3.10,版本太新反而容易遇到个别依赖还没适配的问题。安装完 Python 后再装 PyCharm,然后在 PyCharm 的 Settings → Project → Python Interpreter 里把解释器指向虚拟环境里的 python.exe,这是新手最容易漏的步骤。很多“为什么我 pip list 里明明有 django,但项目还是找不到模块”的报错,就是因为 PyCharm 用的解释器和命令行里激活的虚拟环境不是同一个。
依赖装不上的情况,八成是网络源的问题。解决办法是把 pip 源切换到国内源来加速下载:
pip install django -i https://pypi.tuna.tsinghua.edu.cn/simple补充一个热词场景:如果装 opencv-python 又总是卡住,同样可以先加 -i 参数指定国内源。注意,不要在一台机器上同时折腾多个 Python 版本,虚拟环境就是为了避免这种混乱而存在的。
5.2 Django后端常见报错
后端报错里最有代表性的有三个。第一个是迁移问题,改了模型之后忘记生成迁移文件,或者数据库里已经有旧数据导致迁移失败。我的习惯流程是:每次改完 models.py,立刻执行 makemigrations 和 migrate,别攒着到最后一次性处理,最容易收不了场。
第二个是class did not declare an explicit app_label报错。这个一般是因为 models.py 里写了多个类,但 Meta 里的 app_label 没写对,或者模型没有放在正确 app 的 models.py 里。解决办法是检查模型的 app_label 是否对应 INSTALLED_APPS 里注册的 app 名。
第三个是静态文件 404。Django 在开发环境下处理图片需要你在 urls.py 里加 static 配置:
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)如果不加这段,本地能跑通页面,但上传的图片和简历附件永远打不开。我知道很多人做到最后才暴露这个问题,建议做文件上传模块时就把这段配置写好。
5.3 Vue前端常见报错
Vue 项目跑不起来,最常见的几个原因按出现频率排:npm install 时依赖版本冲突、路由配置里的组件路径写错、跨域请求被浏览器拦截。
依赖冲突在下载网上的项目源码时很常见。解决办法是先删掉 node_modules 和 package-lock.json,再重新 npm install。这样能避免新旧 lock 文件中锁定的版本不一致导致的奇怪错误。
路由配置的问题一般是 import 路径大小写不对,Vue Router 对这种错误提示不算友好,经常只给一个白屏。排查方式是在浏览器开发者工具里看 console 输出,如果有 “Failed to resolve component” 或 “Cannot find module” 提示,检查 import 路径即可。
跨域我在 4.4 说过了,核心就是让后端允许前端来源,或者让前端把请求地址直接指到后端接口。只要不把 baseURL 写成相对路径又没做转发配置,一般不会出问题。
5.4 部署与上线:Flask/Django如何跑在服务器上
项目做完之后通常还要演示,演示就要涉及部署。如果只是本地演示,Django 自带 runserver 就够了;如果要在云服务器上跑,需要把 runserver 换成 gunicorn 这类生产级容器。
以 Django 为例,安装 gunicorn 是 pip install gunicorn,运行命令是:
gunicorn -w 4 -b 0.0.0.0:8000 internship_platform.wsgi:application端口需要开放到安全组里,然后通过配置 Nginx 把外部请求转发到 8000 端口,静态文件交给 Nginx 直接读。如果后端用的是 Flask,思路完全一样,gunicorn 后面指向的入口改成app:app就行。
PythonAnywhere 也是一个适合演示的选择,注册后把项目 git 拉上去,配好虚拟环境,直接给一个公网地址,适合不想买服务器的同学。不过它的免费额度对静态文件数量有限制,上传的图片多了会提示超限,演示前要清理测试数据。
结尾:几个实用的扩展方向
这个项目做完之后,如果你还想往上加亮点,我建议优先考虑这三个方向。第一,把实习数据做成可视化看板,统计各专业、各企业的实习情况,接入 ECharts 绘成柱状图、饼图,技术含量不高但展示效果很好。第二,给企业和学生加一个站内信系统,把审核结果、待办事项用通知中心推送给用户,这会显得业务更完整。第三,把日报和鉴定表导出成 PDF 或 Word,方便学生提交给学院,这一块会让项目在答辩时特别加分。
根据我个人的实操经验,这类全栈项目的最大难点从来不是某个框架的语法,而是“业务状态如何组织、前后端如何衔接”。遇到问题不要急着搜零散的报错片段,先把数据、接口、页面三层串起来看。只要你把本文里的模块划分、表结构、接口清单和联调思路吃透,这个题目就没有想象中那么难。最后再分享一个小技巧:开发时一定要定期用 git 提交,哪怕只是“日报列表接口完成”这种粒度的小提交,遇到改崩的情况回退也快,这是我能给你的最实在的建议。