吧計(jì)費(fèi)管理系統(tǒng)實(shí)戰(zhàn):C語言跨天計(jì)費(fèi)與權(quán)限設(shè)計(jì)全解析)
簡介作為武漢理工大學(xué)計(jì)算機(jī)基礎(chǔ)與編程實(shí)驗(yàn)課程資源這份“網(wǎng)吧計(jì)費(fèi)管理系統(tǒng)”項(xiàng)目包面向相關(guān)課程學(xué)生及初學(xué)管理信息系統(tǒng)開發(fā)的讀者解決網(wǎng)吧場景中上網(wǎng)時長控制、自動計(jì)費(fèi)、批量管理及運(yùn)營維護(hù)等實(shí)驗(yàn)課題并額外加入超級管理員功能使權(quán)限設(shè)計(jì)更貼近實(shí)際應(yīng)用。資源共325個文件壓縮包33.1MB主體為Visual Studio工程結(jié)構(gòu)包含C/C源文件c、h及其編譯產(chǎn)物obj、pdb、ipch、tlog、ilk同時保留exe、vcxproj等文件便于直接打開工程查看源碼、跟蹤編譯過程或運(yùn)行驗(yàn)證。目前已有3178人學(xué)習(xí)下載對需要理解網(wǎng)吧收費(fèi)流程、鏈表與文件操作等核心編程知識或希望基于現(xiàn)成代碼做界面與權(quán)限功能擴(kuò)展的讀者具有明顯參考價(jià)值。1. 網(wǎng)吧計(jì)費(fèi)管理系統(tǒng)課程實(shí)驗(yàn)里最容易被低估的計(jì)費(fèi)與權(quán)限設(shè)計(jì)我最初拿到“網(wǎng)吧計(jì)費(fèi)管理系統(tǒng)”這個課程實(shí)驗(yàn)題目時以為它簡單得就像一份簽到表記錄上機(jī)時間、下機(jī)收錢、再存?zhèn)€文件。真正動手才發(fā)現(xiàn)卡住人的從來不是“輸入賬號密碼”那一步而是跨天通宵怎么計(jì)費(fèi)、程序重啟后上機(jī)記錄怎么恢復(fù)、普通管理員和超級管理員的權(quán)限邊界劃在哪。這篇筆記就按最穩(wěn)妥的方案講C 語言控制臺程序加文本文件存儲把費(fèi)率策略、上機(jī)狀態(tài)機(jī)、登錄鑒權(quán)、超管擴(kuò)展和常見踩坑一次理清楚。適合正在做這個實(shí)驗(yàn)、或者想用同一個骨架改造成其他管理系統(tǒng)的人新手能照著敲熟手直接看第 5 章的坑就行。2. 計(jì)費(fèi)設(shè)計(jì)先立住費(fèi)率時段、數(shù)據(jù)結(jié)構(gòu)與上機(jī)狀態(tài)機(jī)2.1 費(fèi)率策略計(jì)費(fèi)單位、高峰時段與進(jìn)位方式怎么定計(jì)費(fèi)是網(wǎng)吧計(jì)費(fèi)管理系統(tǒng)的靈魂也是課程設(shè)計(jì)評分時最容易拉差距的地方。很多實(shí)現(xiàn)把費(fèi)率寫成一個固定數(shù)字比如“每分鐘一塊錢”演示時只能跑一種場景。實(shí)際做的時候我建議把費(fèi)率定義成多個“時段段”每個時段段包含四個要素時段名稱、起止時間、每分鐘價(jià)格、金額進(jìn)位方式。時段名稱起止時間費(fèi)率規(guī)則適用說明普通時段08:00 - 18:000.8 元/分鐘白天基本價(jià)格高峰時段18:00 - 22:001.2 元/分鐘晚間加價(jià)體現(xiàn)高峰策略通宵時段22:00 - 次日 08:000.5 元/分鐘跨天時段是實(shí)現(xiàn)的難點(diǎn)這里有兩個選擇要把理由說清楚。第一為什么用“分鐘單價(jià)”而不是“每小時單價(jià)”因?yàn)樯蠙C(jī)時長天然落到分鐘粒度用分鐘單價(jià)配合進(jìn)位策略賬單展示最直觀測試邊界也方便。第二金額進(jìn)位我一般用“向上取整到分”。四舍五入雖然數(shù)學(xué)上公平但訂單里可能出現(xiàn) 0.3000000004 這種浮點(diǎn)誤差金額演示時特別難看。向上取整符合網(wǎng)吧行業(yè)慣例也能把浮點(diǎn)計(jì)算的不確定性擋在門外。再說時段切換。真實(shí)系統(tǒng)里高峰時段和通宵時段經(jīng)常相鄰跨天計(jì)費(fèi)必須支持“一段上機(jī)時間跨多個費(fèi)率段”的情況。比如 17:50 上機(jī)、18:10 結(jié)賬前 10 分鐘按普通時段后 10 分鐘按高峰時段。如果費(fèi)率計(jì)算函數(shù)不支持這種分段累加后面接需求時每改一處就要重寫一遍核心邏輯。所以從最開始就把“計(jì)費(fèi) 多個時間片段的金額相加”這個模型立住后續(xù)所有功能都會順。2.2 數(shù)據(jù)結(jié)構(gòu)管理員、會員與上機(jī)記錄三張表數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)直接決定代碼能寫多長、后面擴(kuò)展好不好加。我常用的做法是拆成三張表管理員表、會員表、上機(jī)訂單表。管理員管理后臺登錄會員管理上機(jī)余額訂單管理每一次上機(jī)和結(jié)賬。三者用卡號或用戶名關(guān)聯(lián)不把字段堆在一個大結(jié)構(gòu)體里。typedef struct { char username[20]; // 登錄名 char passwd[64]; // 混淆后的口令不存明文 int role; // 1普通管理員 2超級管理員 int enabled; // 0禁用 1啟用 } Admin; typedef struct { char card_id[16]; // 會員卡號 char name[32]; float balance; // 賬戶余額 int status; // 0離線 1上機(jī)中 2暫停中 } Member; typedef struct { char order_id[24]; // 訂單號 char card_id[16]; char admin[20]; // 操作員賬號 time_t start_time; // 上機(jī)時間統(tǒng)一用 time_t time_t end_time; // 下機(jī)時間未結(jié)賬時為 0 float rate; // 費(fèi)率快照元/分鐘 int minutes; // 計(jì)費(fèi)分鐘數(shù) float amount; // 應(yīng)收金額 int state; // 0進(jìn)行中 1已結(jié)賬 } Order;幾個字段要特別說明。start_time和end_time用time_t而不是字符串是因?yàn)榭缣煊?jì)算必須做時間戳差值HH:MM字符串比較在跨天時會直接算錯。rate字段是費(fèi)率快照不是從全局費(fèi)率表臨時查的訂單生成那一刻把當(dāng)時的單價(jià)存下來之后改費(fèi)率不會影響歷史賬單這是后面擴(kuò)展超管“調(diào)價(jià)功能”的前提。state字段則是系統(tǒng)重啟恢復(fù)的基礎(chǔ)第 5 章會專門講它。會員表和訂單表通過card_id關(guān)聯(lián)管理員表和訂單表通過admin賬號關(guān)聯(lián)。這樣統(tǒng)計(jì)“某個管理員一天開了多少錢的單子”時掃一遍訂單表按admin分組即可不需要回查管理員表。2.3 上機(jī)狀態(tài)機(jī)上機(jī)、暫停、結(jié)賬的事件與邊界狀態(tài)機(jī)是這類型系統(tǒng)里最容易寫成一團(tuán)亂麻的部分。很多代碼用if (status 1) { ... } else if (status 2) { ... }到處散落判斷加一個“暫?!惫δ芫鸵钠甙颂?。正確做法是先定義狀態(tài)和事件的映射表再按表寫代碼。當(dāng)前狀態(tài)觸發(fā)事件下一狀態(tài)需要執(zhí)行的動作空閑管理員點(diǎn)擊上機(jī)上機(jī)中生成訂單state0記錄開始時間上機(jī)中會員臨時離開暫停中記錄暫停開始時間暫停期間不計(jì)費(fèi)暫停中會員回來上機(jī)中恢復(fù)計(jì)費(fèi)累加暫停時長用于展示上機(jī)中管理員結(jié)賬已結(jié)賬計(jì)算費(fèi)用寫訂單文件扣余額暫停中管理員強(qiáng)制結(jié)賬已結(jié)賬按暫停策略結(jié)算比如超過 30 分鐘自動結(jié)賬注意最后一行的“暫停超過 30 分鐘自動結(jié)賬”這是網(wǎng)吧真實(shí)場景里必須有的兜底邏輯。如果只做“暫停不計(jì)費(fèi)”而不做超時處理會員離開后機(jī)器會被一直占用演示時導(dǎo)師問一句“人走了機(jī)器空著怎么辦”就會卡住。實(shí)現(xiàn)上可以在訂單結(jié)構(gòu)體里加一個pause_start字段每次循環(huán)檢測或結(jié)賬時判斷暫停時長。把狀態(tài)機(jī)表畫清楚之后再寫代碼每個函數(shù)只處理“一個事件導(dǎo)致的一次狀態(tài)遷移”避免在菜單函數(shù)里堆邏輯。后面加功能時比如“會員自助換機(jī)”就只是在這個表里加一行新事件不會牽動核心計(jì)費(fèi)。3. 核心流程落地用 C 語言實(shí)現(xiàn)登錄、上機(jī)與結(jié)賬并落進(jìn)文件3.1 存儲選型為什么用文本文件而不是二進(jìn)制課程實(shí)驗(yàn)的數(shù)據(jù)量很小——哪怕模擬 100 個會員、每天 500 條訂單文本文件也就幾百 KB完全沒有性能壓力。所以存儲方案我選文本文件字段之間用|分隔每行一條記錄。這個選擇有三個實(shí)際理由一是出錯時可以打開文件直接看內(nèi)容定位問題二是助教檢查代碼時可以快速理解數(shù)據(jù)結(jié)構(gòu)評分友好三是文本格式不依賴編譯器對結(jié)構(gòu)體的內(nèi)存對齊換到機(jī)房電腦也能讀。二進(jìn)制整體讀寫最大的坑是結(jié)構(gòu)體 padding。sizeof(Order)在 32 位和 64 位編譯環(huán)境下結(jié)果不一樣直接fwrite(order, sizeof(order), 1, fp)寫出來的文件在自己機(jī)器上能讀拷到另一臺電腦上就亂碼。這種問題在答辯現(xiàn)場非常尷尬而且不好排查。文本文件逐字段讀寫雖然代碼多一點(diǎn)但換來的是穩(wěn)定和透明。// 寫入一條訂單字段之間用 | 分隔 void order_save(const Order *o) { FILE *fp fopen(orders.txt, a); if (fp NULL) return; fprintf(fp, %s|%s|%s|%lld|%lld|%.2f|%d|%.2f|%d\n, o-order_id, o-card_id, o-admin, (long long)o-start_time, (long long)o-end_time, o-rate, o-minutes, o-amount, o-state); fclose(fp); }fopen用追加模式a每次寫訂單都直接追加到文件末尾不覆蓋舊數(shù)據(jù)。time_t在輸出時強(qiáng)轉(zhuǎn)成long long是為了避免在 32 位環(huán)境下time_t被截?cái)喑?int 導(dǎo)致時間錯亂。讀取過程則是一行行fgets后用strtok按|切分再逐字段賦值給結(jié)構(gòu)體。3.2 登錄與權(quán)限校驗(yàn)口令混淆、次數(shù)限制與統(tǒng)一錯誤提示登錄模塊是管理后臺的入口代碼本身不難但有兩個細(xì)節(jié)值得認(rèn)真做口令不能明文存、登錄失敗要有統(tǒng)一提示。明文密碼的問題是演示時文件一打開所有賬號密碼全暴露了。課程實(shí)驗(yàn)不要求上正規(guī)哈希算法但至少做一層簡單的異或混淆防止被直接看到。void confuse(char *s) { unsigned char key 0x5A; while (*s) { *s *s ^ key; s; } } int login(Admin *cur) { char u[32], p[64]; printf(請輸入管理員賬號: ); scanf(%31s, u); printf(請輸入密碼: ); scanf(%63s, p); confuse(p); // 對輸入做相同混淆再與文件內(nèi)容比對 for (int i 0; i admin_cnt; i) { if (strcmp(admins[i].username, u) 0 strcmp(admins[i].passwd, p) 0) { if (admins[i].enabled 0) { printf(該賬號已被禁用\n); return 0; } *cur admins[i]; return 1; } } printf(賬號或密碼錯誤\n); return 0; }這里有個安全上的細(xì)節(jié)賬號不存在和密碼錯誤使用同一條提示信息“賬號或密碼錯誤”。如果區(qū)分成“賬號不存在”和“密碼錯誤”別人就能通過提示逐條試探出哪些賬號是有效的。課程設(shè)計(jì)里導(dǎo)師可能會問這個點(diǎn)統(tǒng)一提示是加分項(xiàng)。登錄次數(shù)限制也建議加上連續(xù)輸錯 3 次直接退出程序。實(shí)現(xiàn)方法是在main的登錄循環(huán)里記一個計(jì)數(shù)器login返回 0 時加一超過閾值就讓程序結(jié)束。這個機(jī)制不復(fù)雜但能體現(xiàn)“系統(tǒng)要考慮暴力破解”的意識。密碼混淆只是防瞟一眼真正的安全需要完整哈希和加鹽課堂作業(yè)不做但可以寫進(jìn)報(bào)告里的“擴(kuò)展展望”部分。3.3 計(jì)費(fèi)計(jì)算函數(shù)跨天分段與進(jìn)位取整的正確寫法計(jì)費(fèi)函數(shù)是整個系統(tǒng)最核心的代碼我見過太多實(shí)現(xiàn)直接把end_time - start_time除以 60 乘單價(jià)結(jié)果一到通宵時段就翻車??缣斓谋举|(zhì)是一段上機(jī)時間可能落入多個費(fèi)率段每個段要分別計(jì)價(jià)再相加。處理這個需求不需要寫“跨天特判”而是把費(fèi)率匹配和“跳到下一段邊界”做成兩個小函數(shù)循環(huán)累加即可。// 判斷某個時刻落在哪個費(fèi)率段 RateZone *find_zone(int h, int m, RateZone *z, int n) { int now h * 60 m; for (int i 0; i n; i) { int s z[i].start_h * 60 z[i].start_m; int e z[i].end_h * 60 z[i].end_m; if (s e) { // 不跨天的時段 if (now s now e) return z[i]; } else { // 跨天時段如 22:00-08:00 if (now s || now e) return z[i]; } } return NULL; } // 計(jì)算當(dāng)前時間的下一段邊界 time_t zone_end(time_t t, RateZone *z) { struct tm tm *localtime(t); tm.tm_hour z-end_h; tm.tm_min z-end_m; tm.tm_sec 0; return mktime(tm); } float calc_fee(time_t start, time_t end, RateZone *zones, int zone_cnt) { float total 0; time_t cur start; while (cur end) { struct tm *tm localtime(cur); RateZone *z find_zone(tm-tm_hour, tm-tm_min, zones, zone_cnt); if (z NULL) { cur 60; continue; } time_t stop zone_end(cur, z); if (stop end) stop end; int minutes (int)(stop - cur) / 60; total minutes * z-price; cur stop; } return ceilf(total * 100) / 100; // 分以上取整避免浮點(diǎn)尾差 }這段邏輯的關(guān)鍵在find_zone的跨天判斷當(dāng)結(jié)束小時小于開始小時時說明這個費(fèi)率段跨了午夜now s || now e這個條件能同時覆蓋“22 點(diǎn)到 24 點(diǎn)”和“0 點(diǎn)到 8 點(diǎn)”兩段。而zone_end直接設(shè)置結(jié)束時刻的tm_hour為end_h再交給mktime自動進(jìn)位。比如費(fèi)率段是 22:00 到 08:00zone_end算出的時間戳就是次日 08:00不需要手工判斷日期加一。calc_fee的循環(huán)每輪跳到一個費(fèi)率段的邊界一個訂單最多循環(huán)幾次不會出現(xiàn)性能問題。注意浮點(diǎn)誤差total是 float累加幾十次后可能出現(xiàn) 0.600000001 這種值最后用ceilf向上取整到分金額展示永遠(yuǎn)干凈。取整策略要和 2.1 節(jié)說的一致這里實(shí)現(xiàn)了“進(jìn)一取整”如果想改成四舍五入把ceilf換成roundf即可。4. 擴(kuò)展超級管理員從普通收銀到系統(tǒng)管理員的權(quán)限跨越4.1 權(quán)限邊界設(shè)計(jì)普通管理員與超級管理員各管什么很多同學(xué)做“擴(kuò)展超級管理員功能”只是多加一個用戶名登錄后界面一模一樣這等于沒有擴(kuò)展。真實(shí)的權(quán)限設(shè)計(jì)要把功能清單拆成兩層普通管理員負(fù)責(zé)日常營業(yè)操作超級管理員負(fù)責(zé)系統(tǒng)配置和數(shù)據(jù)安全。功能模塊普通管理員超級管理員會員開卡、充值和上機(jī)結(jié)賬可以可以查詢自己的操作記錄可以可以新增或刪除管理員賬號不可以可以修改費(fèi)率配置不可以可以查看全店?duì)I收統(tǒng)計(jì)不可以可以手工修正異常訂單不可以可以權(quán)限控制要在代碼里集中做不能每個菜單函數(shù)各寫各的。我一般抽一個公共函數(shù)int require(int need_role, const Admin *cur) { if (cur-role need_role) { printf(權(quán)限不足該操作需要%s權(quán)限\n, need_role ROLE_SUPER ? 超級管理員 : 管理員); return 0; } return 1; }每個需要權(quán)限的函數(shù)第一行都調(diào)用它。比如修改費(fèi)率函數(shù)里寫if (!require(ROLE_SUPER, cur)) return;這樣以后新增功能時只要照著這個模式加一行就不會出現(xiàn)漏校驗(yàn)的越權(quán)漏洞。演示時導(dǎo)師切到普通管理員賬號操作超管菜單看到“權(quán)限不足”提示這一步印象分就拿到了。4.2 營收統(tǒng)計(jì)按日期、按管理員維度匯總訂單營收統(tǒng)計(jì)是超管最有說服力的功能。訂單文件里每行是一條已結(jié)賬記錄要做的是按不同維度累加amount。課程實(shí)驗(yàn)規(guī)模小不需要建索引全量掃描一遍文件即可代碼更直接。void stat_daily_report(const char *date) { FILE *fp fopen(orders.txt, r); char line[256]; float total 0; int order_cnt 0; while (fgets(line, sizeof(line), fp)) { Order o; if (parse_order(line, o) 0) continue; char day[16]; strftime(day, sizeof(day), %Y-%m-%d, localtime(o.start_time)); if (strncmp(day, date, 10) ! 0) continue; total o.amount; order_cnt; } fclose(fp); printf(%s 共完成 %d 筆訂單營收 %.2f 元\n, date, order_cnt, total); }這里的parse_order是 3.1 節(jié)讀取函數(shù)的封裝把一行文本解析成Order結(jié)構(gòu)體。統(tǒng)計(jì)時有兩個細(xì)節(jié)一是只用已結(jié)賬即state 1的記錄進(jìn)行中的訂單金額還不確定不能計(jì)入二是按start_time轉(zhuǎn)出來的日期分組而不是按訂單號前綴分組因?yàn)橛唵翁柨缣鞎r前綴可能和實(shí)際日期不一致。如果想按操作員維度統(tǒng)計(jì)把分組鍵從日期改成o.admin循環(huán)里用strcmp匹配即可思路完全一樣。4.3 后臺維護(hù)管理員增刪、費(fèi)率調(diào)整與數(shù)據(jù)備份后臺維護(hù)功能里最容易出問題的是“刪除管理員”的邊界處理。至少要考慮兩條規(guī)則不能刪除當(dāng)前登錄賬號自己系統(tǒng)里至少要保留一個超級管理員。否則一個誤操作把所有超管賬號刪光整個系統(tǒng)就鎖死了。int delete_admin(const char *target, const Admin *cur) { if (strcmp(target, cur-username) 0) { printf(不能刪除當(dāng)前登錄賬號\n); return 0; } int super_cnt 0; for (int i 0; i admin_cnt; i) { if (admins[i].role ROLE_SUPER admins[i].enabled) super_cnt; } if (super_cnt 1 is_super(target)) { printf(系統(tǒng)至少保留一個超級管理員\n); return 0; } // 找到目標(biāo)賬號標(biāo)記 enabled 0 或者從文件移除 printf(管理員 %s 已刪除\n, target); return 1; }費(fèi)率調(diào)整是超管擴(kuò)展里最能體現(xiàn)“設(shè)計(jì)意識”的功能。實(shí)現(xiàn)上建議把費(fèi)率表放一個外部配置文件比如rate.conf程序啟動時讀取超管在程序里修改后寫回文件。這樣做的好處是演示時可以直接打開配置文件改價(jià)格再重啟程序看到新費(fèi)率生效比在程序里層層菜單操作直觀得多。數(shù)據(jù)備份實(shí)現(xiàn)也簡單就是把數(shù)據(jù)文件復(fù)制一份帶時間戳的副本void backup_data() { char name[64]; time_t now time(NULL); strftime(name, sizeof(name), backup_%Y%m%d_%H%M%S.bak, localtime(now)); FILE *src fopen(orders.txt, r); FILE *dst fopen(name, w); char ch; while ((ch fgetc(src)) ! EOF) fputc(ch, dst); fclose(src); fclose(dst); printf(備份完成: %s\n, name); }注意備份和恢復(fù)操作前要提示先關(guān)閉系統(tǒng)其他功能避免文件占用導(dǎo)致復(fù)制不完整?;謴?fù)操作更要把當(dāng)前數(shù)據(jù)文件先改名留底防止覆蓋錯。5. 避坑與排查計(jì)費(fèi)系統(tǒng)最容易翻車的 5 個細(xì)節(jié)5.1 跨天時間差算成負(fù)數(shù)現(xiàn)象通宵上機(jī)從 23:50 開始次日 01:20 結(jié)賬控制臺打出“結(jié)算時長 -1350 分鐘金額 -675.00 元”。原因代碼里把start_time和end_time存成了“HH:MM”格式的字符串跨天時 “01:20” 比 “23:50” 小字符串比較直接判定為負(fù)數(shù)?;蛘哂腥擞胕nt存time_t在 32 位環(huán)境時間戳溢出后計(jì)算混亂。解決從結(jié)構(gòu)體層面根除這個問題——時間字段一律用time_t存時間戳展示時才用localtime轉(zhuǎn)字符串。費(fèi)率計(jì)算用 3.3 節(jié)的calc_fee由find_zone處理跨天段匹配不要在業(yè)務(wù)代碼里手寫小時數(shù)比較。謹(jǐn)記一條血淚經(jīng)驗(yàn)時間計(jì)算只在時間戳層面做字符串只用于給人看。5.2 scanf 讀菜單導(dǎo)致死循環(huán)現(xiàn)象菜單輸入數(shù)字時誤敲了字母程序不是報(bào)錯退出而是像失控一樣瘋狂刷菜單刷到必須強(qiáng)制關(guān)窗口。原因scanf(%d, choice)遇到字母時匹配失敗請求的字符沒有被消費(fèi)殘留在輸入緩沖區(qū)里。下一次循環(huán)scanf又讀到同一個字母再次失敗形成“讀失敗、殘留、再讀同一個字符”的死循環(huán)。解決用fgets讀一行再解析而不是直接用scanf讀整數(shù)。char line[32]; fgets(line, sizeof(line), stdin); if (sscanf(line, %d, choice) ! 1) { printf(輸入無效請重新輸入\n); continue; }fgets會可靠消費(fèi)掉一整行輸入殘留問題從根上消失。這個坑幾乎每個做控制臺課程設(shè)計(jì)的人都會踩一次提前處理能省下大量調(diào)試時間。5.3 刪除超管時把自己的賬號刪了現(xiàn)象超管想清理一個離職同事的賬號測試時順手把自己賬號刪了退出登錄后整個系統(tǒng)沒有超管可進(jìn)后臺徹底鎖死。原因刪除函數(shù)只對比了用戶名沒有檢查“被刪對象是不是當(dāng)前登錄者”和“系統(tǒng)是否還有別的超管”。這個問題平時不觸發(fā)一觸發(fā)就是事故現(xiàn)場。解決刪除前做兩層校驗(yàn)——先比對strcmp(target, cur-username)不允許刪自己再統(tǒng)計(jì)剩余啟用超管數(shù)量小于等于 1 時禁止刪除。這個邏輯在 4.3 節(jié)已經(jīng)給出完整實(shí)現(xiàn)核心是把這個邊界當(dāng)成功能需求來對待而不是碰運(yùn)氣的檢查。5.4 程序重啟后所有機(jī)位全部變成空閑現(xiàn)象有會員正在上機(jī)程序因斷電退出重啟后機(jī)位表顯示全部空閑但晚上對賬發(fā)現(xiàn)少收錢。原因上機(jī)狀態(tài)只存在內(nèi)存里的結(jié)構(gòu)體數(shù)組中沒有及時落盤。正常結(jié)賬時會寫訂單文件但“上機(jī)中”這個狀態(tài)沒有獨(dú)立持久化異常退出時內(nèi)存數(shù)據(jù)全丟。解決上機(jī)動作發(fā)生時立即把訂單寫入文件state字段置 0 表示進(jìn)行中程序啟動時掃描訂單文件把所有state 0的記錄重新加載到機(jī)位表上恢復(fù)營業(yè)現(xiàn)場。這個恢復(fù)函數(shù)放在 6.2 節(jié)具體講它是整個系統(tǒng)健壯性的關(guān)鍵一環(huán)。5.5 二進(jìn)制結(jié)構(gòu)體直寫文件換機(jī)器就亂碼現(xiàn)象在自己電腦上運(yùn)行正??截惖綄?shí)驗(yàn)室電腦后打開數(shù)據(jù)文件訂單金額變成天文數(shù)字會員余額也亂套。原因直接在代碼里fwrite(order, sizeof(order), 1, fp)把結(jié)構(gòu)體內(nèi)存塊原樣寫入文件。不同編譯器、不同對齊選項(xiàng)下sizeof不一樣結(jié)構(gòu)體內(nèi)部的 padding 字節(jié)在不同環(huán)境可能寫入垃圾值換機(jī)器讀回時字段錯位。解決放棄二進(jìn)制整體讀寫統(tǒng)一用文本格式逐字段讀寫字段間用|分隔。課程實(shí)驗(yàn)的數(shù)據(jù)量完全不需要二進(jìn)制文件的性能和體積優(yōu)勢而文本文件的可讀性和跨平臺穩(wěn)定性是實(shí)打?qū)嵉氖找?。代碼實(shí)現(xiàn)就是 3.1 節(jié)的order_save和配套的parse_order讀取函數(shù)。6. 進(jìn)階把實(shí)驗(yàn)從“能跑”做到“敢答辯”的驗(yàn)證與加分項(xiàng)6.1 測試用例清單先想清楚怎么才算驗(yàn)證通過寫代碼之前先列測試用例是答辯時敢說“系統(tǒng)經(jīng)過驗(yàn)證”的底氣。下表是我做這個實(shí)驗(yàn)時用的最小測試集覆蓋正常流程和邊界場景。用例編號測試場景操作步驟預(yù)期結(jié)果T01正常上機(jī)結(jié)賬普通時段上機(jī) 30 分鐘后結(jié)賬金額 30 × 0.8 24 元T02跨時段計(jì)費(fèi)17:50 上機(jī)18:10 結(jié)賬前 10 分鐘普通檔后 10 分鐘高峰檔T03跨天通宵23:50 上機(jī)次日 01:20 結(jié)賬全部按通宵檔 0.5 元/分鐘計(jì)費(fèi)T04余額不足結(jié)賬把會員余額改成 1 元模擬上機(jī) 2 小時系統(tǒng)提示余額不足拒絕下機(jī)或允許賒欠標(biāo)記T05權(quán)限越權(quán)普通管理員操作超管菜單提示“權(quán)限不足”功能不可用T06暫停超時暫停 31 分鐘后點(diǎn)恢復(fù)系統(tǒng)提示已自動結(jié)賬每個用例都要在代碼里跑一遍并把實(shí)際輸出和預(yù)期結(jié)果寫進(jìn)實(shí)驗(yàn)報(bào)告。T03 是最容易翻車的用例如果calc_fee寫對了跨天用例和普通用例共用同一套代碼路徑不需要單獨(dú)寫分支。6.2 兩個藏加分點(diǎn)啟動恢復(fù)與外部費(fèi)率文件啟動恢復(fù)函數(shù)state_recover是我強(qiáng)烈建議加的功能。它的職責(zé)很單純程序啟動時打開訂單文件把所有state 0的記錄讀入內(nèi)存按機(jī)器號恢復(fù)機(jī)位狀態(tài)。void state_recover() { FILE *fp fopen(orders.txt, r); char line[256]; while (fgets(line, sizeof(line), fp)) { Order o; if (!parse_order(line, o)) continue; if (o.state 0) { // 進(jìn)行中的訂單 seat_set_busy(o.card_id, o.start_time); } } fclose(fp); }這個函數(shù)的妙處在于它讓“斷電恢復(fù)”從一句空話變成可演示的功能。答辯時可以現(xiàn)場故意模擬異常退出——先讓一個會員上機(jī)再強(qiáng)制結(jié)束進(jìn)程重新啟動后看到機(jī)位自動恢復(fù)占用狀態(tài)。這個動作比任何口頭解釋“系統(tǒng)會保存狀態(tài)”都有說服力。配合的外部費(fèi)率配置文件則讓調(diào)價(jià)演示變得干凈打開rate.conf把高峰時段價(jià)格改成 1.5重啟程序新訂單按新價(jià)計(jì)費(fèi)歷史賬單金額不受影響因?yàn)橘M(fèi)率快照已經(jīng)存在每條訂單里。我最近一次幫朋友改這類實(shí)驗(yàn)最大的體會是這個題目拿高分的核心不在代碼量而在邊界處理。把跨天計(jì)費(fèi)、狀態(tài)恢復(fù)、權(quán)限邊界這三個點(diǎn)做扎實(shí)整個系統(tǒng)的完成度一下就和普通版本拉開差距。如果你打算自行練手建議按這個順序推進(jìn)——先定結(jié)構(gòu)體和狀態(tài)機(jī)再寫計(jì)費(fèi)函數(shù)最后補(bǔ)超管擴(kuò)展別一上來就寫菜單循環(huán)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取