:前后端分離架構(gòu)與狀態(tài)流轉(zhuǎn)設(shè)計)
簡介本資源為基于Python的貧困生資助管理系統(tǒng)畢業(yè)設(shè)計項目源碼與數(shù)據(jù)庫采用前后端分離架構(gòu)面向計算機相關(guān)專業(yè)正在做畢業(yè)設(shè)計的學生及需要項目實戰(zhàn)練習的學習者。項目經(jīng)導師指導并認可通過評審分98分源碼均經(jīng)本地編譯運行與嚴格調(diào)試難度適中可滿足學習與使用需求。壓縮包共755個文件約12.96MB涵蓋40個py后端源碼、38個vue前端組件、27個html頁面、51個css與162個js樣式腳本以及2個sql數(shù)據(jù)庫腳本、若干圖片與字體資源并附安裝、運行、構(gòu)建等bat腳本便于快速部署。目前已有108人學習下載。讀者可獲得完整可運行的前后端分離項目代碼、數(shù)據(jù)庫腳本與目錄結(jié)構(gòu)參考適合作為畢業(yè)設(shè)計選題模板或課程實戰(zhàn)案例幫助理解資助申請、審核、信息管理等模塊的實現(xiàn)思路與排錯方法。1. 貧困生資助管理系統(tǒng)從一張 Excel 臺賬到前后端分離的完整落地每年秋季學期某高校學工部門的老師都要面對同一件頭疼事幾千份家庭經(jīng)濟困難認定申請表堆在桌上Excel 臺賬改了又改版本從最終版一路排到最終版不再改可一到公示環(huán)節(jié)還是有人發(fā)現(xiàn)數(shù)據(jù)對不上。這不是誰不認真而是用表格管流程本身就撐不住申請—審核—公示—發(fā)放—歸檔這條鏈路?;?Python 的貧困生資助管理系統(tǒng)要解決的正是這個問題把認定流程、資助等級、發(fā)放記錄搬進一個帶權(quán)限、帶狀態(tài)流轉(zhuǎn)、帶審計痕跡的 Web 系統(tǒng)里。它適合正在做畢業(yè)設(shè)計、需要一套能跑通、能講清架構(gòu)、又能真實演示業(yè)務(wù)閉環(huán)的同學也適合想拿它當練手項目、補齊前后端分離經(jīng)驗的開發(fā)者。下面我按自己搭這類系統(tǒng)的順序把選型、建表、接口、聯(lián)調(diào)和踩坑一次講透。2. 技術(shù)選型與數(shù)據(jù)庫設(shè)計為什么這套組合最適合畢業(yè)設(shè)計2.1 后端選 Django 還是 Flask先看你要不要自帶后臺做資助管理系統(tǒng)繞不開一個現(xiàn)實你需要一個能管理用戶、角色、權(quán)限的后臺。Django 自帶 admin、ORM、認證體系開箱就能拿到用戶管理和權(quán)限控制Flask 更輕但用戶、角色、權(quán)限這套東西你得自己搭工作量會翻倍。我的建議是如果這是畢業(yè)設(shè)計選 Django Django REST FrameworkDRF把精力留給業(yè)務(wù)邏輯而不是造輪子。前端這邊Vue 3 Element Plus 是最穩(wěn)的組合。Element Plus 的表格、表單、分頁、彈窗組件齊全學工類系統(tǒng)大量是列表 表單 審批的界面用它開發(fā)效率高答辯演示也好看。前后端通過 JSON 交互前端跑在 5173 端口后端跑在 8000 端口用 CORS 打通。數(shù)據(jù)庫選 MySQL 8.0。資助系統(tǒng)涉及金額、等級、狀態(tài)需要事務(wù)保證一致性MySQL 的 InnoDB 引擎夠用而且部署資料多答辯時老師問起來也好解釋。2.2 核心表結(jié)構(gòu)五張表撐起整條資助鏈路資助系統(tǒng)的數(shù)據(jù)模型其實不復雜關(guān)鍵是別把學生申請審核發(fā)放揉在一張表里。我一般拆成五張核心表表名作用關(guān)鍵字段student學生基礎(chǔ)信息id、學號、姓名、院系、年級、班級application困難認定申請id、學生id、申請學年、困難等級、狀態(tài)、提交時間review_record審核記錄id、申請id、審核人id、審核結(jié)果、意見、審核時間grant_record資助發(fā)放記錄id、申請id、金額、發(fā)放時間、發(fā)放狀態(tài)sys_user系統(tǒng)用戶id、賬號、密碼、角色、關(guān)聯(lián)學生id這里有個設(shè)計要點application表里的狀態(tài)字段是整個系統(tǒng)的靈魂。它決定了這條申請現(xiàn)在處于哪一步常見取值是draft草稿、submitted已提交、reviewing審核中、approved已通過、rejected已駁回、granted已發(fā)放。狀態(tài)流轉(zhuǎn)必須由后端控制前端只能發(fā)起請求不能直接改狀態(tài)否則數(shù)據(jù)就亂了。-- 困難認定申請主表狀態(tài)字段是流程控制的核心 CREATE TABLE application ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 關(guān)聯(lián)學生id, academic_year VARCHAR(20) NOT NULL COMMENT 申請學年如2024-2025, difficulty_level TINYINT DEFAULT NULL COMMENT 困難等級1一般 2困難 3特別困難, status VARCHAR(20) NOT NULL DEFAULT draft COMMENT 流程狀態(tài), submit_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_student (student_id), INDEX idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表時把status和student_id都加上索引因為列表頁幾乎永遠在按狀態(tài)篩選、按學生查詢。difficulty_level允許為空是因為草稿階段學生還沒填完強制非空會導致保存草稿失敗——這個坑我后面還會提。2.3 用 Django ORM 把表映射成模型表建好后用 Django 的模型類對應(yīng)上后續(xù)所有查詢都走 ORM避免手寫 SQL 拼接帶來的注入風險。# models.py 核心模型定義 from django.db import models class Student(models.Model): student_no models.CharField(max_length20, uniqueTrue, verbose_name學號) name models.CharField(max_length30, verbose_name姓名) department models.CharField(max_length50, verbose_name院系) grade models.CharField(max_length10, verbose_name年級) class Meta: db_table student class Application(models.Model): STATUS_CHOICES [ (draft, 草稿), (submitted, 已提交), (reviewing, 審核中), (approved, 已通過), (rejected, 已駁回), (granted, 已發(fā)放), ] student models.ForeignKey(Student, on_deletemodels.CASCADE) academic_year models.CharField(max_length20) difficulty_level models.SmallIntegerField(nullTrue, blankTrue) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultdraft) submit_time models.DateTimeField(nullTrue, blankTrue) class Meta: db_table applicationon_deletemodels.CASCADE表示學生被刪除時其申請一并刪除這在測試階段方便清理數(shù)據(jù)但生產(chǎn)環(huán)境更穩(wěn)妥的做法是用PROTECT防止誤刪學生導致歷史資助記錄丟失。畢業(yè)設(shè)計里用 CASCADE 沒問題但答辯時如果被問到數(shù)據(jù)安全能說出這個區(qū)別是加分項。3. 后端接口開發(fā)把狀態(tài)流轉(zhuǎn)寫成不可繞過的規(guī)則3.1 用 DRF 寫第一個申請列表接口前后端分離的核心是接口。先寫一個查詢當前學生申請列表的接口把認證、序列化、分頁三件事串起來。# views.py 申請列表與創(chuàng)建接口 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from .models import Application from .serializers import ApplicationSerializer class ApplicationListView(APIView): permission_classes [IsAuthenticated] def get(self, request): # 普通學生只能看自己的申請管理員可看全部 if request.user.role student: qs Application.objects.filter(student__userrequest.user) else: qs Application.objects.all() status_filter request.query_params.get(status) if status_filter: qs qs.filter(statusstatus_filter) serializer ApplicationSerializer(qs.order_by(-id), manyTrue) return Response({code: 0, data: serializer.data})這段代碼有兩個關(guān)鍵點。第一權(quán)限判斷寫在視圖里根據(jù)request.user.role決定數(shù)據(jù)范圍學生永遠看不到別人的申請這是數(shù)據(jù)隔離的底線。第二status作為查詢參數(shù)做可選篩選前端切換標簽頁時只傳一個參數(shù)后端不用改代碼。order_by(-id)保證最新的申請排在最前符合使用習慣。3.2 狀態(tài)流轉(zhuǎn)接口誰能改、改成什么必須寫死資助系統(tǒng)最容易翻車的地方就是狀態(tài)被隨意修改。學生提交后自己又改回草稿或者審核員跳過審核中直接標已通過流程就亂了。解決辦法是把合法流轉(zhuǎn)寫成一個映射表任何修改都先校驗。# 狀態(tài)流轉(zhuǎn)規(guī)則當前狀態(tài) - 允許流轉(zhuǎn)到的狀態(tài) ALLOWED_TRANSITIONS { draft: [submitted], submitted: [reviewing, rejected], reviewing: [approved, rejected], approved: [granted], rejected: [draft], # 駁回后允許學生修改重新提交 granted: [], # 已發(fā)放為終態(tài) } class ApplicationStatusView(APIView): permission_classes [IsAuthenticated] def post(self, request, pk): app Application.objects.get(pkpk) target request.data.get(status) # 校驗目標狀態(tài)是否在允許范圍內(nèi) if target not in ALLOWED_TRANSITIONS.get(app.status, []): return Response({code: 4001, msg: 非法的狀態(tài)流轉(zhuǎn)}, status400) # 只有審核角色能操作審核類狀態(tài) if target in (reviewing, approved, rejected) and request.user.role ! reviewer: return Response({code: 4003, msg: 無審核權(quán)限}, status403) app.status target app.save() return Response({code: 0, msg: 操作成功})ALLOWED_TRANSITIONS這張表是整個流程的黑匣子所有合法路徑一目了然。granted對應(yīng)空列表意味著一旦發(fā)放就不能再改這是終態(tài)保護。角色校驗和狀態(tài)校驗分開寫是因為它們回答的是兩個不同問題這個狀態(tài)能不能到以及這個人有沒有資格操作。分開后報錯信息更精確前端也好提示。3.3 審核記錄每一次操作都要留痕狀態(tài)改了但誰改的、什么時候改的、什么意見必須單獨記一張表。這不是可選項資助系統(tǒng)涉及公平性沒有審核記錄公示階段被質(zhì)疑時你拿不出證據(jù)。# 審核時同步寫入審核記錄 class ReviewView(APIView): permission_classes [IsAuthenticated] def post(self, request, pk): app Application.objects.get(pkpk) result request.data.get(result) # pass 或 reject comment request.data.get(comment, ) target approved if result pass else rejected if target not in ALLOWED_TRANSITIONS.get(app.status, []): return Response({code: 4001, msg: 當前狀態(tài)不可審核}, status400) app.status target app.save() ReviewRecord.objects.create( applicationapp, reviewerrequest.user, resultresult, commentcomment ) return Response({code: 0, msg: 審核完成})審核和寫記錄放在同一個請求里用數(shù)據(jù)庫事務(wù)包起來更穩(wěn)Django 里加transaction.atomic裝飾器。如果審核成功但記錄寫入失敗數(shù)據(jù)就不一致了這種問題在答辯演示時一旦出現(xiàn)很難解釋。4. 前端聯(lián)調(diào)與權(quán)限控制讓界面跟著角色走4.1 用 axios 封裝統(tǒng)一請求把 token 帶上前端所有請求都走一個封裝好的 axios 實例避免每個頁面重復寫 baseURL 和請求頭。// request.js 統(tǒng)一請求封裝 import axios from axios const request axios.create({ baseURL: http://127.0.0.1:8000/api, timeout: 10000, }) // 請求攔截自動攜帶 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 響應(yīng)攔截統(tǒng)一處理錯誤碼 request.interceptors.response.use( res res.data, err { if (err.response err.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(err) } ) export default request攔截器做了兩件事請求發(fā)出前自動加 token響應(yīng)回來后統(tǒng)一處理 401。401 意味著登錄過期直接清 token 跳登錄頁比在每個頁面單獨判斷省事得多。timeout設(shè) 10 秒本地開發(fā)夠用如果后端查詢慢可以調(diào)大但別設(shè)成無限等待否則接口掛了頁面會一直轉(zhuǎn)圈。4.2 路由守衛(wèi)不同角色進不同頁面學生登錄后應(yīng)該看到我的申請審核員看到待審核列表管理員看到全部數(shù)據(jù)。這個分流在路由層面做比在頁面里判斷更干凈。// router/index.js 路由守衛(wèi) router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.requiresAuth !token) { next(/login) // 未登錄跳登錄頁 } else if (to.meta.roles !to.meta.roles.includes(role)) { next(/403) // 角色不符跳無權(quán)限頁 } else { next() } })路由的meta里聲明requiresAuth和roles守衛(wèi)統(tǒng)一判斷。這樣新增頁面時只要配好 meta不用改守衛(wèi)邏輯。注意前端路由守衛(wèi)只是體驗優(yōu)化真正的權(quán)限校驗必須在后端做前端能繞過后端不能省。4.3 申請表單草稿和提交走同一個接口學生填表時經(jīng)常填一半要保存所以表單要有保存草稿和提交兩個按鈕但后端可以用同一個創(chuàng)建/更新接口靠狀態(tài)字段區(qū)分。// 提交申請action 為 save 存草稿為 submit 直接提交 async function submitForm(action) { const payload { academic_year: form.academicYear, difficulty_level: form.level, status: action submit ? submitted : draft } const res await request.post(/applications/, payload) if (res.code 0) { ElMessage.success(action submit ? 提交成功 : 草稿已保存) } }這里status由前端傳但后端必須再校驗一次學生只能傳draft或submitted不能傳approved。前端傳什么不可信這是接口安全的基本功。5. 避坑與排查那些讓我返工三次的問題5.1 跨域報錯 CORS別只會加一個頭現(xiàn)象前端調(diào)接口瀏覽器控制臺報has been blocked by CORS policy請求根本沒到后端。原因前后端端口不同瀏覽器同源策略攔截。有人圖省事在響應(yīng)里加Access-Control-Allow-Origin: *但帶 token 的請求攜帶憑證時通配符不被允許。解決用django-cors-headers配置里寫明確的前端地址并開啟憑證支持。# settings.py 跨域配置 INSTALLED_APPS [corsheaders] MIDDLEWARE.insert(0, corsheaders.middleware.CorsMiddleware) CORS_ALLOW_CREDENTIALS True CORS_ALLOWED_ORIGINS [http://localhost:5173, http://127.0.0.1:5173]注意CorsMiddleware要盡量放在中間件靠前位置否則可能被其他中間件提前攔截。5.2 保存草稿失敗因為字段設(shè)了非空現(xiàn)象點保存草稿報 400提示difficulty_level不能為空。原因建表時把困難等級設(shè)成了NOT NULL但草稿階段學生還沒選等級。解決把difficulty_level改成允許為空模型里加nullTrue, blankTrue。凡是草稿階段可能沒填的字段都要允許為空等提交時再用業(yè)務(wù)校驗強制必填。5.3 列表分頁后篩選失效現(xiàn)象前端切到第二頁篩選條件丟了又回到全部數(shù)據(jù)。原因篩選參數(shù)只存在組件狀態(tài)里翻頁請求沒帶上。解決把篩選條件統(tǒng)一存到一個查詢對象里翻頁和篩選都從這個對象取值拼參數(shù)。別在翻頁時只傳page把status、academic_year一起帶上。5.4 金額字段用浮點數(shù)對賬差幾分錢現(xiàn)象發(fā)放金額累加后和臺賬差 0.01。原因數(shù)據(jù)庫用FLOAT存金額浮點運算有精度誤差。解決金額一律用DECIMAL(10,2)Django 里用DecimalField(max_digits10, decimal_places2)。涉及錢的地方浮點數(shù)就是后悔藥都買不回來的坑。5.5 刪除學生導致資助記錄消失現(xiàn)象測試時刪了一個學生歷史發(fā)放記錄跟著沒了。原因外鍵用了CASCADE。解決涉及歷史記錄的關(guān)聯(lián)改成PROTECT刪除前先檢查是否有關(guān)聯(lián)數(shù)據(jù)。測試環(huán)境圖方便用 CASCADE上線前一定要改回來。6. 進階技巧把系統(tǒng)從能跑做到敢演示到這一步系統(tǒng)基本能跑通了但畢業(yè)設(shè)計答辯和真實使用之間還差幾個細節(jié)。我一般會補三樣東西成本不高但演示效果和可信度提升明顯。第一樣是數(shù)據(jù)導出。學工老師最需要的功能之一就是把篩選后的名單導成 Excel。用openpyxl在后端生成文件前端用responseType: blob接收下載。# 導出資助名單為 Excel from openpyxl import Workbook from django.http import HttpResponse def export_grants(request): wb Workbook() ws wb.active ws.append([學號, 姓名, 困難等級, 資助金額, 發(fā)放時間]) qs GrantRecord.objects.select_related(application__student) for g in qs: s g.application.student ws.append([s.student_no, s.name, g.application.difficulty_level, float(g.amount), g.grant_time.strftime(%Y-%m-%d)]) resp HttpResponse( content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet ) resp[Content-Disposition] attachment; filenamegrants.xlsx wb.save(resp) return respselect_related一次性把關(guān)聯(lián)的學生信息查出來避免循環(huán)里逐條查庫導致 N1 問題。導出幾百條數(shù)據(jù)時這個優(yōu)化能讓響應(yīng)從幾秒降到幾百毫秒。第二樣是操作日志。除了審核記錄登錄、導出、刪除這類敏感操作也建議記一條日志字段就四個操作人、操作類型、目標對象、時間。答辯時老師問怎么保證數(shù)據(jù)可追溯這張表就是答案。第三樣是狀態(tài)機的前端可視化。在申請詳情頁用步驟條Element Plus 的el-steps把draft → submitted → reviewing → approved → granted畫出來當前狀態(tài)高亮。這個改動很小但演示時一眼就能讓評委看懂流程比口頭解釋強得多。最后說個我自己的習慣每次改完狀態(tài)流轉(zhuǎn)規(guī)則我都會手動把六種狀態(tài)兩兩組合跑一遍看哪些該攔、哪些該放。這個笨辦法幫我抓到過好幾次規(guī)則漏洞——比如駁回后重新提交這條路徑一開始忘了允許學生被駁回后就卡死了。系統(tǒng)能不能用往往不取決于功能多而取決于異常路徑有沒有堵死。希望幫到你。本文還有配套的精品資源點擊獲取