現(xiàn)大學(xué)生招聘系統(tǒng):設(shè)計(jì)、源碼與部署)
最近不少同學(xué)在找基于 Spring Boot 的畢業(yè)設(shè)計(jì)或者課程設(shè)計(jì)項(xiàng)目要求很一致源碼完整、能跑起來、功能不能太敷衍。我手里這套“大學(xué)生招聘系統(tǒng)”網(wǎng)上流傳的 32850 版本正好是這類項(xiàng)目里的一個(gè)典型。它不像那些只有登錄注冊(cè)和增刪改查的花架子從學(xué)生、企業(yè)到管理員三條用戶線的核心業(yè)務(wù)都閉環(huán)了技術(shù)棧也踩得很穩(wěn)Spring Boot 搭后端Vue 搭前端MySQL 存數(shù)據(jù)另外還整合了 JWT 認(rèn)證和 MinIO 文件存儲(chǔ)。這篇文章我就把它從設(shè)計(jì)到實(shí)現(xiàn)的思路、關(guān)鍵源碼以及我實(shí)際搭建過程中踩過的坑全盤拆給你。這套系統(tǒng)的域名和用途很明確面向高校場景下的招聘平臺(tái)。你可以把它當(dāng)成一個(gè)簡化版的“校招內(nèi)推網(wǎng)站”學(xué)生在前臺(tái)搜職位、傳簡歷、投遞、收面試邀請(qǐng)企業(yè)注冊(cè)后可以發(fā)布崗位、篩選簡歷、安排面試管理員在后臺(tái)做用戶和職位的審核以及看運(yùn)營數(shù)據(jù)。它解決了傳統(tǒng)線下校招信息不對(duì)稱、簡歷管理混亂、面試安排靠人工催的問題對(duì)學(xué)習(xí)者來說它是一個(gè)能完整串聯(lián)前后端、權(quán)限、消息流和文件存儲(chǔ)的絕佳練手項(xiàng)目。下面我按自己實(shí)際拆分這套源碼的順序一步步把內(nèi)容講透。1. 整體設(shè)計(jì)思路為什么這套系統(tǒng)能直接用于答辯和項(xiàng)目展示拿到一套源碼第一件事不是急著點(diǎn)運(yùn)行而是先把它的功能邊界和使用場景摸清楚。這套系統(tǒng)的核心場景是“校招”圍繞“學(xué)生找工作、企業(yè)招人”這一對(duì)供需關(guān)系展開并加入管理員作為平臺(tái)運(yùn)營方。這種三角色模型是招聘系統(tǒng)最經(jīng)典的結(jié)構(gòu)也是面試官最容易認(rèn)可的業(yè)務(wù)閉環(huán)。1.1 功能模塊到底有哪些從用戶端劃分系統(tǒng)大概可以拆成三個(gè)端加一個(gè)公共模塊。學(xué)生端C 端功能偏“投遞”注冊(cè)登錄、個(gè)人中心、簡歷管理基礎(chǔ)信息、教育經(jīng)歷、項(xiàng)目經(jīng)歷、技能標(biāo)簽、職位瀏覽、按關(guān)鍵詞和分類搜索職位、投遞簡歷、查看投遞狀態(tài)投遞中、已被查看、已發(fā)面試、不合適、收到企業(yè)面試邀請(qǐng)后在系統(tǒng)里確認(rèn)并查看面試安排。企業(yè)端B 端功能偏“篩選”注冊(cè)后提交營業(yè)執(zhí)照等資質(zhì)材料、登錄發(fā)布崗位、管理已發(fā)布崗位上下架、修改、查看投遞了本企業(yè)職位的學(xué)生列表、查看學(xué)生簡歷詳情、對(duì)簡歷進(jìn)行篩選操作通過/淘汰、向通過篩選的學(xué)生發(fā)送面試邀請(qǐng)。管理員端P 端功能偏“治理”對(duì)企業(yè)注冊(cè)進(jìn)行資質(zhì)審核、對(duì)職位進(jìn)行審核和違規(guī)下架、管理學(xué)生賬號(hào)禁用/解禁、查看平臺(tái)核心數(shù)據(jù)崗位總數(shù)、投遞量、注冊(cè)用戶量、熱門職位排行。公共模塊則包括人機(jī)驗(yàn)證、修改密碼、會(huì)話保持、操作日志等。這些功能共同構(gòu)成了一個(gè)可演示的完整閉環(huán)學(xué)生注冊(cè)→投遞→企業(yè)篩選→面試邀請(qǐng)→學(xué)生確認(rèn)面試。1.2 三條用戶線的權(quán)限邊界我在看這套代碼時(shí)最先關(guān)注的是它的權(quán)限是怎么做的。很多課設(shè)項(xiàng)目把權(quán)限寫在頁面上比如“不是管理員就不顯示按鈕”但真正的做法是要在接口層做控制。這套系統(tǒng)的做法是在后端用 Spring Security 攔截所有請(qǐng)求通過角色標(biāo)識(shí)判斷當(dāng)前登錄用戶是ROLE_STUDENT、ROLE_COMPANY還是ROLE_ADMIN然后在接口上通過PreAuthorize(hasRole(ADMIN))之類的注解做細(xì)粒度控制。比如學(xué)生能訪問/api/job/list但訪問不了/api/company/apply/list管理員能訪問/api/admin/statistics但普通學(xué)生訪問會(huì)返回 403。這種設(shè)計(jì)不僅保證了數(shù)據(jù)安全也讓代碼結(jié)構(gòu)更清晰。實(shí)際使用中業(yè)務(wù)層還要再校驗(yàn)一次數(shù)據(jù)歸屬比如企業(yè)只能操作自己創(chuàng)建的職位這個(gè)邏輯不能只依賴前端傳參。1.3 數(shù)據(jù)庫表設(shè)計(jì)的核心邏輯這套項(xiàng)目的數(shù)據(jù)庫一共十幾張表核心表我用一個(gè)表格列出來你就明白數(shù)據(jù)是怎么流轉(zhuǎn)的表名主要字段作用userid, username, password, role, phone, status統(tǒng)一用戶表用 role 區(qū)分學(xué)生/企業(yè)/管理員resumeid, user_id, name, school, major, education, skills學(xué)生簡歷一對(duì)一掛在用戶下companyid, user_id, company_name, license_url, audit_status企業(yè)資質(zhì)信息注冊(cè)時(shí)填寫待審核job_postid, company_id, title, category, city, salary_min, salary_max, description, status職位表包含薪資區(qū)間、城市、上下架狀態(tài)job_applicationid, job_id, resume_id, student_id, company_id, status, create_time投遞記錄狀態(tài)隨時(shí)間流轉(zhuǎn)interviewid, application_id, company_id, student_id, time, location, remark面試邀請(qǐng)關(guān)聯(lián)到具體的投遞記錄這種設(shè)計(jì)的巧妙之處在于用一個(gè)user表撐起了三種角色再用角色前置的輔助表存各自的擴(kuò)展信息。企業(yè)注冊(cè)后生成company記錄并處于待審核狀態(tài)學(xué)生注冊(cè)后自動(dòng)創(chuàng)建一個(gè)空resume這樣可以避免后面業(yè)務(wù)邏輯里各種查不到記錄的空指針問題屬于比較成熟的項(xiàng)目組織方式。2. 技術(shù)選型的門道Spring Boot Vue MinIO 這樣組合的理由這套源碼使用的技術(shù)組合很常見但每一個(gè)選型背后都有明確的理由我實(shí)際踩過一遍后對(duì)這些取舍更清楚了。2.1 后端Spring Boot 的自動(dòng)裝配和生態(tài)優(yōu)勢Spring Boot 能讓項(xiàng)目“開箱即用”核心是它的自動(dòng)配置機(jī)制。比如你在pom.xml里引入了spring-boot-starter-webBoot 會(huì)自動(dòng)檢查是否存在WebMvcConfigurationSupport、Servlet等類然后自動(dòng)創(chuàng)建需要的 Bean。這就是我在項(xiàng)目啟動(dòng)時(shí)幾乎不需要手動(dòng)寫DispatcherServlet配置的原因。對(duì)于學(xué)生來說這也意味著可以少寫大量 XML 和配置把精力放在業(yè)務(wù)邏輯上。這套系統(tǒng)用的版本是 Spring Boot 2.x配合 MyBatis-Plus 操作數(shù)據(jù)庫。MyBatis-Plus 簡直是為課設(shè)和中小型系統(tǒng)量身定做的BaseMapper里已經(jīng)封裝好了selectById、selectList、insert這些常用方法不需要自己寫 SQL分頁查詢用Page對(duì)象就能搞定。我曾在單表查詢非常多的地方直接用LambdaQueryWrapper來做條件拼接比如職位搜索時(shí)按關(guān)鍵詞、城市、薪資范圍過濾代碼比 XML 里的動(dòng)態(tài) SQL 還要好讀LambdaQueryWrapperJobPost wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), JobPost::getTitle, keyword) .eq(StringUtils.hasText(city), JobPost::getCity, city) .orderByDesc(JobPost::getCreateTime); return jobPostMapper.selectPage(new Page(pageNum, pageSize), wrapper);這種寫法的好處是類型安全字段名拼錯(cuò)了編譯階段就會(huì)暴露而不是等到運(yùn)行時(shí)報(bào)錯(cuò)。2.2 前端Vue 與后端靜態(tài)資源整合的坑系統(tǒng)前端是 Vue 寫的。我拿到的源碼里前端項(xiàng)目在一個(gè)frontend目錄下里面是標(biāo)準(zhǔn) Vue 2 Element UI axios vue-router vuex 結(jié)構(gòu)。開發(fā)環(huán)境上前后端通過 devServer 代理解決跨域生產(chǎn)環(huán)境上這套代碼采用了“把前端打包后塞進(jìn) Spring Boot 靜態(tài)目錄”的方式這也是熱詞里“vue打包放進(jìn)springboot”的典型訴求。為什么這么做因?yàn)檎n程設(shè)計(jì)或畢業(yè)設(shè)計(jì)的演示環(huán)境通常只有一臺(tái)服務(wù)器前端單獨(dú)部署還要配 Nginx麻煩且容易失敗。直接把 Vue 打包出來的dist目錄復(fù)制到src/main/resources/static下面Spring Boot 會(huì)自動(dòng)把它當(dāng)作靜態(tài)資源處理。也就是說除了/api/開頭的請(qǐng)求其他路徑都會(huì)返回 Vue 的頁面。這樣最終只需要打一個(gè) Jar 包就能同時(shí)提供頁面和數(shù)據(jù)接口部署起來省心很多。但這里有一個(gè)非常隱晦的坑Vue Router 默認(rèn)是history模式頁面路由是比如/student/course如果用戶直接刷新這個(gè)路徑Spring Boot 的靜態(tài)資源處理器會(huì)嘗試尋找student/course.html然后返回 404。解決方法就是在后端加一個(gè)控制器把非 API 的路徑都重定向到index.html。這套源碼里就是用Controller包了個(gè)ForwardControllerController public class ForwardController { GetMapping(value {/, /{path:[^\\.]*}, /{path:[^\\.]*}/{sub:[^\\.]*}}) public String forward() { return forward:/index.html; } }這個(gè)細(xì)節(jié)很多人一開始不知道我建議拿到源碼后先改掉默認(rèn)的 hash 路由用上面的方式處理否則部署完會(huì)彈出詭異的白屏。2.3 認(rèn)證與文件存儲(chǔ)JWT MinIO 的實(shí)戰(zhàn)選擇為什么選 JWT 而不用 Session因?yàn)橄到y(tǒng)的前端是 Vue而且未來可能會(huì)拆分成多個(gè)服務(wù)JWT 無狀態(tài)、跨域友好、能直接攜帶用戶信息。它的加密算法可以選對(duì)稱加密 HS256也可以用非對(duì)稱加密 RS256。這套系統(tǒng)用 HS256 更省事密鑰放在application.yml中登錄成功后生成一個(gè)包含userId和role的 token。同時(shí)它整合了 MinIO 來存圖片和 PDF 簡歷。MinIO 是一個(gè)對(duì)象存儲(chǔ)服務(wù)兼容 Amazon S3 API比起自建分布式文件系統(tǒng)簡單得多。熱詞中“minio加入到springboot”也是常見問題。這套系統(tǒng)在pom.xml里引入minio客戶端依賴配置好 endpoint、accessKey、secretKey 和 bucket然后封裝一個(gè)MinioServicepublic String uploadFile(MultipartFile file) { String fileName System.currentTimeMillis() _ file.getOriginalFilename(); minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .contentType(file.getContentType()) .stream(file.getInputStream(), file.getSize(), -1) .build()); return endpoint / bucketName / fileName; }用 MinIO 存簡歷 PDF 和公司資質(zhì)圖片比存 BLOB 字段更合理。數(shù)據(jù)庫維護(hù)文件的 URL 就行避免了數(shù)據(jù)庫膨脹也讓靜態(tài)資源可以走獨(dú)立域名。2.4 項(xiàng)目目錄結(jié)構(gòu)說明拿到源碼后建議先看目錄結(jié)構(gòu)。這是這套系統(tǒng)的包組織方式src/main/java/com/recruit ├── config/ # Security、MinIO、CORS、WebMvc 配置 ├── controller/ # 接口層按功能拆 Admin/Student/Company ├── service/ # 業(yè)務(wù)邏輯層接口 impl ├── mapper/ # MyBatis-Plus 持久層接口 ├── entity/ # 數(shù)據(jù)庫實(shí)體類 ├── dto/ # 接收參數(shù)的類 ├── vo/ # 返回給前端的數(shù)據(jù)對(duì)象 ├── common/ # 統(tǒng)一返回結(jié)果、異常處理、常量 ├── utils/ # JWT 工具類等我見過太多源碼所有類都堆在com.example下找起來非常痛苦。這套系統(tǒng)的包名雖然沒有特別規(guī)范但至少按功能拆得很清楚Controller 對(duì)應(yīng)的 Service、Mapper 可以直接跳轉(zhuǎn)這對(duì)學(xué)習(xí)和后續(xù)二次開發(fā)都很友好。3. 關(guān)鍵功能實(shí)現(xiàn)源碼級(jí)別的拆解整個(gè)系統(tǒng)的核心不在頁面布局而在幾個(gè)關(guān)鍵鏈路。我把其中最值得閱讀和修改的四個(gè)部分拎出來講。3.1 注冊(cè)與登錄Spring Security JWT 的集成要點(diǎn)登錄是所有系統(tǒng)的入口但很多人的登錄邏輯是當(dāng)成擺設(shè)。這套系統(tǒng)比較好的地方是它把 Spring Security 的UsernamePasswordAuthenticationToken和自定義 JWT 工具類結(jié)合了起來。具體實(shí)現(xiàn)上登錄接口先校驗(yàn)用戶名密碼成功后就生成 token。生成 token 的代碼大致是// utils/JwtUtil.java public String generateToken(User user) { return Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(role, user.getRole()) .claim(username, user.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireTime)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }然后寫一個(gè)JwtAuthenticationFilter繼承 Spring Security 的OncePerRequestFilter在每次請(qǐng)求進(jìn)來時(shí)取出 Authorization 頭解析 token把用戶權(quán)限寫入SecurityContextHolder。我在查看代碼時(shí)特別注意到它自己寫了一個(gè)getAuthentication方法定義了一些SimpleGrantedAuthority權(quán)限。集成時(shí)最容易出問題的點(diǎn)是 Security 放行規(guī)則。一定要記住/api/auth/login/api/auth/register/api/captcha這些公共接口不能攔截否則登錄都過不去。另外 Swagger 如果接入的話也要放行swagger-resources、v3/api-docs等路徑。這套系統(tǒng)在這塊處理得比較清楚你可以沿用它的規(guī)則。3.2 學(xué)生投遞簡歷的核心流程投遞是招聘系統(tǒng)的“下單”動(dòng)作所以我特別關(guān)注它的完整事務(wù)。學(xué)生點(diǎn)“投遞”按鈕后前端發(fā)送一個(gè)請(qǐng)求包含職位ID和簡歷ID。后端邏輯大致如下Transactional public Result apply(Integer studentId, Integer jobId, Integer resumeId) { JobPost job jobPostMapper.selectById(jobId); if (job null || job.getStatus() ! 1) { return Result.error(職位不存在或已下架); } // 防止重復(fù)投遞 Long count applicationMapper.selectCount( new LambdaQueryWrapperJobApplication() .eq(JobApplication::getJobId, jobId) .eq(JobApplication::getStudentId, studentId)); if (count 0) { return Result.error(請(qǐng)勿重復(fù)投遞); } JobApplication application new JobApplication(); application.setJobId(jobId); application.setResumeId(resumeId); application.setStudentId(studentId); application.setCompanyId(job.getCompanyId()); application.setStatus(0); // 0投遞中 applicationMapper.insert(application); return Result.success(投遞成功); }注意這里我用Transactional來保證事務(wù)因?yàn)檫@里讀后寫且存在重復(fù)投遞的并發(fā)風(fēng)險(xiǎn)。雖然實(shí)際中課設(shè)項(xiàng)目并發(fā)量不高但寫代碼時(shí)要把這個(gè)意識(shí)帶上。對(duì)于學(xué)生端來說之后查看投遞記錄的接口要把職位信息和面試信息聯(lián)表查出來不能只返回一個(gè)狀態(tài)數(shù)字。3.3 企業(yè)發(fā)布職位與簡歷篩選企業(yè)側(cè)的邏輯主要集中在CompanyController中。發(fā)布職位時(shí)公司不是必填字段而是從當(dāng)前登錄用戶的user記錄去關(guān)聯(lián)company表然后取companyName。我在源碼里看到它用SecurityUtils.getCurrentUserId()來獲取當(dāng)前登錄者的 ID再用 ID 去查關(guān)聯(lián)公司。這是一個(gè)很值得借鑒的小技巧避免前端手工傳 user_id 或 company_id減少越權(quán)風(fēng)險(xiǎn)。簡歷篩選的流程是企業(yè)查看某個(gè)職位下的所有投遞列表列表里要拼接學(xué)生的名字、學(xué)校、專業(yè)等點(diǎn)擊“查看簡歷”時(shí)調(diào)用/api/company/resume/{id}Controller 返回的是一個(gè)ResumeVO包含遠(yuǎn)程學(xué)歷信息和文件 URL。企業(yè)點(diǎn)擊“通過”后后端將job_application.status改為 2并自動(dòng)創(chuàng)建一條interview記錄此時(shí)學(xué)生端就能看到“面試邀請(qǐng)”狀態(tài)。PostMapping(/interview) public Result createInterview(RequestBody InterviewDTO dto) { JobApplication application applicationMapper.selectById(dto.getApplicationId()); application.setStatus(2); applicationMapper.updateById(application); Interview interview new Interview(); interview.setApplicationId(dto.getApplicationId()); interview.setCompanyId(application.getCompanyId()); interview.setStudentId(application.getStudentId()); interview.setTime(dto.getTime()); interview.setLocation(dto.getLocation()); interview.setRemark(dto.getRemark()); interviewMapper.insert(interview); return Result.success(); }這里其實(shí)可以做得更好比如發(fā)送站內(nèi)信或郵件通知但作為課設(shè)項(xiàng)目目前夠用。3.4 管理員數(shù)據(jù)看板的實(shí)現(xiàn)思路管理員的首頁一般是幾個(gè)統(tǒng)計(jì)卡片和圖表。這套系統(tǒng)的統(tǒng)計(jì)接口用了簡單的聚合查詢例如統(tǒng)計(jì)總注冊(cè)學(xué)生數(shù)、總企業(yè)數(shù)、總職位數(shù)、總投遞數(shù)。對(duì)于熱門職位排行它用了 MyBatis-Plus 的groupByQueryWrapperJobApplication wrapper new QueryWrapper(); wrapper.select(job_id, count(*) as cnt) .groupBy(job_id) .orderByDesc(cnt) .last(limit 10);然后把結(jié)果顯示成柱狀圖數(shù)據(jù)交給前端。雖然談不上大數(shù)據(jù)但管理員的日常運(yùn)營看板就是這種模式調(diào)試起來很直觀。你也可以在此基礎(chǔ)上增加時(shí)間范圍的篩選條件來統(tǒng)計(jì)某個(gè)月份的投遞趨勢。4. 從源碼到上線完整構(gòu)建、配置與避坑實(shí)錄很多人在本地能跑起來一到換個(gè)環(huán)境就歇菜了。我按照從零開始構(gòu)建這套系統(tǒng)的順序把配置和部署過程中的步驟和坑逐一記錄下來。4.1 環(huán)境準(zhǔn)備與基礎(chǔ)配置你需要準(zhǔn)備好這些JDK 1.8 或更高版本、Maven 3.6、MySQL 5.7、MinIO 服務(wù)、Node.js 14用于前端構(gòu)建。建議版本不要拉太高Spring Boot 2.x 在 JDK 17 下也能跑但一些老依賴可能存在問題熱詞里“springboot版本太高”就是一個(gè)常見坑如果你下載的依賴版本互相沖突優(yōu)先鎖定 Boot 版本為 2.7.x這是穩(wěn)定很多的安全牌。數(shù)據(jù)庫配置在application.yml里重點(diǎn)關(guān)注下面幾項(xiàng)server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/recruit?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket: recruit-file jwt: secret: your-secret-key-please-change-me expire-time: 86400000這些配置文件里最容易錯(cuò)的是時(shí)區(qū)和 URL。serverTimezoneAsia/Shanghai不設(shè)置的話可能會(huì)出現(xiàn) 8 小時(shí)的時(shí)間偏差。文件大小限制也要調(diào)大否則上傳簡歷 PDF 會(huì)直接報(bào)文件超限。4.2 前端打包如何塞進(jìn) Spring Boot前端項(xiàng)目在frontend文件夾里首先要把它的代理配置填好。開發(fā)階段vue.config.js里設(shè)proxy: /api指向http://localhost:8080用來解決本地前后端聯(lián)調(diào)的跨域問題。生產(chǎn)構(gòu)建時(shí)你需要先清理掉代理配置因?yàn)榍岸?static 目錄里的資源默認(rèn)同源。打包命令也很簡單cd frontend npm install npm run build構(gòu)建完成后dist目錄下會(huì)生成index.html和一堆 JS/CSS 文件。把這整個(gè)dist里的內(nèi)容復(fù)制到后端的src/main/resources/static目錄下。注意是復(fù)制內(nèi)容不是把dist文件夾復(fù)制進(jìn)去。復(fù)制完之后后端項(xiàng)目重新打包。前端里的接口請(qǐng)求地址必須用相對(duì)路徑例如/api/student/job/list不能寫死http://localhost:8080/...。我在拿到源碼后發(fā)現(xiàn)它寫了一個(gè)統(tǒng)一的request.jsbaseURL 設(shè)為空字符串這是合理設(shè)計(jì)。如果原來的源碼中有絕對(duì)地址你要先改成相對(duì)路徑否則上線必掛。4.3 后端部署Maven 插件、數(shù)據(jù)庫初始化數(shù)據(jù)庫的初始化腳本一般在sql目錄下比如recruit.sql。你需要用 Navicat 或者命令行導(dǎo)入。如果 SQL 文件里有測試數(shù)據(jù)直接跑一遍即可。我建議你把管理員賬號(hào)、測試學(xué)生、測試企業(yè)的密碼都問清楚或者自己先用后端的注冊(cè)頁注冊(cè)一個(gè)學(xué)生賬號(hào)再手動(dòng)把它的role改成ADMIN這樣至少能保證進(jìn)后臺(tái)。后端打包時(shí)Spring Boot 默認(rèn)打包會(huì)執(zhí)行測試如果項(xiàng)目里有測試類但環(huán)境不完整可能會(huì)失敗。我通常會(huì)跳過測試mvn clean package -DskipTests打完的target/*.jar就是可執(zhí)行 jar。啟動(dòng)時(shí)指定配置文件java -jar recruit-system.jar --spring.profiles.activeprod如果你沒有太多服務(wù)器經(jīng)驗(yàn)也可以用 spring-boot-maven-plugin 的repackage目標(biāo)把依賴都打進(jìn)去確保 jar 能獨(dú)立運(yùn)行。4.4 常見問題排查速查表我根據(jù)自己跑這類項(xiàng)目的實(shí)際經(jīng)歷把最常見的 6 個(gè)問題整理成了一張表當(dāng)你搭建時(shí)遇到卡住可以直接對(duì)照排查癥狀可能原因解決辦法啟動(dòng)后前端頁面能打開接口 404前端靜態(tài)資源和后端 API 路徑?jīng)_突或者 ForwardController 沒有包含某條路徑檢查index.html里的資源路徑是否正確重點(diǎn)看 js/css 引用的是相對(duì)還是絕對(duì)提交登錄接口報(bào) 403Security 配置里沒有放行登錄路徑或者 CSRF 未禁用在 SecurityConfig 里.csrf().disable()并把/api/auth/**加入 permitAll數(shù)據(jù)庫中文亂碼JDBC URL 缺少characterEncodingutf8或者表編碼不是 utf8mb4修改連接串并執(zhí)行ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4;文件上傳失敗MinIO 桶未創(chuàng)建或密鑰錯(cuò)誤手動(dòng)用 browser 登錄 MinIO 控制臺(tái)創(chuàng)建指定的 bucket并檢查 access-key/secret-key刷新頁面變成白屏Vue Router history 模式?jīng)]有后端兜底加入 ForwardController 轉(zhuǎn)發(fā)所有非/api路徑到index.html接口總是 401 token 失效JWT 密鑰未統(tǒng)一或者系統(tǒng)時(shí)間不一致檢查前后端和服務(wù)器的系統(tǒng)時(shí)間JWT 過期時(shí)間不要設(shè)太短這些坑每一個(gè)我都遇到過一次以上。尤其是端口沖突問題在 IDE 里啟動(dòng)多個(gè)實(shí)例時(shí)經(jīng)常被占用可以在application.yml里改端口也可以在 IDEA 啟動(dòng)配置的 VM 參數(shù)里寫-Dserver.port8081。很多同學(xué)改完前端代碼重新打包發(fā)現(xiàn)頁面還是舊的原因是用瀏覽器緩存強(qiáng)烈建議部署時(shí)讓 Nginx 或后端靜態(tài)資源響應(yīng)頭加上Cache-Control: no-cache否則調(diào)試到懷疑人生。最后再分享一個(gè)能立刻提高演示好感度的小技巧——IDEA 里配置啟動(dòng)參數(shù)。如果你要在不同端口上開多套環(huán)境驗(yàn)證權(quán)限不用反復(fù)改 yml 文件而是點(diǎn)擊右上角啟動(dòng)配置在 Program arguments 里填寫--server.port9090Spring Boot 的命令行參數(shù)優(yōu)先級(jí)高于配置文件這個(gè)參數(shù)能直接覆蓋 yml 里的端口。這是我在實(shí)際開發(fā)中經(jīng)常用的一招尤其當(dāng)你同時(shí)跑后端服務(wù)和前端 devServer 的時(shí)候能少踩不少端口沖突的坑。把這套系統(tǒng)跑通弄明白每個(gè)模塊為什么這樣設(shè)計(jì)你不僅能完成一個(gè)像樣的課設(shè)還能在面試?yán)锪某鲆粋€(gè)有業(yè)務(wù)深度、有權(quán)限控制、有文件存儲(chǔ)的完整項(xiàng)目。