十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django+Vue前后端分离:大学生兼职管理系统开发实战

Django+Vue前后端分离:大学生兼职管理系统开发实战 最近帮一个高校就业指导中心做了套大学生兼职管理系统用的是 python django 做后端、vue 做前端管理端的方案。做之前对方没提太多要求只强调了一句学生端要轻、管理端要能管得住事。等真把系统搭起来发现这里面的门道比我预想的多——尤其是兼职发布、报名、签约这条链路每一步都牵扯到状态流转、权限控制和数据一致性。这篇文章就把这套系统的完整设计思路和落地方案掰开来讲包括数据库模型怎么建、DJANGO REST FRAMEWORK 的接口怎么设计、VUE 前端怎么对接、签约流程怎么保证不丢数据以及我在实际开发里踩过的坑。如果你正准备用 django vue 做类似的管理系统或者只是想把前后端分离这套流程跑通这篇应该能帮你省不少时间。1. 为什么是 Django Vue这套技术组合选型背后的考量先说说选型这件事。市面上做管理系统主流方案无外乎三种纯模板渲染、前后端分离、以及用现成的 admin 框架。我最后选了 django vue 前后端分离核心原因是这款系统同时要服务两类用户——普通学生和企业管理员这两类用户的使用场景差异很大。1.1 前后端分离与模板渲染的取舍纯 Django 模板渲染在开发初期的确快admin 后台开箱即用对于能跑就行的内部工具完全够用。但大学生兼职系统的痛点在于学生端需要频繁刷新兼职列表、查看签约状态、维护个人简历这些操作如果用模板渲染每点一次按钮都要整页刷新交互体验明显跟不上。而且模板渲染模式下前端逻辑和后端逻辑耦合在一个工程里后期改版维护都很痛苦。选 vue 做前端核心看中的是组件化和响应式数据流。兼职列表的筛选、排序、分页签约流程的状态流转简历编辑器的表单校验这些交互用 vue 写起来比 jQuery 时代清爽太多。Django 这边专心提供 REST API两边各管各的联调时只需要对齐接口协议。1.2 Django 提供的现成能力Django 在这一栈里承担的是地基的角色。本身带 ORM、Admin、认证体系、迁移工具不需要额外引一堆第三方库。尤其是 ORM兼职系统里学生、企业、兼职信息、签约记录、消息通知这些表的关系复杂用原生 SQL 写关联查询会累死Django ORM 的 select_related 和 prefetch_related 能把查询次数压下来性能上也有保证。另外Django 的 admin 后台直接可以用来做管理员的日常操作界面。这里多说一句网上很多人吐槽 Django admin 丑但它默认就带了用户管理、权限控制、增删改查做内部运营后台绰绰有余。我做的这个系统里学校就业中心的老师基本就是靠 admin 来管理兼职信息和审核企业入驻根本不需要额外开发管理端。至于界面美化那都是后话功能稳定才是第一位的。1.3 Vue 在交互体验上的加分项前端用 vue 还有一个直接原因现在的前端生态里vue 的路由、状态管理、UI 组件库已经非常成熟。做兼职管理系统这种中后台应用vue-router 做页面跳转、vuex 管理登录状态、Element UI 提供表格和表单组件几乎把重复工作全部封装好了开发效率非常可观。2. 数据库模型设计兼职系统最核心的实体关系梳理数据库模型是这套系统的根基。第一版设计里我按惯性思维把用户设计成一张大表用 user_type 字段区分学生、企业和管理员。跑通 demo 之后发现这种设计在业务逻辑层非常别扭——学生相关的字段学号、专业、简历、企业相关的字段公司名、营业执照、信用代码全挤在一张表里大量字段为空校验逻辑也要到处判断用户类型。2.1 用户角色的拆分设计踩过坑之后我重新梳理了需求将用户模型拆成两块Django 自带的 User 表只存通用账号信息用户名、密码、手机号、邮箱另外建 student_profile 和 company_profile 两张表分别存储学生和企业管理员的具体资料通过 OneToOneField 关联到 User。这样设计的直接好处是学生和企业资料不互相污染各自的字段可以做各自的校验Django 自带的权限框架group 和 permission直接复用不需要额外开发角色体系后续扩展新角色比如学校审核员只需要再建一张 profile 表不用动主表结构class StudentProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_namestudent_profile) student_id models.CharField(max_length20, uniqueTrue, verbose_name学号) major models.CharField(max_length50, verbose_name专业) grade models.CharField(max_length10, verbose_name年级) resume models.TextField(blankTrue, verbose_name个人简介)2.2 兼职信息模型的设计要点兼职信息表是整个系统的信息中心学生端浏览的是它企业端发布的是它。设计这张表时我特别关注了三个问题兼职类型的多样性、上下架状态的管理、以及展示信息的完整性。字段设计上除了 title、description、salary、location 这类基础字段我额外加了 job_type、status 两个关键字段。job_type 用来区分线上兼职和线下兼职不同兼职类型在地点要求、结算方式的逻辑上差别很大status 用整数做状态位1 表示招聘中、2 表示已下架、3 表示已招满这样企业端管理兼职时操作起来很直观。class Job(models.Model): company models.ForeignKey(CompanyProfile, on_deletemodels.CASCADE, related_namejobs) title models.CharField(max_length100, verbose_name职位名称) description models.TextField(verbose_name职位描述) salary models.DecimalField(max_digits10, decimal_places2, verbose_name薪资) job_type models.IntegerField(choicesJOB_TYPE_CHOICES, default1, verbose_name兼职类型) status models.IntegerField(choicesJOB_STATUS_CHOICES, default1, verbose_name状态) published_at models.DateTimeField(auto_now_addTrue, verbose_name发布时间)2.3 签约流程的数据结构设计签约这条链路是这套系统里业务逻辑最重的部分。学生的完整操作路径是浏览兼职信息 → 投递简历报名 → 企业查看简历并确认 → 学生确认 → 正式签约。这个流程看似简单但每一个步骤都有状态而且状态流转有方向性一旦发生错误操作比如重复投递、企业误操作确认要有对应的处理机制。我设计了 Application 表报名/申请记录和 Contract 表签约记录。Application 表记录的是投递行为本身包含投递时间、当前状态待查看、已查看、已通过、已拒绝Contract 表记录的是正式签约关系包含签约时间、状态待学生确认、已生效、已完成、已终止。class Application(models.Model): student models.ForeignKey(StudentProfile, on_deletemodels.CASCADE, related_nameapplications) job models.ForeignKey(Job, on_deletemodels.CASCADE, related_nameapplications) status models.IntegerField(choicesAPPLICATION_STATUS_CHOICES, default1, verbose_name投递状态) created_at models.DateTimeField(auto_now_addTrue, verbose_name投递时间)2.4 数据一致性设计上的一个关键决策设计签约流程时最纠结的是要不要把 Application 和 Contract 合并成一张表。很多系统为了省事把投递和签约状态放在一起用一个整数字段表示完整生命周期。我最终选择拆表原因是生命周期不同Application 是投递事件Contract 是签约关系。投递之后企业可以不看、可以拒绝但不产生合约签约之后可能中途终止也不影响投递记录的完整性。拆开之后两张表的逻辑各自简单清晰统计投递了多少次、签约了多少份也各自能独立聚合不需要复杂的条件筛选。3. 学生端 API 开发从 JWT 认证到兼职列表接口数据库模型定下来之后接下来是 API 层的开发。学生端需要的能力比较明确注册登录、浏览兼职列表、投递岗位、查看签约状态、维护个人简历。这里我重点讲三个部分JWT 认证方案的选型、兼职列表接口的查询优化、以及投递接口的幂等性处理。3.1 登录认证为什么选了 JWT 而不是 SessionDjango 自带的 Session 认证用起来很简单前后端分离模式下却有个天然的麻烦跨域请求时 Session Cookie 的处理很啰嗦而且如果以后要接小程序或移动端Session 的适配性也比较差。JWTJSON Web Token的方案是用户登录成功后后端签发一个 Token前端拿到之后存起来每次请求带在 Header 里。后端通过djangorestframework-simplejwt这个库验证 Token不需要服务端存 Session天然适合前后端分离的场景。实际配置的时候有几个细节值得注意# settings.py 中 JWT 相关配置 SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(days1), REFRESH_TOKEN_LIFETIME: timedelta(days7), AUTH_HEADER_TYPES: (Bearer,), ROTATE_REFRESH_TOKENS: True, }我设置了访问令牌有效期 1 天、刷新令牌 7 天。这个有效期需要根据自己的业务场景掂量太短了用户频繁重新登录体验差太长了安全风险高。大学生兼职系统属于中等敏感度应用1 天 7 天的组合在体验和安全之间比较平衡。3.2 兼职列表接口筛选、分页与查询优化学生端第一个核心接口就是兼职列表。初始版本的接口很简单直接Job.objects.all()然后返回但一上线就发现两个问题一是数据量大时接口响应特别慢二是学生想按类型、薪资范围筛选的时候接口完全不够用。优化之后的接口方案是通过 django-filter 库做筛选再配合 DRF 的 PageNumberPagination 做分页class JobFilter(django_filters.FilterSet): min_salary django_filters.NumberFilter(field_namesalary, lookup_exprgte) max_salary django_filters.NumberFilter(field_namesalary, lookup_exprlte) job_type django_filters.NumberFilter(field_namejob_type) keyword django_filters.CharFilter(field_nametitle, lookup_expricontains) class Meta: model Job fields [job_type, min_salary, max_salary, keyword]查询性能方面Job表关联了CompanyProfile如果不做处理每次序列化都要查询企业信息会产生 N1 问题。解决方法是视图集里重写get_queryset用select_related把关联表提前查出来class JobViewSet(viewsets.ModelViewSet): queryset Job.objects.select_related(company__user).filter(status1) serializer_class JobSerializer filter_backends (DjangoFilterBackend,) filterset_class JobFilter用上 select_related 之后兼职列表接口的数据查询从原来的几十次 SQL 降到了 1 次响应速度快了一个量级。这个小细节在数据量小的时候看不出来兼职信息一旦上了千条差别非常明显。3.3 投递接口的幂等性防止重复投递投递接口看起来就是一个 POST 请求往里插一条记录完事。但实际中遇到过一个很典型的场景学生网络不好点击投递按钮没反应又点了一下结果后台出现了两条投递记录。为了防止这种重复投递我在 Application 表的模型层加了 UniqueConstraintclass Meta: constraints [ models.UniqueConstraint( fields[student, job], nameunique_student_job ) ]同时在创建投递记录的视图里用get_or_create代替create配合事务处理。这样即使前端连续调用了两次第二次调用也不会重复插入而是返回第一次创建的记录接口从行为上保证了幂等性。这类用户无感知的重复请求在真实系统里非常常见处理起来成本极低但能避免大量脏数据。3.4 签约状态的查询与展示学生端我的签约页面需要展示所有签约记录及其状态。这里有一个需要处理的点签约记录中包含企业信息、职位信息、签约状态、结算周期等不同来源的数据前端一次接口调用希望能拿到完整的展示数据。我做了 ContractDetailSerializer把关联的企业和职位信息嵌套序列化class ContractSerializer(serializers.ModelSerializer): job_title serializers.CharField(sourceapplication.job.title, read_onlyTrue) company_name serializers.CharField(sourceapplication.job.company.company_name, read_onlyTrue) salary serializers.DecimalField(sourceapplication.job.salary, max_digits10, decimal_places2, read_onlyTrue) class Meta: model Contract fields [id, contract_no, start_date, end_date, status, job_title, company_name, salary]前端拿着这个接口的数据渲染列表页面就非常省事不需要前端自己去拼装多个接口的数据。设计 API 时的一个核心思路是按前端页面的数据需求来组织响应结构宁可一个接口多返回几个字段也不要让前端为了一个列表页面调三四个接口。4. 签约流程的状态机设计与数据一致性保障签约是整套系统里业务规则最多的地方。我把签约流程明确拆成投递 → 企业确认 → 学生确认 → 签约生效四个阶段每个阶段从数据层面就是一条状态记录。如果控制不好会出现学生还没确认、企业那边已经显示签约完成的错乱情况。4.1 状态流转的显式控制我将 Application 和 Contract 的状态流转都做成显式的方法而不是让前端直接传状态字段修改。比如学生确认签约的操作def confirm_contract(user, contract_id): try: contract Contract.objects.get(idcontract_id, studentuser.student_profile, status2) except Contract.DoesNotExist: raise ValidationError(签约记录不存在或当前状态不可确认) with transaction.atomic(): contract.status 3 contract.activated_at timezone.now() contract.save(update_fields[status, activated_at]) job contract.application.job job.status 3 # 兼职状态变为已招满 job.save(update_fields[status]) return contract这一段代码里有两层关键设计一是状态必须先查后改。确认操作的前提是当前状态为 2待学生确认查询条件里直接带上当前状态如果查不到就说明状态不对直接报错。这种方式比先取出对象再 if 判断再保存要安全得多能在数据库层避免并发情况下两个人同时操作导致的脏数据。二是用了事务原子性。签约生效时同步将兼职信息的状态改成已招满两个操作要么同时成功、要么同时失败避免了学生签约成功但职位仍然显示招聘中这种数据不一致。4.2 并发情况的兜底策略虽然状态流转已经在逻辑上做了顺序控制真正上线前我还是对学生报名同一岗位这个场景做了并发测试——开两个浏览器窗口同时报名同一个兼职结果后端没有出现重复记录这得益于第 3.3 节的 UniqueConstraint 兜底。UniqueConstraint 在整个系统里承担的是最后一道防线的角色即使视图层逻辑有 bug数据库层面也会拒绝重复数据不会污染数据根基。4.3 签约过期时间与企业审核超时还有一个产品层面的设计值得分享。在企业确认学生投递之后、学生确认之前如果学生一直不操作这条签约申请会一直挂着既占着职位名额又影响企业继续筛选其他学生。我加了一个待确认过期时间的概念企业确认投递后学生需要在 48 小时内确认超时自动视为放弃。实现方式不复杂定时任务每 10 分钟扫描一次过期记录把状态为待学生确认且创建时间超过 48 小时的 Contract 自动关闭。这样既保证了流程的严肃性也释放了职位名额。5. Vue 前端侧的状态管理与权限控制说完了后端再来看前端。Vue 这边的核心任务有两个管好用户的登录状态以及拦住不该访问的页面。这些工作主要落在 Vuex 和路由守卫身上。5.1 Vuex 管理用户状态前端把登录拿到的 Token 存到 localStorage同时把用户基本信息用户 ID、用户类型、用户名存一份到 Vuex。这样做的好处是刷新页面之后Vuex 里的状态会丢失但 localStorage 里还有可以在初始化的时候同步恢复。这里我特别想强调一个经验Token 和用户信息不要放在一起存Token 只需要传给后端用户信息是前端自己展示用的。存的时候分开两个 key读取和清除的时候各自独立避免某个操作误清 Token 导致用户状态丢失。// store/index.js state: { userInfo: {}, token: localStorage.getItem(token) || }, mutations: { setUserInfo(state, info) { state.userInfo info }, setToken(state, token) { state.token token localStorage.setItem(token, token) }, logout(state) { state.userInfo {} state.token localStorage.removeItem(token) window.location.href /login } }5.2 路由守卫控制页面权限兼职管理系统的用户有三种身份学生、企业管理员、系统管理员。学生端和企业端的功能差异明显如果前端不做权限控制学生直接访问企业端页面后端虽然会因为缺权限返回 403但用户体验非常差。我在 vue-router 的路由配置里给每个路由加了一个 meta 字段标记这个页面允许哪些角色访问{ path: /company, name: CompanyDashboard, component: CompanyDashboard, meta: { requiresAuth: true, roles: [company] } }然后在路由守卫里做统一判断router.beforeEach((to, from, next) { const token store.state.token if (to.meta.requiresAuth !token) { next(/login) return } const userRole store.state.userInfo.role if (to.meta.roles !to.meta.roles.includes(userRole)) { next(/403) return } next() })5.3 axios 拦截器处理 Token 和异常前端和后端的交互完全依赖 axios。我在 axios 封装里做了两件事第一请求拦截器统一把 Token 塞进 Authorization Header前端各个业务模块就不需要重复写认证逻辑。axios.interceptors.request.use(config { const token store.state.token if (token) { config.headers.Authorization Bearer ${token} } return config })第二响应拦截器统一处理后端返回的异常状态码。401 说明 Token 过期直接跳转登录页403 说明没有权限跳转无权限页面500 说明服务器异常弹出统一的提示。这样业务代码里只需要处理正常逻辑异常处理统一走拦截器代码量大幅减少。axios.interceptors.response.use( response response, error { if (error.response.status 401) { store.commit(logout) router.push(/login) } else if (error.response.status 403) { router.push(/403) } else { ElementUI.Message.error(服务器开小差了请稍后再试) } return Promise.reject(error) } )5.4 面试中经常被问到的 Vue 项目环境配置还有一个非常常见的问题vue 前端项目开发时怎么和后端联调。我用的是 Vite 的 proxy 配置在 vite.config.js 里把 /api 开头的请求全部代理到本地 Django 服务server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } }这个配置最直接的价值是解决了开发环境的跨域问题。如果不用代理前端直接请求 Django 接口会触发 CORS 拦截要么在后端装 django-cors-headers 放行要么通过代理转发。开发阶段用代理是最省事的不用在后端配一堆 CORS 白名单。6. 前后端联调中的实际问题与避坑记录最后分享几个真实开发中遇到并且解决掉的问题不少是文档上写得含糊、实际一跑就翻车的地方。6.1 Django Admin 后台的美化与实用配置系统上线后学校就业中心的老师每天都在用 Django Admin。他们反馈最多的是界面太朴素、操作入口不清晰。我先在 admin.py 里做了几个实用配置list_display 控制列表显示哪些字段、list_filter 增加状态筛选、search_fields 增加关键字搜索这些配置让后台从能用变成好用。class JobAdmin(admin.ModelAdmin): list_display [title, company, job_type, status, salary, published_at] list_filter [job_type, status] search_fields [title, company__company_name] list_per_page 20 admin.site.register(Job, JobAdmin)列表页支持按类型和状态筛选支持用关键字搜职位名称。之前老师要从几百条兼职里找特定一条得一条条翻现在一个关键字秒出结果。Django Admin 本身功能并不差只是默认配置比较朴素花一点时间做定制就能够满足大部分内部管理需求。6.2 图片上传与媒体文件路径问题兼职管理系统涉及企业 LOGO、岗位封面图的场景Django 处理上传文件有一套固定的流程settings.py 配好 MEDIA_URL 和 MEDIA_ROOT模型里用 ImageField 关联本地开发时再配置静态文件路由让图片能通过 URL 访问。MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media本地调试时需要在项目的 urls.py 里加一段路由from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)开发环境下图片能正常显示但部署上线时如果忘了把 media 目录里的文件做持久化会出现用户上传的图片重启服务后全丢的情况。我之前就吃过这个亏后来在部署方案里把 media 目录映射到独立的磁盘目录才彻底避免这个问题。6.3 Vue 打包后的静态资源路径问题前端开发完成之后需要执行npm run build把生成的静态文件交给服务器。第一次打包上线时我遇到了一个经典问题打包后的 index.html 加载 js/css 资源的路径是从根目录开始的如果部署时把前端文件放在了服务器的子目录下资源就全部加载不出来页面一片空白。解决方法是修改 vite.config.js 里的 base 配置export default defineConfig({ base: ./, // 其他配置 })改完之后打包生成的资源路径从绝对路径变成了相对路径放在任何子目录下都能正常加载。这个坑几乎每个 vue 项目都会遇到值得提前记住。6.4 查询性能问题当兼职数据量上来以后系统上线运行一个月后商家发布了几千条兼职信息学生端兼职列表页开始出现卡顿。我用 Django Debug Toolbar 检查发现列表页每次请求产生了超过 30 条 SQL除了之前优化过的 select_related还有职位收藏状态、报名状态这些额外查询。凡是需要展示当前学生是否已报名此职位的页面都是逐个查 Application 表导致的 N1。最终优化方案分两层列表接口的学生端视图里用annotate和Exists做条件聚合批量查出当前学生的报名状态不再逐条查询对于收藏状态用 Redis 做缓存学生切换收藏时更新缓存列表页直接读缓存。优化后列表页 SQL 数量降到了 3 条以内接口响应时间从原来的 800ms 降到了 100ms 左右。7. 系统部署与后续扩展的个人建议系统开发完成后很多朋友会问如果我要部署需要注意些什么。Django 的部署方式比较成熟nginx uwsgi 的组合可以一个项目跑到老。我目前用的是 Gunicorn 代替 uwsgi配置简单内存占用比 uwsgi 低不少。前端打包后交给 nginx 管理API 请求通过反向代理转发给 Gunicorn 进程。前后端分离模式下静态资源由 nginx 直接处理API 请求才进入后端这样分工让整个系统的请求链路非常清晰。如果后续要做扩展我会优先考虑这几件事消息通知模块目前用的是简单的邮件加站内信可以接微信公众号模板消息薪酬结算目前是线下处理可以做成线上结算流程学生评价体系目前缺失企业招聘学生后双方都没有互评入口。每一次系统演进都是在现有数据结构之上不断增加业务厚度。最后再多说一句经验之谈技术栈选型这件事django vue 不是万能的但它非常适合有明确数据模型、有管理后台需求、有一定交互复杂度的信息管理系统。这套组合上手快、生态成熟、团队的招聘成本低对一个高校兼职管理场景来说算是个很务实的方案。如果你正在评估类似的技术选型别被新框架的噱头带偏先把数据模型设计清楚把状态流转的逻辑想明白技术只是落地的载体。
返回列表