練報名系統(tǒng)全棧開發(fā)實踐)
接手本地業(yè)余足球俱樂部的運(yùn)營管理系統(tǒng)時我遇到的情況相當(dāng)?shù)湫途銟凡坷镉兴氖嗝郧騿T、三名兼職教練每周安排三到四次訓(xùn)練還穿插著青少年訓(xùn)練營和周末友誼賽。在此之前球員檔案散落在 Excel 表格里訓(xùn)練報名靠微信群接龍教練通知靠群發(fā)公告每次活動結(jié)束后統(tǒng)計出勤都要手工核對聊天記錄經(jīng)常出現(xiàn)“報名了沒到場、到場了沒報名”的亂賬。后來我用 Node.js 和 Vue 框架完整做了一套足球俱樂部管理系統(tǒng)把球員信息、訓(xùn)練計劃、活動報名、出勤統(tǒng)計全部收攏到一個前后端分離的 Web 系統(tǒng)里。這篇博文我會把這套球員訓(xùn)練活動報名系統(tǒng)從需求拆解、技術(shù)選型、數(shù)據(jù)庫設(shè)計到前后端具體實現(xiàn)的整個流程掰開揉碎地講給正在做類似管理系統(tǒng)、或者想練手全棧開發(fā)的讀者一份能直接落地的參考。1. 項目背景與整體設(shè)計思路1.1 需求場景拆解俱樂部的管理痛點做這類系統(tǒng)最忌諱一上來就寫代碼。我花了兩天時間跟俱樂部負(fù)責(zé)人、教練和幾個球員挨個聊把真實的工作流梳理清楚發(fā)現(xiàn)核心痛點集中在四塊。第一是球員檔案管理。俱樂部里每個球員除了姓名和電話還涉及年齡段、場上位置、體檢狀態(tài)、緊急聯(lián)系人。原來這些信息在教練手機(jī)上各存一份臨時需要某個位置的球員名單時只能翻聊天記錄。第二是訓(xùn)練計劃安排。教練每周要發(fā)布訓(xùn)練時間、地點、帶隊安排還要區(qū)分普通訓(xùn)練、體能課、對抗賽和青少年訓(xùn)練營不同課程對應(yīng)不同的適用人群。第三是報名和請假。球員要先看到訓(xùn)練安排再決定是否參加教練需要知道哪些人確定來以便準(zhǔn)備訓(xùn)練器材和分組。第四是出勤統(tǒng)計。俱樂部每年要給球員做評估出勤率是重要參考靠人工統(tǒng)計幾乎不可能準(zhǔn)確。把這些業(yè)務(wù)規(guī)則抽象出來之后系統(tǒng)功能就清晰了球員檔案 CRUD、訓(xùn)練課程排期、活動報名與取消、報名名單導(dǎo)出、通知公告、出勤統(tǒng)計。每個功能看起來簡單但背后都有隱含邏輯。比如報名不是點了就算完得處理滿員、截止時間、取消后名額返還、請假留痕這些都需要在數(shù)據(jù)模型上提前想清楚。這個階段我把需求畫成簡單的流程草圖和頁面原型跟需求方確認(rèn)后再進(jìn)入技術(shù)設(shè)計省掉了后面很多返工。1.2 技術(shù)選型為什么是 Node.js Vue這套系統(tǒng)的定位是俱樂部內(nèi)部使用的管理工具數(shù)據(jù)規(guī)模撐死幾千條并發(fā)量也不高但對開發(fā)效率和維護(hù)成本很敏感。選 Node.js 和 Vue 主要有三個理由。第一個理由是前后端語言統(tǒng)一。Node.js 和 Vue 都基于 JavaScript一個人維護(hù)全棧項目不需要切換語言上下文工具鏈也能共用。第二個理由是生態(tài)成熟。后端用 Express 寫 REST API 非常輕量配合 mysql2 操作數(shù)據(jù)庫、jsonwebtoken 做登錄鑒權(quán)、bcryptjs 做密碼加密這些庫都很穩(wěn)定。前端用 Vue 2 或 Vue 3 組件化開發(fā)配合 Element UI 這類現(xiàn)成組件庫后臺管理頁面的表格、表單、彈窗幾乎不用自己寫樣式開發(fā)速度很快。第三個理由是部署簡單。Node.js 應(yīng)用可以直接跑在一臺小服務(wù)器上前端打包成靜態(tài)文件交給 Nginx 托管運(yùn)維成本很低。也對比過 Java Spring Boot 方案。Spring Boot 在企業(yè)級項目和畢業(yè)設(shè)計里很常見框架規(guī)范、功能全面但對這種體量的內(nèi)部系統(tǒng)來說偏重環(huán)境配置和構(gòu)建鏈路更復(fù)雜開發(fā)節(jié)奏會慢不少。PHP 方案也有考慮但前后端分離后配套的工程化體驗不如 Node.js 順手。最終定下來的技術(shù)棧是后端 Node.js Express MySQL前端 Vue Vue Router Pinia Axios Element UI登錄采用 JWT 方案。這套組合對中小型管理類系統(tǒng)非常合適既能支撐功能擴(kuò)展又不會過度設(shè)計。1.3 功能邊界與角色劃分系統(tǒng)涉及三類用戶角色權(quán)限邊界必須在一開始就定死否則后面接口校驗會寫得很亂。我用一張功能矩陣來管理需求功能模塊管理員教練球員球員檔案維護(hù)增刪改查查看查看本人訓(xùn)練計劃創(chuàng)建全部操作創(chuàng)建/編輯自己負(fù)責(zé)的課程只讀活動報名查看名單查看名單/確認(rèn)到場報名/取消通知公告發(fā)布發(fā)布查看出勤統(tǒng)計全部查看所帶班級查看本人這個表的作用不只是存需求后續(xù)每個接口的權(quán)限校驗都要對照它來寫。比如球員調(diào)用刪除訓(xùn)練計劃的接口后端必須在中間件里攔截并返回 403而不是等進(jìn)了業(yè)務(wù)邏輯再判斷。我還刻意控制住了功能邊界第一期不做支付、不做多俱樂部 SaaS、不做復(fù)雜財務(wù)報表。這些在初期都屬于范圍蔓延真正使用起來才發(fā)現(xiàn)俱樂部最急需的就是把“訓(xùn)練 — 報名 — 出勤”這條核心鏈路跑通。2. 數(shù)據(jù)庫設(shè)計與接口規(guī)劃2.1 角色權(quán)限模型這個系統(tǒng)的權(quán)限模型比較簡單不引入 RBAC 那套復(fù)雜的角色-權(quán)限-菜單表直接在用戶表里加一個 role 字段就夠了。原因很簡單系統(tǒng)只有三種角色權(quán)限規(guī)則是固定的沒有必要把關(guān)系表設(shè)計得過度抽象。用戶登錄成功后后端會簽發(fā)一個 JWT里面帶上 userId 和 role。前端拿到 token 存在本地每次請求通過 Authorization 頭帶上。后端寫了一個 authMiddleware在需要權(quán)限的接口上掛載角色判斷直接用中間件參數(shù)完成。比如只允許管理員和教練調(diào)用的接口寫法大致是這樣的const requireRole (...roles) { return (req, res, next) { if (!req.user) { return res.status(401).json({ code: 401, message: 未登錄 }); } if (!roles.includes(req.user.role)) { return res.status(403).json({ code: 403, message: 沒有權(quán)限 }); } next(); }; }; router.post(/training-sessions, requireRole(admin, coach), sessionController.create);JWT 本身是無狀態(tài)的服務(wù)端不需要存 session這特別適合前后端分離部署。但要注意 JWT 密鑰必須放在環(huán)境變量里不能寫死在代碼中密鑰泄露等于所有用戶的登錄憑證都可偽造。2.2 核心表結(jié)構(gòu)設(shè)計數(shù)據(jù)庫我選了 MySQL用 Sequelize 還是直接寫 SQL 也糾結(jié)過。最后決定用 mysql2 連接池加手寫 SQL因為這個項目表之間關(guān)系不復(fù)雜手寫 SQL 的調(diào)試成本更低也更容易控制事務(wù)邊界。核心表一共規(guī)劃了五張。表名主要字段說明usersid, username, password_hash, role, player_id, created_at登錄賬號表與球員檔案關(guān)聯(lián)playersid, name, age_group, position, phone, emergency_contact, medical_status, created_at球員檔案training_sessionsid, title, coach_id, start_time, end_time, location, max_participants, status, created_at訓(xùn)練和活動安排training_registrationsid, session_id, player_id, status, register_time, remark報名記錄noticesid, title, content, publisher_id, publish_time, is_important通知公告重點說一下 training_registrations 這張表。報名記錄的 status 我設(shè)計了三個值registered 表示已報名待確認(rèn)confirmed 表示教練確認(rèn)到場cancelled 表示已取消。很多人在做報名系統(tǒng)時會直接物理刪除報名記錄但這里有個問題如果球員取消報名就刪行那“某人曾經(jīng)報過名但取消了”這個信息就丟失了而教練往往需要知道有人臨時請假以便復(fù)盤出勤情況。保留 cancelled 狀態(tài)出勤統(tǒng)計和請假分析都能做代價只是多一行數(shù)據(jù)而已非常劃算。在 session_id 和 player_id 上還加了唯一索引防止同一個球員對同一場訓(xùn)練重復(fù)報名。這個索引在并發(fā)請求下是最后一道防線后面的并發(fā)問題章節(jié)會詳細(xì)講。另外所有表的主鍵都用了自增整數(shù)沒有用 UUID。原因很簡單內(nèi)部系統(tǒng)的數(shù)據(jù)量少自增主鍵查詢性能好、索引占用小UUID 的優(yōu)勢在這里體現(xiàn)不出來。2.3 接口路徑與統(tǒng)一返回格式接口設(shè)計遵循 REST 風(fēng)格路徑按資源劃分。主要接口如下請求方法路徑功能權(quán)限POST/api/auth/login登錄公開GET/api/players球員列表管理員/教練POST/api/players新增球員管理員GET/api/training-sessions訓(xùn)練計劃列表登錄用戶POST/api/training-sessions創(chuàng)建訓(xùn)練管理員/教練GET/api/training-sessions/:id訓(xùn)練詳情與報名狀態(tài)登錄用戶POST/api/training-sessions/:id/register報名球員DELETE/api/training-sessions/:id/register取消報名球員GET/api/training-sessions/:id/registrations報名名單管理員/教練POST/api/notices發(fā)布通知管理員/教練所有接口統(tǒng)一返回格式{ code: 0, message: ok, data: {} }code 為 0 表示成功非 0 表示業(yè)務(wù)錯誤碼。這個約定必須在項目第一天就定好否則前后端聯(lián)調(diào)時容易各寫各的。我還把錯誤碼文檔放在項目 README 里比如 1001 表示訓(xùn)練已滿員、1002 表示訓(xùn)練已開始無法報名、1003 表示重復(fù)報名聯(lián)調(diào)時雙方對著文檔查錯誤信息能省下大量溝通時間。3. 實操開發(fā)流程與關(guān)鍵實現(xiàn)3.1 環(huán)境準(zhǔn)備Node.js 安裝配置與工程初始化開發(fā)這類項目第一步是搭環(huán)境。Node.js 我直接裝了官方 LTS 版本沒有用最新版因為 LTS 的穩(wěn)定性對項目開發(fā)更重要。裝完在終端驗證node -v和npm -v確保路徑正常。國內(nèi)環(huán)境還需要把 npm 源切換成鏡像源我用的是 npm config 命令npm config set registry https://registry.npmmirror.com這個步驟很多人會跳過但裝依賴時差距非常大尤其是 electron、sharp 這類帶二進(jìn)制文件的包用默認(rèn)源下載速度慢且容易失敗。后端工程用一個空目錄初始化npm init -y之后安裝依賴。我裝的包有 express、mysql2、jsonwebtoken、bcryptjs、cors、dotenv 和 nodemon。其中 nodemon 作為開發(fā)依賴文件變更后自動重啟服務(wù)。前端工程用 Vue CLI 創(chuàng)建vue create club-fe創(chuàng)建時選擇了 Vue 3 和 TypeScript。這里插一句業(yè)界對 TypeScript 的態(tài)度經(jīng)常分成兩派但對于管理系統(tǒng)這類表單密集、數(shù)據(jù)類型定義清晰的項目TypeScript 的優(yōu)勢能被放大能顯著減少字段名寫錯、類型不匹配這類低級問題。隨后繼續(xù)安裝 vue-router、pinia、axios 和 element-plus。環(huán)境初始化完成后有一個細(xì)節(jié)必須做在項目根目錄創(chuàng)建.env文件把數(shù)據(jù)庫連接信息、JWT 密鑰、服務(wù)端口放進(jìn)環(huán)境變量用 dotenv 加載。我見過太多項目把數(shù)據(jù)庫密碼硬編碼在 config 文件里提交到代碼倉庫這種習(xí)慣一旦倉庫權(quán)限失守整個數(shù)據(jù)庫就裸奔了。3.2 后端核心模塊實現(xiàn)后端代碼我按職責(zé)分層控制器只處理 HTTP 請求和響應(yīng)業(yè)務(wù)邏輯獨立成 service數(shù)據(jù)庫操作通過 model 層封裝。目錄結(jié)構(gòu)長這樣server/ ├── app.js ├── config/ │ └── db.js ├── middleware/ │ └── auth.js ├── controllers/ │ ├── authController.js │ ├── sessionController.js │ └── playerController.js ├── services/ │ ├── sessionService.js │ └── registrationService.js └── routes/ ├── auth.js ├── sessions.js └── players.js登錄接口是第一個需要落地的接口邏輯不復(fù)雜查出用戶比對 bcrypt 哈希密碼通過后簽發(fā) JWT。密碼絕不能明文存儲bcryptjs 的 hash 方法會自動加鹽不用自己拼隨機(jī)字符串。JWT 有效期我設(shè)成了 7 天前端在響應(yīng)攔截器里檢測到 401 就跳轉(zhuǎn)登錄頁并清掉本地緩存。報名接口是整個系統(tǒng)業(yè)務(wù)邏輯最密集的地方。我梳理了以下步驟async function registerSession(sessionId, userId, db) { const conn await db.getConnection(); try { await conn.beginTransaction(); // 1. 查出球員信息順便確認(rèn)賬號狀態(tài) const [playerRows] await conn.execute( SELECT id FROM players WHERE user_id ? AND status 1, [userId] ); if (playerRows.length 0) { throw new Error(球員檔案不存在或已禁用); } const playerId playerRows[0].id; // 2. 查出訓(xùn)練信息并鎖定當(dāng)前行 const [sessionRows] await conn.execute( SELECT id, start_time, max_participants FROM training_sessions WHERE id ? FOR UPDATE, [sessionId] ); const session sessionRows[0]; if (!session || session.start_time new Date()) { throw new Error(訓(xùn)練已開始或不存在); } // 3. 統(tǒng)計當(dāng)前有效報名人數(shù) const [countRows] await conn.execute( SELECT COUNT(*) AS cnt FROM training_registrations WHERE session_id ? AND status ! cancelled, [sessionId] ); if (countRows[0].cnt session.max_participants) { throw new Error(訓(xùn)練名額已滿); } // 4. 插入報名記錄 await conn.execute( INSERT INTO training_registrations (session_id, player_id, status, register_time) VALUES (?, ?, registered, NOW()), [sessionId, playerId] ); await conn.commit(); return { success: true }; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } }這段邏輯里最關(guān)鍵的是第 2 步的SELECT ... FOR UPDATE。在事務(wù)里鎖住訓(xùn)練計劃記錄后兩個球員同時提交報名請求時后一個請求必須等前一個事務(wù)提交統(tǒng)計人數(shù)時拿到的才是最新值。如果不用行鎖兩個并發(fā)請求可能同時讀到剩余名額 1然后都執(zhí)行插入導(dǎo)致實際人數(shù)超過 max_participants。這個坑在真實項目中很容易出現(xiàn)尤其在活動開放報名的前幾秒。3.3 前端頁面與報名功能實現(xiàn)前端我用 Vue Router 做了三個主頁面訓(xùn)練列表頁、訓(xùn)練詳情頁、球員管理頁加上登錄頁和儀表盤頁。路由配置里加了全局前置守衛(wèi)每次跳轉(zhuǎn)前檢查本地是否存在 token沒有就強(qiáng)制去登錄頁有就繼續(xù)。管理端頁面再根據(jù) role 判斷是否允許進(jìn)入。訓(xùn)練列表頁是球員最常用的頁面核心要求是信息清晰、操作直白。每張訓(xùn)練卡片展示標(biāo)題、時間、地點、帶隊教練和剩余名額。剩余名額的計算邏輯不放在前端而是由后端接口在返回列表時直接計算好。前端拿到數(shù)據(jù)后渲染“剩余名額”字段顯示為已滿時按鈕置灰不可點擊。報名狀態(tài)的判斷是前端最容易出錯的點。我通過詳情接口一次返回三個關(guān)鍵信息訓(xùn)練基本信息、當(dāng)前用戶對該訓(xùn)練的報名狀態(tài)、報名名單數(shù)組。前端拿到之后const canRegister computed(() { return ( !myRegistration.value session.value.remaining 0 new Date(session.value.start_time) new Date() ); });這里要特別說明 computed 的依賴收集。按鈕是否可用受三個條件控制任何一個變化都要重新計算Vue 的響應(yīng)式系統(tǒng)會自動處理但前提是模板里已經(jīng)綁定到對應(yīng)的響應(yīng)式變量。如果漏掉了對 myRegistration 的依賴就會出現(xiàn)“報名成功后按鈕還是可點”的這種情況實際上就是因為 computed 緩存沒有失效。報名成功后的交互也要考慮細(xì)節(jié)。我做了兩步第一步調(diào)用報名接口成功后重新拉取詳情接口刷新數(shù)據(jù)而不是本地手動把按鈕置灰。理由很簡單本地改的只是 UI 狀態(tài)真實數(shù)據(jù)可能因為其他球員同時報名已經(jīng)變了以服務(wù)端返回的數(shù)據(jù)為準(zhǔn)更穩(wěn)妥。第二步給出提示告訴用戶報名成功如果訓(xùn)練前 24 小時不能到場可以在詳情頁自行取消。3.4 前后端聯(lián)調(diào)與細(xì)節(jié)處理前后端聯(lián)調(diào)是管理類項目里最耗時間的環(huán)節(jié)很多問題不是邏輯寫錯而是雙方對接口的約定不一致。我在 axios 封裝上做了一些固定處理減少這類問題的出現(xiàn)。首先設(shè)置統(tǒng)一的 baseURL開發(fā)環(huán)境通過 Vite 的 proxy 把/api前綴代理到后端的http://localhost:3000這樣前端代碼里不用寫死域名生產(chǎn)環(huán)境也不用手動改。其次在請求攔截器里統(tǒng)一注入 Authorization 頭service.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });響應(yīng)攔截器統(tǒng)一處理兩種情況業(yè)務(wù)錯誤碼非 0 時彈出 message 提示HTTP 401 時清除 token 并跳轉(zhuǎn)登錄頁。這種集中式處理讓業(yè)務(wù)代碼里不用到處寫 try catch 和錯誤彈窗??缬騿栴}在開發(fā)環(huán)境通過 proxy 已經(jīng)解決生產(chǎn)環(huán)境則由 Nginx 反向代理解決。如果后端直接部署在云服務(wù)器上不經(jīng)過 Nginx那就得在 Express 里掛 cors 中間件并指定允許的來源白名單不要直接origin: *這樣存在被無關(guān)站點調(diào)用接口的風(fēng)險。聯(lián)調(diào)階段我還整理了一份接口自測清單每個接口至少覆蓋成功、未登錄、無權(quán)限、參數(shù)缺失四類情況確保前端拿到錯誤時能區(qū)分處理。4. 常見問題與排查技巧實錄4.1 開發(fā)環(huán)境安裝配置的三個常見坑開發(fā)環(huán)境的坑往往是卡住新手最久的地方。第一個就是 npm 腳本無法運(yùn)行的 PowerShell 報錯錯誤信息長這樣npm : 無法加載文件 D:\Program Files\nodejs\npm.ps1因為在此系統(tǒng)上禁止運(yùn)行腳本這個問題的原因很簡單Windows PowerShell 默認(rèn)執(zhí)行策略限制運(yùn)行腳本。解決辦法是以管理員身份打開 PowerShell執(zhí)行Set-ExecutionPolicy RemoteSigned確認(rèn)即可。這里要注意 RemoteSigned 只對本地腳本放行已簽名的遠(yuǎn)程腳本才允許運(yùn)行比設(shè)為 Unrestricted 要安全得多。第二個是 Node.js 版本跟 Vue CLI 或者某些依賴不兼容。比如 Node 版本過舊時Vue CLI 可能直接報錯無法啟動版本過新又有可能遇到某些原生模塊沒跟上。我的建議是長期使用 LTS 版本并且裝一個 nvm-windows 用來切換版本不同項目用不同版本的 Node這是開發(fā)多年積累下來的經(jīng)驗。第三個是端口占用。后端默認(rèn) 3000 端口被占用時Express 會報 EADDRINUSE 錯誤。排查方法是用netstat -ano | findstr :3000查看占用進(jìn)程再決定是殺掉進(jìn)程還是換端口。但在開發(fā)環(huán)境更推薦把端口配置放到.env里遇到?jīng)_突直接改環(huán)境變量不用動代碼。4.2 聯(lián)調(diào)過程中的典型問題聯(lián)調(diào)期間遇到最多的問題就是跨域。開發(fā)環(huán)境由 Vite proxy 解決但如果你跳過 proxy 直接從前端地址請求后端地址就會出現(xiàn) CORS 錯誤。這時候先確認(rèn) Nginx 或 proxy 配置是否生效再確認(rèn)后端 cors 中間件的白名單是否正確。排查口訣是先用 curl 直接請求后端接口能通就說明問題出在前端代理層。第二個問題是時間差。前端傳的start_time是帶時區(qū)的 ISO 字符串后端存進(jìn) MySQL 之后可能因為時區(qū)設(shè)置不對取出來發(fā)現(xiàn)比本地時間差 8 小時。這種問題很難靠肉眼察覺但會導(dǎo)致“訓(xùn)練 19:00 開始系統(tǒng)顯示 11:00”。解決方案是連接數(shù)據(jù)庫時在連接串里顯式指定timezone: 08:00并且數(shù)據(jù)庫表字段用 DATETIME 而不是 TIMESTAMP。TIMESTAMP 會受 MySQL 時區(qū)影響而 DATETIME 不帶時區(qū)配合后端統(tǒng)一處理更可控。第三個問題是中文亂碼。創(chuàng)建數(shù)據(jù)庫時一定要指定 utf8mb4 字符集稍微老一些的項目還在用 utf8能存中文但存不了 emoji。比如球員備注里寫了象形的表情符號直接報錯而 utf8mb4 沒這個問題。建庫語句我習(xí)慣寫完整CREATE DATABASE club_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;第四個問題是 Vue Router 的 history 模式打包后刷新 404。因為前端路由是瀏覽器端模擬的服務(wù)器上沒有對應(yīng)的物理文件。開發(fā)環(huán)境沒問題生產(chǎn)環(huán)境必須在 Nginx 里配置try_files $uri $uri/ /index.html;否則用戶在某個頁面按 F5 就整頁白屏。這個問題我第一次部署時就踩了排查了很久才發(fā)現(xiàn)是 Nginx 的 fallback 沒有加。4.3 報名并發(fā)與數(shù)據(jù)一致性問題報名模塊作為系統(tǒng)的核心并發(fā)問題必須認(rèn)真對待。除了前面講的SELECT ... FOR UPDATE行鎖我還在數(shù)據(jù)庫層做了兩道兜底。第一道是唯一索引。即使業(yè)務(wù)代碼有 bug并發(fā)插入同一球員同一訓(xùn)練的兩條報名記錄唯一索引也會讓第二條插入失敗。第二道是狀態(tài)機(jī)的約束。報名記錄一旦變成 cancelled不會允許重新變回 registered必須重新插入新記錄。這樣保證了取消操作的不可逆性歷史記錄才可信。實際還遇到過一種情況球員報名時名額還剩 1 個但同一時刻有兩個球員提交事務(wù)開得很晚導(dǎo)致其中一個失敗。這個在業(yè)務(wù)上是可以接受的因為確實只剩一個名額。關(guān)鍵是不能出現(xiàn)兩個都成功且超員這才是系統(tǒng)真正要避免的問題。我在壓測階段用并發(fā)腳本同時發(fā) 20 個報名請求逐個檢查列表人數(shù)最終確認(rèn)超員問題被鎖和唯一索引攔住了。這里要提醒的是如果用 ORM 的findOne先查再插不主動開事務(wù)加鎖ORM 層面很難保證一致性必須深入到數(shù)據(jù)庫事務(wù)那一層去設(shè)計。4.4 部署上線注意事項系統(tǒng)開發(fā)完成后我部署在一臺輕量云服務(wù)器上。前端先執(zhí)行npm run build打包產(chǎn)物是一個 dist 目錄里面全是靜態(tài)文件交給 Nginx 托管。后端代碼部署到服務(wù)器的/var/www/club-server目錄安裝生產(chǎn)依賴后通過 pm2 啟動。Node.js 進(jìn)程管理強(qiáng)烈建議用 pm2。直接node app.js啟動的話進(jìn)程一旦因為未捕獲異常退出服務(wù)就斷了還不會有任何自動恢復(fù)機(jī)制。pm2 常用命令非常簡潔pm2 start app.js --name club-server pm2 save pm2 logs club-server其中pm2 save會把當(dāng)前進(jìn)程列表保存下來配合pm2 startup生成系統(tǒng)服務(wù)服務(wù)器重啟后 Node.js 服務(wù)也會自動拉起。這個細(xì)節(jié)直接決定了系統(tǒng)能不能長時間穩(wěn)定運(yùn)行。Nginx 配置里需要同時處理靜態(tài)文件托管和接口反向代理server { listen 80; server_name your-domain.com; root /var/www/club-fe/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }生產(chǎn)環(huán)境數(shù)據(jù)庫密碼、JWT 密鑰這些敏感信息我在服務(wù)器上通過系統(tǒng)環(huán)境變量注入代碼倉庫里只保留.env.example模板文件這樣即使代碼泄露也不會直接連累線上數(shù)據(jù)。這套系統(tǒng)從立項到上線大約用了一個月的業(yè)余時間主體功能全部跑通后俱樂部用了兩個賽季報名和出勤統(tǒng)計基本不再出錯?;仡^看真正值得沉淀的不是 Node.js 和 Vue 的技術(shù)棧本身而是先把業(yè)務(wù)規(guī)則梳理清楚再用合適的數(shù)據(jù)結(jié)構(gòu)和事務(wù)去承接它們。如果讓我重新做一遍我仍然會堅持先畫表格、先定接口再動手寫代碼這個順序。個人實際操作中的體會是像足球俱樂部管理這類內(nèi)部系統(tǒng)需求方要的不是花哨的界面而是清晰的操作路徑和可靠的數(shù)據(jù)。報名系統(tǒng)尤其要處理好“并發(fā)”和“狀態(tài)這兩個關(guān)鍵詞寧可一開始多花時間在事務(wù)設(shè)計和唯一索引上也不要等到線上數(shù)據(jù)出錯再補(bǔ)救。后續(xù)這個項目還可以繼續(xù)擴(kuò)展比如給報名名單加二維碼簽到、按月份導(dǎo)出訓(xùn)練報表、接入企業(yè)微信通知等核心鏈路已經(jīng)打通擴(kuò)展方向就看你自己的業(yè)務(wù)場景需要什么了。