算機(jī)學(xué)習(xí)網(wǎng)站開(kāi)發(fā)全流程:從需求分析到上線部署實(shí)戰(zhàn))
每年這時(shí)候都有不少朋友來(lái)問(wèn)畢設(shè)選題尤其是計(jì)算機(jī)科學(xué)與技術(shù)方向十個(gè)里有六七個(gè)想做學(xué)習(xí)類(lèi)網(wǎng)站。這個(gè)方向確實(shí)討巧需求清晰、技術(shù)棧通用、演示效果直觀但正因?yàn)樽龅娜硕喾炊菀鬃龀梢欢压δ芏哑龅恼n程列表頁(yè)。我前段時(shí)間剛完成一個(gè)編號(hào)為m248的項(xiàng)目——計(jì)算機(jī)科學(xué)與技術(shù)學(xué)習(xí)網(wǎng)站的設(shè)計(jì)與開(kāi)發(fā)從需求分析到部署上線完整走了一遍有些經(jīng)驗(yàn)值得拿出來(lái)聊聊。這個(gè)網(wǎng)站的核心定位不是做一個(gè)放視頻的課件站而是把課程檢索、學(xué)習(xí)路線、在線練習(xí)、個(gè)人筆記幾個(gè)環(huán)節(jié)串起來(lái)讓一個(gè)剛接觸計(jì)算機(jī)的新手或者想系統(tǒng)補(bǔ)基礎(chǔ)的同學(xué)能沿著一條明確路徑往前走。整個(gè)開(kāi)發(fā)周期大約六周技術(shù)棧選了Spring Boot加Vue的前后端分離方案數(shù)據(jù)庫(kù)用的MySQL另外用Redis做緩存和會(huì)話管理。這篇文章會(huì)把設(shè)計(jì)思路、模塊實(shí)現(xiàn)、踩過(guò)的坑、以及上線后的運(yùn)營(yíng)細(xì)節(jié)都攤開(kāi)講適合正在做類(lèi)似畢設(shè)、或者想自己搭一個(gè)學(xué)習(xí)站點(diǎn)的同學(xué)參考。1. 這個(gè)學(xué)習(xí)網(wǎng)站到底解決什么問(wèn)題從用戶痛點(diǎn)倒推需求動(dòng)手寫(xiě)第一行代碼之前最該想清楚的是網(wǎng)站給誰(shuí)用、解決什么具體問(wèn)題。我見(jiàn)過(guò)太多同類(lèi)項(xiàng)目一上來(lái)就堆課程分類(lèi)、視頻播放、論壇討論最后界面長(zhǎng)得像低配版慕課網(wǎng)但用戶根本不知道從哪里開(kāi)始學(xué)。做學(xué)習(xí)網(wǎng)站最核心的價(jià)值不是提供資源而是提供路徑。1.1 目標(biāo)用戶畫(huà)像與核心痛點(diǎn)我做需求分析時(shí)把目標(biāo)用戶分成三類(lèi)各自的痛點(diǎn)非常不一樣計(jì)算機(jī)專(zhuān)業(yè)低年級(jí)本科生大一、大二剛接觸編程學(xué)校課程按學(xué)期推進(jìn)但很多人在數(shù)據(jù)結(jié)構(gòu)、操作系統(tǒng)這類(lèi)核心課上跟不上需要課外補(bǔ)充學(xué)習(xí)資料。他們的痛點(diǎn)是資料太多、不知道按什么順序看。轉(zhuǎn)碼自學(xué)者非科班出身想通過(guò)自學(xué)進(jìn)入開(kāi)發(fā)行業(yè)。這類(lèi)人往往不缺學(xué)習(xí)資源和教程缺的是系統(tǒng)性的路線圖和能檢驗(yàn)學(xué)習(xí)效果的練習(xí)題。課程設(shè)計(jì)/期末復(fù)習(xí)人群需要快速查找特定知識(shí)點(diǎn)比如二叉樹(shù)遍歷HTTP狀態(tài)碼希望有精準(zhǔn)的搜索結(jié)果和精簡(jiǎn)的總結(jié)。三類(lèi)人群交集明顯都需要結(jié)構(gòu)化內(nèi)容 可驗(yàn)證的學(xué)習(xí)成果。所以我把網(wǎng)站定位為學(xué)習(xí)路徑 內(nèi)容檢索 在線練習(xí)三位一體的工具型站點(diǎn)而不是又一個(gè)視頻點(diǎn)播平臺(tái)。1.2 功能需求收斂砍掉哪些、留住哪些初期我列了十多個(gè)功能點(diǎn)包括視頻彈幕、論壇、積分商城、在線聊天后來(lái)全部砍掉。保留下來(lái)并且最終做出來(lái)的只有五個(gè)模塊模塊解決的問(wèn)題關(guān)鍵功能課程中心知識(shí)獲取課程分類(lèi)、關(guān)鍵詞檢索、章節(jié)內(nèi)容瀏覽學(xué)習(xí)路線學(xué)習(xí)路徑按階段展示路線節(jié)點(diǎn)、節(jié)點(diǎn)進(jìn)度標(biāo)記在線評(píng)測(cè)學(xué)習(xí)效果驗(yàn)證精選練習(xí)題目、代碼提交、自動(dòng)判題學(xué)習(xí)筆記知識(shí)沉淀個(gè)人筆記寫(xiě)、改、刪、按課程歸類(lèi)后臺(tái)管理內(nèi)容維護(hù)課程/題目/路線圖的增刪改查、用戶管理砍功能的原則很簡(jiǎn)單每一項(xiàng)功能必須能回答用戶使用它的頻率有多高和沒(méi)有它會(huì)不會(huì)影響核心體驗(yàn)。視頻彈幕這種功能做出來(lái)熱鬧但和學(xué)習(xí)效率這個(gè)核心價(jià)值沒(méi)有強(qiáng)關(guān)聯(lián)反而是在線評(píng)測(cè)這種看起來(lái)不顯眼的功能對(duì)學(xué)習(xí)效果的提升是實(shí)打?qū)嵉?。事?shí)證明最后演示的時(shí)候評(píng)委最感興趣的也是自動(dòng)判題這個(gè)模塊。2. 技術(shù)選型的取舍邏輯一個(gè)人開(kāi)發(fā)也要考慮未來(lái)有人維護(hù)技術(shù)選型是這類(lèi)項(xiàng)目最容易糾結(jié)的環(huán)節(jié)。選太熱門(mén)的怕?tīng)€大街選太冷門(mén)的怕出問(wèn)題沒(méi)人幫忙。我的結(jié)論是在成熟生態(tài)里挑一套自己最熟悉的組合比追求技術(shù)新穎更重要。2.1 前后端方案對(duì)比與最終選擇主選方案是Spring Boot 3 Vue 3 MySQL 8備選方案是Python Flask/Django Vue SQLite/MySQL。兩者的取舍我做了個(gè)簡(jiǎn)單對(duì)比對(duì)比維度Java Spring Boot方案Python方案上手成本中高需要理解Bean、依賴注入等概念低Flask幾十行能跑起服務(wù)生態(tài)成熟度高企業(yè)級(jí)應(yīng)用廣泛遇到問(wèn)題容易搜到答案高但偏腳本場(chǎng)景重型業(yè)務(wù)少代碼執(zhí)行/判題支持多語(yǔ)言編譯執(zhí)行方便javac/gcc等進(jìn)程管理成熟執(zhí)行子進(jìn)程也方便但并發(fā)和資源隔離要小心答辯友好度高評(píng)審普遍認(rèn)可Spring全家桶中視評(píng)審老師偏好而定部署成本稍高要裝JDK、打jar包低pip依賴裝完就能跑最終選了Spring Boot主要是因?yàn)楫呍O(shè)場(chǎng)景下可預(yù)測(cè)性優(yōu)先。Java的生態(tài)太龐大了遇到前端跨域、Redis連接、文件上傳這類(lèi)問(wèn)題網(wǎng)上隨便一搜就是完整的解決方案不太會(huì)在環(huán)境層面卡住。另外在線評(píng)測(cè)模塊需要調(diào)用外部編譯器比如javac、gcc、pythonJava在管理子進(jìn)程、限制資源占用比如超時(shí)、內(nèi)存上限方面的做法更成熟這一點(diǎn)在后面的開(kāi)發(fā)中確實(shí)省了很多事。2.2 關(guān)鍵依賴組件清單除了主框架我還選了幾個(gè)配套組件這里列一下選型和理由Redis 6.x做登錄會(huì)話緩存、課程點(diǎn)擊量緩存。邏輯上Session原來(lái)可以放服務(wù)器內(nèi)存但考慮到將來(lái)可能部署到多實(shí)例用Redis屬于現(xiàn)在麻煩一點(diǎn)、以后少改架構(gòu)的決策。MySQL 8 Nginx數(shù)據(jù)持久化和靜態(tài)資源/CDN入口。MySQL的全文索引ngram parser在搜索功能里幫了大忙這個(gè)后面細(xì)說(shuō)。MyBatis-Plus數(shù)據(jù)訪問(wèn)層。比純MyBatis少寫(xiě)很多CRUD樣板代碼尤其在后臺(tái)管理的增刪改查上效率提升明顯。前端用Vite構(gòu)建 Element Plus組件庫(kù)Element Plus的后臺(tái)表格、表單、樹(shù)形控件都是現(xiàn)成的能保住開(kāi)發(fā)進(jìn)度。提示選型階段一定要寫(xiě)一個(gè)簡(jiǎn)單的技術(shù)選型說(shuō)明文檔把每個(gè)組件用來(lái)解決什么問(wèn)題寫(xiě)清楚。答辯的時(shí)候老師幾乎必問(wèn)為什么用Redis為什么不用MongoDB提前準(zhǔn)備好理由比到時(shí)候現(xiàn)想強(qiáng)得多。3. 核心功能模塊設(shè)計(jì)與實(shí)現(xiàn)順序先做骨架再填血肉功能實(shí)現(xiàn)順序我建議按用戶主路徑來(lái)排先讓他能瀏覽課程內(nèi)容再給他路線圖和練習(xí)最后才是個(gè)人筆記和后臺(tái)管理。這樣每一步做完都能跑起來(lái)看到效果不會(huì)出現(xiàn)開(kāi)發(fā)兩個(gè)月、聯(lián)調(diào)才崩的窘境。3.1 數(shù)據(jù)庫(kù)設(shè)計(jì)五張核心表一張路線表數(shù)據(jù)庫(kù)設(shè)計(jì)是這類(lèi)項(xiàng)目的地基。我建的核心表如下字段設(shè)計(jì)考慮了擴(kuò)展性表名核心字段作用usersid, username, password_hash, role用戶認(rèn)證與權(quán)限區(qū)分categoriesid, name, parent_id, sort_order課程分類(lèi)樹(shù)coursesid, category_id, title, summary, cover_url課程基本信息course_chaptersid, course_id, title, content_html, sort_order章節(jié)內(nèi)容problemsid, title, description, input_desc, output_desc, difficulty練習(xí)題目描述learning_path_nodesid, title, description, order_index, link_type, link_id學(xué)習(xí)路線節(jié)點(diǎn)judge_recordsid, user_id, problem_id, code, language, status, execute_time提交與判題記錄notesid, user_id, course_id, title, content_markdown個(gè)人學(xué)習(xí)筆記這里想特別說(shuō)一下learning_path_nodes這張表的設(shè)計(jì)。學(xué)習(xí)路線本質(zhì)上是一個(gè)有向無(wú)環(huán)圖節(jié)點(diǎn)之間有依賴關(guān)系比如學(xué)完數(shù)據(jù)結(jié)構(gòu)再學(xué)算法。我最初想用鄰接表存節(jié)點(diǎn)關(guān)系后來(lái)發(fā)現(xiàn)對(duì)學(xué)習(xí)路線來(lái)說(shuō)線性序列 階段分組就夠用了。每個(gè)節(jié)點(diǎn)存一個(gè)order_index前端按這個(gè)排序渲染時(shí)間線后臺(tái)調(diào)整順序只需要改這個(gè)字段。真要做復(fù)雜的課程依賴判斷比如必須先修完課程A才能解鎖課程B再用單獨(dú)的依賴表也不遲前期不做過(guò)度設(shè)計(jì)。3.2 課程中心搜索是門(mén)面分類(lèi)樹(shù)是骨架課程中心是整個(gè)網(wǎng)站訪問(wèn)量最大的頁(yè)面實(shí)現(xiàn)上分兩部分分類(lèi)瀏覽和關(guān)鍵詞搜索。分類(lèi)瀏覽用兩級(jí)分類(lèi)就夠了一級(jí)是編程語(yǔ)言數(shù)據(jù)結(jié)構(gòu)操作系統(tǒng)計(jì)算機(jī)網(wǎng)絡(luò)數(shù)據(jù)庫(kù)這樣的大類(lèi)二級(jí)是具體課程或?qū)n}。前端渲染分類(lèi)樹(shù)用遞歸組件數(shù)據(jù)一次從后端取完不搞懶加載減少請(qǐng)求次數(shù)。搜索是重點(diǎn)也是坑點(diǎn)。剛開(kāi)始我用的是 MySQL 的LIKE %關(guān)鍵詞%課程少的時(shí)候沒(méi)問(wèn)題但數(shù)據(jù)量上來(lái)之后有兩個(gè)問(wèn)題一是性能差不能走索引全表掃描二是匹配不聰明搜二分查找匹配不到標(biāo)題為Binary Search 算法詳解的課程。后來(lái)改成 MySQL 全文索引配合 ngram 解析器ALTER TABLE courses ADD FULLTEXT INDEX ft_course_title (title) WITH PARSER ngram;查詢時(shí)用MATCH(title) AGAINST(二分查找 IN NATURAL LANGUAGE MODE)中英文混合的內(nèi)容也能正確匹配。課程數(shù)量級(jí)在幾萬(wàn)條以內(nèi)MySQL全文索引完全夠用沒(méi)必要上Elasticsearch運(yùn)維成本高不少。課程詳情頁(yè)除了展示章節(jié)內(nèi)容我還加了一個(gè)相關(guān)課程推薦的邏輯按同一分類(lèi)下點(diǎn)擊量排序取前五條。這個(gè)功能雖然簡(jiǎn)單但對(duì)用戶體驗(yàn)的提升很明顯讓用戶在看完一節(jié)課之后有地方可以去而不是關(guān)掉頁(yè)面。3.3 在線評(píng)測(cè)最費(fèi)心也是最有亮點(diǎn)的一塊在線評(píng)測(cè)模塊是整個(gè)網(wǎng)站技術(shù)含量最高的部分也是答辯時(shí)老師問(wèn)得最多的地方。核心流程是用戶提交代碼 → 后端調(diào)用相應(yīng)語(yǔ)言的編譯器/解釋器 → 在受控環(huán)境中運(yùn)行 → 比對(duì)輸出 → 返回結(jié)果。整個(gè)流程我簡(jiǎn)化成四個(gè)環(huán)節(jié)第一代碼執(zhí)行。后端接收入?yún)⒑蟀汛a寫(xiě)入臨時(shí)文件調(diào)用javac、gcc、python等命令編譯執(zhí)行。判題用例放在受限目錄運(yùn)行時(shí)把用戶程序的輸出重定向到臨時(shí)文件然后和答案文件比對(duì)。第二資源限制。這是必須做的否則一個(gè)惡意死循環(huán)就能把服務(wù)器拖垮。我用了兩層防護(hù)外層在調(diào)用層設(shè)置超時(shí)一般單用例 3 秒內(nèi)層用操作系統(tǒng)的資源限制功能限制進(jìn)程 CPU 時(shí)間和內(nèi)存subprocess.run( [command, code_file, input_file], capture_outputTrue, timeout10, preexec_fnlimit_resources )Java端也有對(duì)應(yīng)的方案用ProcessBuilder起子進(jìn)程配合destroyForcibly()做超時(shí)兜底。第三安全隔離。說(shuō)實(shí)話讓用戶提交任意代碼在服務(wù)器上執(zhí)行還是有風(fēng)險(xiǎn)的。演示環(huán)境里我用 Docker 容器做隔離每個(gè)判題請(qǐng)求起一個(gè)臨時(shí)容器跑完直接銷(xiāo)毀。雖然資源開(kāi)銷(xiāo)大但安全性能保證。如果實(shí)在不想引入Docker至少要做到禁止用戶提交Shell腳本、限制可執(zhí)行文件路徑白名單、以低權(quán)限用戶運(yùn)行判題進(jìn)程。第四結(jié)果反饋。判題結(jié)果不只是通過(guò)/不通過(guò)我會(huì)給用戶展示編譯錯(cuò)誤信息、運(yùn)行超時(shí)提示、第一個(gè)不匹配的用例的期望輸出和實(shí)際輸出。這一步對(duì)學(xué)習(xí)體驗(yàn)很重要——用戶看到具體的失敗原因才知道怎么改代碼。3.4 學(xué)習(xí)路線圖用JSON配置而非硬編碼學(xué)習(xí)路線圖我最初想在前端寫(xiě)死節(jié)點(diǎn)后來(lái)發(fā)現(xiàn)每次調(diào)整路線都要改前端代碼、重新打包發(fā)布太麻煩了。改成運(yùn)行時(shí)可配置的方案路線圖數(shù)據(jù)存數(shù)據(jù)庫(kù)前端從接口拉取按階段和時(shí)間線渲染。簡(jiǎn)化版的路線JSON結(jié)構(gòu)類(lèi)似{ 階段1: [編程語(yǔ)言入門(mén), 計(jì)算機(jī)基礎(chǔ)], 階段2: [數(shù)據(jù)結(jié)構(gòu), 算法設(shè)計(jì)], 階段3: [操作系統(tǒng), 計(jì)算機(jī)網(wǎng)絡(luò), 數(shù)據(jù)庫(kù)原理], 階段4: [實(shí)踐項(xiàng)目, 專(zhuān)項(xiàng)進(jìn)階] }前端對(duì)每個(gè)節(jié)點(diǎn)渲染卡片用戶點(diǎn)擊某個(gè)節(jié)點(diǎn)可以跳轉(zhuǎn)到對(duì)應(yīng)的課程或練習(xí)。學(xué)習(xí)進(jìn)度標(biāo)記就存user_id node_id is_finished邏輯簡(jiǎn)單但很實(shí)用。線路圖的數(shù)據(jù)內(nèi)容我花了不少心思。參考了幾所高校的教學(xué)計(jì)劃把CS核心課程按照依賴關(guān)系排了一遍高等數(shù)學(xué)、離散數(shù)學(xué)作為前置基礎(chǔ)然后才是數(shù)據(jù)結(jié)構(gòu)、計(jì)算機(jī)組成原理再往后是操作系統(tǒng)、編譯原理、計(jì)算機(jī)網(wǎng)絡(luò)。這個(gè)依賴關(guān)系雖然不是絕對(duì)正確但對(duì)新手來(lái)說(shuō)是一個(gè)非常友好的引導(dǎo)。4. 踩坑實(shí)錄開(kāi)發(fā)途中真正浪費(fèi)時(shí)間的五個(gè)問(wèn)題這個(gè)項(xiàng)目前后踩了不少坑挑五個(gè)最典型的寫(xiě)出來(lái)每一個(gè)都是在搜索引擎上繞了很久才搞定的希望后來(lái)的同學(xué)少走彎路。4.1 富文本編輯器的XSS注入問(wèn)題學(xué)習(xí)筆記模塊一開(kāi)始我用的是富文本編輯器用戶可以輸入任意HTML。結(jié)果測(cè)試時(shí)發(fā)現(xiàn)一個(gè)大問(wèn)題用戶可以在筆記里插入script標(biāo)簽或者惡意事件屬性比如img srcx onerroralert(1)。如果不做處理別人瀏覽到這篇筆記時(shí)就會(huì)執(zhí)行這段腳本典型的存儲(chǔ)型XSS漏洞。解決思路是前端渲染前過(guò)濾 后端入庫(kù)前直接拒絕危險(xiǎn)內(nèi)容。后端用Jsoup框架做白名單過(guò)濾只允許正常的p、a、img、code、pre等標(biāo)簽所有on*屬性和script標(biāo)簽一律清除。筆記內(nèi)容從Markdown源文本保存渲染成HTML時(shí)才過(guò)濾雙保險(xiǎn)。4.2 課程搜索搜不到和搜不準(zhǔn)并存沒(méi)上全文索引之前搜索有兩個(gè)毛病。搜動(dòng)態(tài)規(guī)劃能出結(jié)果但搜dp就匹配不到任何內(nèi)容因?yàn)闃?biāo)題里寫(xiě)的是中文全稱(chēng)反過(guò)來(lái)搜Java會(huì)把JavaScript的課程也搜出來(lái)因?yàn)長(zhǎng)IKE %Java%子串匹配的粒度太粗了。后來(lái)用了ngram全文索引情況好了很多但還有一個(gè)細(xì)節(jié)要處理全文索引默認(rèn)按相關(guān)性排序相關(guān)性評(píng)分受詞頻影響課程標(biāo)題字段短、關(guān)鍵詞密度高評(píng)分反而合理。另外我在搜索接口上加了一個(gè)簡(jiǎn)單的中文分詞步驟把用戶輸入的查詢短語(yǔ)按常規(guī)分隔符空格、逗號(hào)拆開(kāi)再拼接成MATCH ... AGAINST的布爾模式SELECT * FROM courses WHERE MATCH(title) AGAINST(Java 并發(fā) IN BOOLEAN MODE)這樣Java 并發(fā)可以被精確匹配而不是被當(dāng)成一個(gè)完整長(zhǎng)詞匹配效果肉眼可見(jiàn)地變好。4.3 Vue Router History模式部署后刷新404前端用的History路由模式本地開(kāi)發(fā)一切正常打包部署到Nginx之后訪問(wèn)首頁(yè)沒(méi)問(wèn)題但在/courses/12這樣的詳情頁(yè)按F5刷新直接404。原因很簡(jiǎn)單Nginx默認(rèn)在找不到對(duì)應(yīng)文件時(shí)才走不到前端路由因?yàn)樯a(chǎn)環(huán)境沒(méi)有/courses/12這個(gè)真實(shí)路徑。解決辦法是在Nginx配置里加一個(gè)回退邏輯location / { try_files $uri $uri/ /index.html; }所有非靜態(tài)文件的請(qǐng)求都回退到index.html由前端路由接管。這個(gè)坑我記得特別清楚因?yàn)榕挪闀r(shí)一度懷疑是Vue Router版本問(wèn)題調(diào)了很久最后發(fā)現(xiàn)就是一行Nginx配置的事。4.4 在線評(píng)測(cè)的隱藏扣分項(xiàng)運(yùn)行時(shí)錯(cuò)誤信息不完整判題模塊在測(cè)試時(shí)發(fā)現(xiàn)用戶代碼如果出現(xiàn)數(shù)組越界、空指針、除以零這類(lèi)異常只能看到一個(gè)籠統(tǒng)的運(yùn)行時(shí)錯(cuò)誤具體哪一行出錯(cuò)、錯(cuò)誤信息是什么完全看不到。對(duì)學(xué)習(xí)者來(lái)說(shuō)這種反饋基本等于沒(méi)有。后來(lái)改成了捕獲并記錄異常堆棧在Java判題場(chǎng)景里用戶代碼的異常信息會(huì)打印到標(biāo)準(zhǔn)錯(cuò)誤輸出流判題進(jìn)程把stderr的內(nèi)容捕獲下來(lái)如果判題結(jié)果是RERuntime Error就把堆棧的前幾行返回給前端展示。注意這里要做好信息截?cái)嘀伙@示異常類(lèi)型和行號(hào)不暴露服務(wù)器內(nèi)部路徑和敏感信息。4.5 跨域問(wèn)題差點(diǎn)讓前端聯(lián)調(diào)崩掉前后端分離開(kāi)發(fā)時(shí)前端跑在localhost:5173后端跑在localhost:8080必然會(huì)遇到跨域問(wèn)題。一開(kāi)始我在后端加了CrossOrigin注解能解決一部分GET請(qǐng)求但帶自定義Header的POST請(qǐng)求依然報(bào)跨域錯(cuò)誤。正確的做法是在后端寫(xiě)一個(gè)全局的CORS配置類(lèi)允許指定的前端來(lái)源、請(qǐng)求方法、請(qǐng)求頭和攜帶憑證registry.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true);這里有一個(gè)小坑allowedOrigins(*)和allowCredentials(true)不能同時(shí)使用瀏覽器會(huì)直接拒絕必須寫(xiě)明確的前端域名。聯(lián)調(diào)階段這個(gè)配置幫我省了一整天的排查時(shí)間。5. 數(shù)據(jù)從哪來(lái)種子內(nèi)容準(zhǔn)備與內(nèi)容安全底線網(wǎng)站功能寫(xiě)完只是第一步更費(fèi)時(shí)間的是讓網(wǎng)站看起來(lái)有內(nèi)容。一個(gè)空蕩蕩的學(xué)習(xí)網(wǎng)站再好的功能也沒(méi)人愿意用。我給自己定了一個(gè)目標(biāo)上線時(shí)至少要有一套完整的課程內(nèi)容和100道可判題的練習(xí)。5.1 種子數(shù)據(jù)的來(lái)源與版權(quán)處理課程內(nèi)容這塊我的做法是引用公開(kāi)資料 整理原創(chuàng)講義而不是直接搬運(yùn)別人的課程視頻或文檔。具體來(lái)源包括公開(kāi)課程大綱參考國(guó)內(nèi)外知名高校公開(kāi)的課程頁(yè)面吸收他們的知識(shí)點(diǎn)組織方式自己重新編寫(xiě)摘要和章節(jié)結(jié)構(gòu)。經(jīng)典教材目錄比如數(shù)據(jù)結(jié)構(gòu)、操作系統(tǒng)這類(lèi)經(jīng)典教材的章節(jié)順序本身就是很好的知識(shí)組織框架據(jù)此設(shè)計(jì)課程分類(lèi)。網(wǎng)絡(luò)公開(kāi)的OJ題目很多OJ平臺(tái)有開(kāi)放題目集自己重新改寫(xiě)題目描述和測(cè)試用例標(biāo)注題目來(lái)源不能原樣復(fù)制。這里要特別提醒一句畢設(shè)源碼和內(nèi)容都要注意版權(quán)邊界。直接從慕課網(wǎng)、B站把別人的視頻嵌進(jìn)來(lái)雖然演示時(shí)方便但存在侵權(quán)風(fēng)險(xiǎn)。我的方案是以圖文講義和代碼示例為主視頻只是補(bǔ)充版權(quán)上穩(wěn)妥得多。5.2 在線題目的打磨測(cè)試用例比描述更重要練習(xí)題準(zhǔn)備中我最大的體會(huì)是一道題值的不是題目描述而是測(cè)試用例。一道判斷回文數(shù)的題目測(cè)試用例至少要包含正例1211221、負(fù)例12310、邊界值0負(fù)數(shù)、溢出情況。原本我以為寫(xiě)完題目描述就完事了后來(lái)測(cè)試時(shí)發(fā)現(xiàn)用戶提交的代碼五花八門(mén)邊界用例不充分判題結(jié)果非常不穩(wěn)定。后面我每道題至少配8到10個(gè)用例3個(gè)公開(kāi)示例用例前端顯示 5個(gè)以上隱藏用例判題時(shí)使用。隱藏用例的意義在于用戶就算看到了示例也不能只針對(duì)示例寫(xiě)死答案必須真正理解算法。5.3 內(nèi)容安全底線UGC內(nèi)容不能裸奔學(xué)習(xí)筆記和用戶昵稱(chēng)是用戶生成內(nèi)容UGC這部分必須有審核機(jī)制。我的做法是三層過(guò)濾第一層前端輸入限制。昵稱(chēng)長(zhǎng)度、筆記字?jǐn)?shù)、圖片上傳格式先在前端攔住。第二層后端內(nèi)容過(guò)濾。引入敏感詞庫(kù)做關(guān)鍵詞匹配命中直接拒絕提交或者把內(nèi)容標(biāo)記為待審核狀態(tài)。第三層運(yùn)營(yíng)后臺(tái)審核。后臺(tái)管理列表里加一個(gè)筆記審核頁(yè)簽管理員可以查最近一周產(chǎn)生的筆記一鍵下架違規(guī)內(nèi)容。這個(gè)設(shè)計(jì)在答辯時(shí)很加分因?yàn)槟荏w現(xiàn)安全意識(shí)和隱私保護(hù)意識(shí)而很多同類(lèi)項(xiàng)目完全沒(méi)考慮這些問(wèn)題。6. 部署上線與運(yùn)營(yíng)細(xì)節(jié)從畢設(shè)到真正可使用的學(xué)習(xí)站點(diǎn)功能開(kāi)發(fā)完成后我部署在一臺(tái)輕量云服務(wù)器上2核4G配置系統(tǒng)用的Ubuntu 22.04環(huán)境是Nginx MySQL Redis Spring Boot jar包。整個(gè)部署過(guò)程比較標(biāo)準(zhǔn)說(shuō)幾個(gè)值得注意的細(xì)節(jié)。6.1 前后端分開(kāi)部署與Nginx配置前端打包后的靜態(tài)文件放在/var/www/html后端jar包用systemd管理服務(wù)設(shè)置開(kāi)機(jī)自啟和崩潰自動(dòng)重啟。Nginx這里配置一個(gè)反向代理把/api/前綴的請(qǐng)求轉(zhuǎn)發(fā)到后端服務(wù)server { listen 80; server_name your-domain.com; root /var/www/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }如果服務(wù)器帶寬不高建議Nginx開(kāi)啟壓縮圖片資源做一下瘦身網(wǎng)頁(yè)首屏加載速度會(huì)有顯著提升。為了省流量我的封面圖統(tǒng)一用了WebP格式比JPG大概小30%左右。6.2 用HTTPS保護(hù)用戶會(huì)話登錄功能上線后HTTP明文傳輸用戶的賬號(hào)密碼肯定不行。我申請(qǐng)了免費(fèi)的HTTPS證書(shū)在Nginx里配置443端口和證書(shū)路徑同時(shí)把80端口請(qǐng)求301重定向到HTTPS。這一步看起來(lái)不起眼但它保證了登錄會(huì)話的安全性也避免部分瀏覽器對(duì)非HTTPS頁(yè)面的不安全警告勸退用戶。6.3 冷啟動(dòng)讓網(wǎng)站真正被用起來(lái)網(wǎng)站上線后最尷尬的是沒(méi)有用戶。冷啟動(dòng)階段我做了三件事拉身邊人試用找了十來(lái)位同學(xué)、朋友注冊(cè)體驗(yàn)收集了一輪真實(shí)反饋。印象最深的是有人反饋學(xué)習(xí)路線圖很清晰但點(diǎn)擊節(jié)點(diǎn)跳轉(zhuǎn)后不知道該怎么回來(lái)于是我在路線圖頁(yè)面加了面包屑導(dǎo)航。SEO基礎(chǔ)配置給每個(gè)課程頁(yè)面配置合理的title和meta description提交搜索引擎收錄。雖然學(xué)習(xí)網(wǎng)站不是靠搜索流量起家的但別讓搜索引擎完全抓不到。定期更新題目上線后每周更新5到10道新題保持在線評(píng)測(cè)模塊的活躍度。網(wǎng)站不怕小怕的是放著不管。6.4 備份最后的保命手段數(shù)據(jù)備份是最容易被忽略的環(huán)節(jié)。我配置了一個(gè)簡(jiǎn)單的定時(shí)任務(wù)每天凌晨用mysqldump備份數(shù)據(jù)庫(kù)保留最近七天的備份文件。另外用戶上傳的圖片和筆記附件單獨(dú)打包備份到對(duì)象存儲(chǔ)。畢設(shè)答辯結(jié)束、網(wǎng)站不維護(hù)了數(shù)據(jù)備份依然有價(jià)值——至少交出去的時(shí)候還能恢復(fù)給下一屆同學(xué)或者作為代碼倉(cāng)庫(kù)的一部分。最后說(shuō)點(diǎn)實(shí)在的做這個(gè)計(jì)算機(jī)科學(xué)與技術(shù)學(xué)習(xí)網(wǎng)站我最深的感受是寫(xiě)代碼的時(shí)間只占三分之一剩下三分之二是需求梳理、數(shù)據(jù)準(zhǔn)備和排坑。尤其是學(xué)習(xí)路線和在線評(píng)測(cè)這兩個(gè)模塊前者要理解教學(xué)邏輯后者要保證判題安全都是純CRUD頁(yè)面體現(xiàn)不了的能力。如果讓我重做一次我會(huì)把在線評(píng)測(cè)的沙箱方案從Docker改成更輕量級(jí)的多進(jìn)程隔離減少每次判題的資源開(kāi)銷(xiāo)提高并發(fā)能力。另外后臺(tái)管理界面我會(huì)提前兩周開(kāi)始做別拖到最后通宵趕工——后臺(tái)的CRUD頁(yè)面雖然邏輯不復(fù)雜但表單字段多、列表篩選條件多工作量比想象中大不少。最后分享一個(gè)小技巧把學(xué)習(xí)路線圖的配置數(shù)據(jù)單獨(dú)抽成一個(gè)JSON文件放在前端資源目錄里開(kāi)發(fā)時(shí)先不用接數(shù)據(jù)庫(kù)直接讀JSON渲染頁(yè)面等UI確定了再切換到后端接口。這樣前端開(kāi)發(fā)和后端開(kāi)發(fā)在路線圖模塊上完全解耦是這次項(xiàng)目里節(jié)奏最順的一個(gè)模塊。