開發(fā)實戰(zhàn):從數(shù)據(jù)庫設(shè)計到部署避坑指南)
簡介這是一份基于C# Winform窗體的酒店管理系統(tǒng)完整項目面向C#課程大作業(yè)、畢業(yè)設(shè)計以及對管理類軟件開發(fā)感興趣的初學(xué)者與開發(fā)者。系統(tǒng)采用C#與SQL Server作為技術(shù)棧內(nèi)置管理員和學(xué)生兩類用戶支持管理員信息維護、學(xué)生信息維護、課程開設(shè)與查詢、成績錄入與統(tǒng)計等典型管理功能能夠幫助讀者深入理解Winform窗體布局、數(shù)據(jù)表關(guān)系設(shè)計、數(shù)據(jù)庫連接以及基礎(chǔ)增刪改查操作的實現(xiàn)方式。壓縮包內(nèi)共包含六百一十七個文件涵蓋三十九個C#源代碼文件、六十六個數(shù)據(jù)庫文件、一百九十二個GIF操作演示圖、一百九十二個SSK皮膚美化資源以及動態(tài)鏈接庫、項目配置文件、資源文件、可執(zhí)行文件、圖標(biāo)與音頻等輔助材料整體壓縮包大小約為九十六點五八兆字節(jié)。其中GIF動畫可直觀展示系統(tǒng)各功能的運行流程SSK皮膚與資源文件負(fù)責(zé)界面美化可執(zhí)行文件適合快速預(yù)覽整體效果源碼和數(shù)據(jù)庫文件則可直接重新編譯與調(diào)試。目前已經(jīng)有一百七十三人學(xué)習(xí)下載。同時包內(nèi)提供完整的解決方案工程、項目配置與數(shù)據(jù)庫文件并附帶運行所需的各類資源和演示素材目錄結(jié)構(gòu)規(guī)整便于按功能模塊快速檢索定位。既可以直接運行查看系統(tǒng)效果也可以對照源碼研究業(yè)務(wù)邏輯在此基礎(chǔ)上進行模塊擴展與二次開發(fā)是完成課程設(shè)計、畢業(yè)設(shè)計或面試項目時可復(fù)用的參考版本。1. 為什么酒店管理系統(tǒng)用C# Winform而不是網(wǎng)頁拿到這個基于C# Winform窗體的酒店管理系統(tǒng).zip別急著解壓跑起來。它對應(yīng)的是很多中小賓館、民宿仍在用的桌面管理模式前臺電腦上裝一個exe連一份本地數(shù)據(jù)庫客人來了開房、退房、結(jié)賬全部操作都發(fā)生在Winform窗體里。相比網(wǎng)頁版它不用部署Web服務(wù)器斷網(wǎng)也能收銀配合小票打印機、身份證讀卡器、門鎖管理這些本地外設(shè)反而更順手。如果你是剛接觸C#的開發(fā)者或者正在做課程設(shè)計、接手這類小系統(tǒng)這篇就把從代碼結(jié)構(gòu)到運行流程的落地路徑講清楚。2. 把系統(tǒng)拆成模塊與數(shù)據(jù)表先設(shè)計再寫窗體2.1 酒店管理系統(tǒng)的核心模塊與數(shù)據(jù)模型拿到zip后我一般不會先找登錄窗體而是先打開數(shù)據(jù)庫腳本或者 .mdb 文件。Winform窗體只是皮真正的業(yè)務(wù)邏輯在表結(jié)構(gòu)和SQL里。酒店管理系統(tǒng)繞不開這幾個模塊房型和房間、入住登記、退房結(jié)賬、客戶檔案、系統(tǒng)用戶外加一份訂單/消費明細。表設(shè)計花不了多少時間但決定了后面寫窗體順不順利。常見的核心表如下表名作用關(guān)鍵字段SysUser系統(tǒng)登錄用戶UserId, UserName, Password, RoleIdRoomType房型定義TypeId, TypeName, Price, BedCountRoom房間信息RoomId, RoomNo, Floor, TypeId, StatusCustomer客戶檔案CustomerId, CustomerName, Phone, IdCardOrders入住訂單OrderId, OrderNo, RoomId, CheckInTime, CheckOutTime, Amount, Deposit, Status, OperatorIdOrderItem消費明細ItemId, OrderId, ItemName, Price, Quantity其中Room.Status建議用int枚舉0空閑、1入住、2臟房、3維修、4預(yù)訂。把狀態(tài)存在單獨一張表也可以但是會多一次join小系統(tǒng)用int最省事。Orders.Status則建議用0進行中、1已結(jié)賬、2已取消。要注意的是Amount不在入住時寫死而是退房時按實際消費重新計算避免后來加床、加餐改不了。下面這段建表SQL是精簡版實際還可以加入樓層、房間備注等字段CREATE TABLE RoomType( TypeId INT IDENTITY(1,1) PRIMARY KEY, TypeName NVARCHAR(20) NOT NULL, Price DECIMAL(10,2) NOT NULL DEFAULT 0, BedCount INT DEFAULT 1 ); CREATE TABLE Room( RoomId INT IDENTITY(1,1) PRIMARY KEY, RoomNo NVARCHAR(10) NOT NULL UNIQUE, Floor INT, TypeId INT FOREIGN KEY REFERENCES RoomType(TypeId), Status INT NOT NULL DEFAULT 0 ); CREATE TABLE Orders( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(20) NOT NULL, RoomId INT, CustomerName NVARCHAR(50), CheckInTime DATETIME, CheckOutTime DATETIME, Amount DECIMAL(10,2) DEFAULT 0, Deposit DECIMAL(10,2) DEFAULT 0, Status INT DEFAULT 0, OperatorId INT );說明RoomNo用了UNIQUE房間號不能重復(fù)。Price放RoomType而不是Room因為同房型統(tǒng)一價格如果要做淡旺季調(diào)價就需要再拆一張PriceRule表。數(shù)據(jù)庫腳本里最好再補一段初始化數(shù)據(jù)把默認(rèn)管理員賬號和兩三個房型先插進去否則程序第一次啟動連登錄都進不去。管理員密碼不要直接寫明文可以在C#初始化工具里生成一次MD5再寫入。2.2 為什么是C/S架構(gòu)選Winform不選網(wǎng)頁的三個硬理由很多新手會問“都什么年代了還學(xué)Winform”這里不想爭論技術(shù)新老只說酒店前臺這個場景桌面程序有三個Web頁面很難替代的優(yōu)勢。第一是本地硬件訪問。小票打印機、身份證讀卡器、門鎖系統(tǒng)、現(xiàn)金錢箱這些設(shè)備大多提供Windows驅(qū)動或串口接口Winform通過引用廠商DLL或者直接操作串口就能連上瀏覽器反而受權(quán)限沙箱限制經(jīng)常要自己封裝服務(wù)端再走WebSocket。第二是離線可用。酒店前臺不能因為外網(wǎng)斷了就結(jié)不了賬桌面程序只要數(shù)據(jù)庫在局域網(wǎng)或本機斷網(wǎng)照常運行。第三是開發(fā)效率。有經(jīng)驗的開發(fā)者用Winform拖拖拽拽幾天就能跑通一個能演示、能交付的版本改起來也很直觀。網(wǎng)頁版雖然好打包但需要處理路由、權(quán)限、部署環(huán)境成本明顯更高。當(dāng)然也有邊界如果要做多門店連鎖、小程序訂房、在線支付Winform就免為其難了。那種場景應(yīng)該改用Web后端加小程序前端Winform只留給門店收銀端。認(rèn)識到這個邊界才不會被“老技術(shù)”三個字帶偏。2.3 數(shù)據(jù)庫選型與項目分層數(shù)據(jù)庫選型方面我看zip的App.config或引用就能猜個大概。如果connectionStrings里providerName是System.Data.SqlClient就是SQL Server如果用了System.Data.OleDb多半是Access。SQL Server部署時難度略高但穩(wěn)定Access勝在免安裝但多用戶并發(fā)能力和32/64位驅(qū)動經(jīng)常讓人翻車。如果這個項目是要長期維護的我一般會建議改成 SQLServer Express 或者 SQLiteSQLite可以用NuGet的System.Data.SQLite一個DLL搞定學(xué)習(xí)項目完全夠用。工程結(jié)構(gòu)上常見的做法是模擬三層Models放實體類DAL放SQLHelper和各個表的數(shù)據(jù)操作BLL放業(yè)務(wù)規(guī)則UI層放Winform窗體。這樣換數(shù)據(jù)庫或改結(jié)賬規(guī)則時不用一個一個窗體去翻。一個最簡單的分層可以這樣Solution ├─ Hotel.Model ├─ Hotel.DAL ├─ Hotel.BLL └─ Hotel.Winform有些壓縮包里可能把所有類堆在同一個項目也能跑但后續(xù)改起來很痛苦。我會先把入口Form和DAL分離至少保證窗體里不出現(xiàn)“select * from”這種裸SQL。這一步做好后面的坑少一半。3. 用Winform把核心流程跑通登錄、開房、退房最小實現(xiàn)3.1 連接配置與SQLHelper封裝在Winform里最基礎(chǔ)的是連數(shù)據(jù)庫。不要在窗體里到處寫SqlConnection然后開關(guān)連接那樣會出現(xiàn)連接泄漏和代碼重復(fù)。我習(xí)慣先寫App.config再封裝一個靜態(tài)SQLHelper。App.config里這樣配configuration connectionStrings add nameHotelDb connectionStringData Source.\SQLEXPRESS;Initial CatalogHotelDb;Integrated SecurityTrue; providerNameSystem.Data.SqlClient / /connectionStrings /configuration然后寫一個簡單的SQLHelperpublic static class SqlHelper { private static readonly string _connStr ConfigurationManager.ConnectionStrings[HotelDb].ConnectionString; public static DataTable ExecuteQuery(string sql, params SqlParameter[] parms) { using (var conn new SqlConnection(_connStr)) using (var cmd new SqlCommand(sql, conn)) { if (parms ! null) cmd.Parameters.AddRange(parms); var dt new DataTable(); conn.Open(); new SqlDataAdapter(cmd).Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, params SqlParameter[] parms) { using (var conn new SqlConnection(_connStr)) using (var cmd new SqlCommand(sql, conn)) { if (parms ! null) cmd.Parameters.AddRange(parms); conn.Open(); return cmd.ExecuteNonQuery(); } } }邏輯說明ExecuteQuery用于查詢并返回DataTableExecuteNonQuery用于增刪改using確保連接和命令自動釋放不需要頻繁打開關(guān)閉連接。參數(shù)用SqlParameter數(shù)組能防SQL注入尤其是客戶名、備注這種字符串字段。使用該類需要在項目里引用System.Configuration并在頂部using System.Data.SqlClient。3.2 登錄窗體驗證用戶并傳遞角色登錄是第一個要跑的流程。點擊登錄按鈕后做三件事校驗空值、查用戶表、進入主窗體。密碼我這里用MD5轉(zhuǎn)一下雖然還不是絕對安全但至少不會在表里明文存密碼。private void btnLogin_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtUser.Text) || string.IsNullOrWhiteSpace(txtPwd.Text)) { MessageBox.Show(用戶名和密碼不能為空); return; } string sql SELECT UserId, UserName, RoleId FROM SysUser WHERE UserNameun AND Passwordpwd; var dt SqlHelper.ExecuteQuery(sql, new SqlParameter(un, txtUser.Text.Trim()), new SqlParameter(pwd, GetMd5(txtPwd.Text))); if (dt.Rows.Count 0) { MessageBox.Show(用戶名或密碼錯誤); return; } CurrentUser.UserId Convert.ToInt32(dt.Rows[0][UserId]); CurrentUser.RoleId Convert.ToInt32(dt.Rows[0][RoleId]); var main new MainForm(); main.Show(); this.Hide(); }參數(shù)說明un和pwd分別綁定用戶名和密碼避免了在SQL里拼字符串。GetMd5是寫在工具類里的方法用SHA256也可以。CurrentUser我一般設(shè)計成全局靜態(tài)類保存UserId和RoleId后續(xù)窗體直接用省得在主窗體構(gòu)造函數(shù)里層層傳遞。3.3 開房登記事務(wù)保證訂單和房間狀態(tài)一起變開房不只是往Orders里插一條記錄還要把對應(yīng)房間狀態(tài)改成“入住”。這兩個操作必須在一個事務(wù)里否則會出現(xiàn)訂單已建但房間還是空閑的矛盾狀態(tài)。下面是一個能直接抄的實現(xiàn)private void btnCheckIn_Click(object sender, EventArgs e) { string connStr ConfigurationManager.ConnectionStrings[HotelDb].ConnectionString; using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { var cmd conn.CreateCommand(); cmd.Transaction tran; cmd.CommandText INSERT INTO Orders(RoomId, CustomerName, CheckInTime, Deposit, Status, OperatorId) VALUES(roomId, name, time, deposit, 0, opId); SELECT SCOPE_IDENTITY();; cmd.Parameters.Clear(); cmd.Parameters.AddWithValue(roomId, comboRoom.SelectedValue); cmd.Parameters.AddWithValue(name, txtCustomer.Text.Trim()); cmd.Parameters.AddWithValue(time, DateTime.Now); cmd.Parameters.AddWithValue(deposit, numDeposit.Value); cmd.Parameters.AddWithValue(opId, CurrentUser.UserId); int orderId Convert.ToInt32(cmd.ExecuteScalar()); cmd.CommandText UPDATE Room SET Status1 WHERE RoomIdroomId; cmd.Parameters.Clear(); cmd.Parameters.AddWithValue(roomId, comboRoom.SelectedValue); cmd.ExecuteNonQuery(); tran.Commit(); MessageBox.Show($入住成功訂單號 {orderId}); LoadRooms(); } catch (Exception ex) { tran.Rollback(); MessageBox.Show(操作失敗 ex.Message); } } } }邏輯說明先用ExecuteScalar取新訂單的ID再更新房間狀態(tài)。事務(wù)保證兩步要么全成功要么全回滾。AddWithValue的寫法比較簡潔但如果字段是datetime或decimal我建議顯式創(chuàng)建SqlParameter并指定DbType避免隱式轉(zhuǎn)換把時間截掉或把金額精度弄丟。3.4 退房結(jié)賬計算金額并置房間為臟房退房則是反過來更新訂單的離店時間和金額把房間狀態(tài)從“入住”改成“臟房”。金額計算可以按小時也可以按天下面給一個按小時乘以房型價格的思路string sql SELECT o.CheckInTime, rt.Price FROM Orders o JOIN Room r ON o.RoomId r.RoomId JOIN RoomType rt ON r.TypeId rt.TypeId WHERE o.OrderIdorderId; var dt SqlHelper.ExecuteQuery(sql, new SqlParameter(orderId, orderId)); double hours (DateTime.Now - Convert.ToDateTime(dt.Rows[0][CheckInTime])).TotalHours; decimal price Convert.ToDecimal(dt.Rows[0][Price]); decimal amount (decimal)Math.Ceiling(hours) * price;這樣計算出來的金額是最簡模型。真實酒店通常會有“鐘點房頭兩個小時xx元之后每半小時xx元”的階梯規(guī)則那部分不是Winform的難點而是業(yè)務(wù)規(guī)則建議單獨放到BLL層寫一個CalculateFee方法別在按鈕Click里堆。退房還需要把房間狀態(tài)更新為2臟房。這一步和更新訂單用同一個事務(wù)做法和開房一致。到這里最小閉環(huán)就通了登錄-開房-退房-查房。4. 把界面從“能跑”做到“能交付”美化、布局與狀態(tài)反饋4.1 Winform視覺升級第一步去掉默認(rèn)控件味道默認(rèn)的Winform窗體看起來像不穿衣服的控件排列交付給酒店老板很難過關(guān)。我自己做這種項目時界面美化寧可少加特效也要先把配色和布局統(tǒng)一。最常見的就是把Button的FlatStyle設(shè)為FlatBackColor換成深藍色或青色ForeColor設(shè)為White。然后在窗體上放一個左側(cè)PanelDockLeft里面垂直排列功能按鈕點擊按鈕時右側(cè)內(nèi)容區(qū)切換不同的UserControl或窗體。很多資料里會提“菜單折疊箭頭怎么繪制”其實不需要去繼承復(fù)雜的控件。如果你想要一個可折疊的側(cè)邊欄只需要一個Button在它的Paint事件里畫一個向右或向下的三角形箭頭private void btnSidebar_Paint(object sender, PaintEventArgs e) { var btn sender as Button; if (btn null) return; Point[] pts; if (menuCollapsed) { pts new Point[] { new Point(btn.Width - 20, btn.Height / 2 - 4), new Point(btn.Width - 12, btn.Height / 2 4), new Point(btn.Width - 20, btn.Height / 2 4) }; } else { pts new Point[] { new Point(btn.Width - 20, btn.Height / 2 - 4), new Point(btn.Width - 12, btn.Height / 2), new Point(btn.Width - 20, btn.Height / 2 4) }; } e.Graphics.DrawLines(Pens.White, pts); }說明Paint事件里根據(jù)一個bool變量切換三角形的頂點。DrawLines只畫箭頭輪廓如果希望三角形成填充樣式用Graphics.DrawPolygon并設(shè)置一個SolidBrush也可以。菜單折疊的本質(zhì)不是動畫只是切換子按鈕的Visible和刷新箭頭方向別把簡單問題復(fù)雜化。4.2 透明窗體、DPI縮放與高分屏“透明窗體”是Winform里一個很吸引人但容易翻車的話題。如果只是讓整個窗體半透明直接設(shè)置Form.Opacity0.9即可這個屬性對頂層窗體有效。但如果你期望讓某個Panel透明、露出后面的桌面壁紙Winform原生做不到需要調(diào)用DwmEnableBlurBehindWindow等系統(tǒng)API有性能和平臺版本限制一般不建議放在酒店管理系統(tǒng)里。高分屏問題倒是必須處理的。現(xiàn)在很多前臺電腦是4K顯示器如果不做DPI感知窗體上的文字會發(fā)虛、控件會錯位。常見做法是兩處在Program.cs里調(diào)用SetProcessDPIAware把每個窗體的AutoScaleMode設(shè)為Dpi或Font。下面是最小啟用代碼[STAThread] static void Main() { if (Environment.OSVersion.Version.Major 6) SetProcessDPIAware(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new LoginForm()); } [DllImport(user32.dll)] private static extern bool SetProcessDPIAware();說明SetProcessDPIAware必須在Application.Run之前調(diào)用否則整個進程的DPI縮放已經(jīng)按系統(tǒng)默認(rèn)處理控件坐標(biāo)會被二次縮放。另外還要盡量避免在窗體構(gòu)造函數(shù)里直接給控件設(shè)死Width和Height多用Anchor、Dock和TableLayoutPanel這樣高分屏下才會跟隨縮放。4.3 StatusStrip、進度條與后臺任務(wù)前臺系統(tǒng)不能讓界面卡死。比如導(dǎo)出今天的入住報表數(shù)據(jù)量不大也可能會耗時一兩秒如果把查詢直接放在UI線程窗體就會轉(zhuǎn)圈圈。我會用StatusStrip放一個ToolStripStatusLabel顯示當(dāng)前用戶和連接狀態(tài)再用ToolStripProgressBar配合BackgroundWorker顯示進度。private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { for (int i 0; i 100; i) { Thread.Sleep(20); backgroundWorker.ReportProgress(i); } } private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { toolStripProgressBar.Value e.ProgressPercentage; }邏輯說明BackgroundWorker的ProgressChanged回調(diào)運行在UI線程所以可以直接更新進度條。如果你直接用普通Task在回調(diào)里更新控件必須用Invoke不然就是第5章要講的跨線程異常。小項目里Task一開始很爽但遇到控件交互時BackgroundWorker的“少樣板代碼”優(yōu)勢就出來了。5. Winform酒店管理系統(tǒng)的常見翻車點與排查清單這一章來點直接的踩坑記錄。下面幾條都是我見過或?qū)嶋H踩過的每條按現(xiàn)象、原因、解決的順序?qū)憽?.1 部署到其他電腦提示“找不到服務(wù)器/連接未打開”現(xiàn)象程序在自己的開發(fā)機跑得好好的拷貝到前臺電腦后點擊登錄就報“無法打開連接 SQL Server”或“未找到服務(wù)器”。有些運氣好一點的能進系統(tǒng)但一點查詢就崩。原因連接字符串里的Data Source寫的是開發(fā)機名或“.\SQLEXPRESS”目標(biāo)電腦的SQL Server實例名不一樣或者目標(biāo)機器根本沒裝SQL Server只把exe和數(shù)據(jù)庫文件拷過去了。這不是程序問題是運行環(huán)境沒對齊。解決最簡單的方案是放棄拖拖拉拉找服務(wù)名改成SQLite數(shù)據(jù)庫或者用用戶實例。如果堅持用SQL Server那就把部署步驟寫明白先在目標(biāo)機裝SQL Server Express再執(zhí)行腳本建庫再改App.config里的Data Source。連接字符串我通常會在交付時留一個占位參數(shù)并附上一句注釋“這里改成目標(biāo)機器的實例名”。另外一定要檢查SQL Server的TCP/IP協(xié)議是否啟用很多開發(fā)機上用命名管道能連換到局域網(wǎng)就失敗就是因為TCP/IP沒開。5.2 后臺線程直接更新UI控件就拋異常現(xiàn)象在BackgroundWorker或Task里執(zhí)行 toolStripStatusLabel1.Text 完成界面瞬間拋出“正在創(chuàng)建窗口的線程上不能調(diào)用其他線程對此控件進行操作”。原因Winform控件不是線程安全的UI控件只能在主UI線程更新。使用Task或者Thread的時候回調(diào)所在的線程是線程池線程沒有訪問控件的許可。解決要么改用BackgroundWorker用ReportProgress和ProgressChanged更新控件要么手動Invoke。下面是手動升級的式子this.Invoke(new Action(() { toolStripStatusLabel1.Text 查詢完成; }));說明Invoke是同步等待主線程處理BeginInvoke是異步投遞。在連續(xù)更新進度條場景用BeginInvoke更順但要注意不要大批量連續(xù)調(diào)用否則界面反而跟不上。5.3 DataGridView 數(shù)據(jù)源更新后界面還是舊數(shù)據(jù)現(xiàn)象在內(nèi)存里修改了一個房型對象的價格重新查詢數(shù)據(jù)庫發(fā)現(xiàn)數(shù)據(jù)變了可DataGridView卻顯示舊值必須重新設(shè)置DataSource才變。原因直接給DataGridView的DataSource賦了一個List 而List 不通知界面自己變了。只有在重新綁定時才會刷新。解決如果只是內(nèi)存修改把列表換成BindingList 如果是數(shù)據(jù)庫操作后再刷新就重新查一次DataTable并重新賦值。重新賦值外加一句 bindingSource1.ResetBindings(false) 能避免列表數(shù)據(jù)變化后界面不動。更穩(wěn)妥的做法是每次增刪改后都調(diào)用同一個LoadRooms()方法讓窗體回到“查詢-綁定”這個統(tǒng)一入口。5.4 按日期查詢訂單數(shù)據(jù)明明有卻查不到現(xiàn)象用戶選擇“今天”作為查詢條件列表是空的。但直接在數(shù)據(jù)庫看數(shù)據(jù)今天的訂單明明存在。原因DateTimePicker生成的日期是今天的00:00:00SQL里如果用“CheckInTime begin AND CheckInTime end”那么end等于今天零點剛好把所有今天的訂單排除掉如果拼接字符串日期格式也可能因為區(qū)域設(shè)置造成錯配。解決結(jié)束時間改用“第二天0點”用小于號而不是小于等于。常用寫法是DateTime endDate dateTimePicker2.Value.Date.AddDays(1); string sql SELECT * FROM Orders WHERE CheckInTime begin AND CheckInTime end; cmd.Parameters.AddWithValue(begin, dateTimePicker1.Value.Date); cmd.Parameters.AddWithValue(end, endDate);說明加一個“第二天零點”是查詢?nèi)掌诜秶畛R姷牧?xí)慣能避免用DATETIME精度導(dǎo)致的90%邊界漏數(shù)據(jù)。如果你還要查小時級別再往前回?fù)芤幻搿?.5 中文亂碼代碼拷到新電腦變成問號現(xiàn)象同一份源碼從一臺電腦拷貝到另一臺打開后SQL腳本里的中文注釋和插入語句全變成問號窗體標(biāo)題或DataGridView列名在界面上也顯示成亂碼。原因文件編碼不一致。Visual Studio保存的.cs文件默認(rèn)可能是帶簽名的UTF-8但如果曾經(jīng)用記事本或舊版工具編輯并存成ANSI再放到新電腦上就可能亂。SQL Server的數(shù)據(jù)庫排序規(guī)則如果不支持中文比如默認(rèn)的SQL_Latin1_General_CP1_CI_AS插入中文也會變問號。解決統(tǒng)一把.cs、.sql和App.config保存為“帶簽名的UTF-8”。SQL Server建庫時指定排序規(guī)則為Chinese_PRC_CI_AS或者建表時用NVARCHARN。這個坑很難查因為代碼邏輯完全沒變只是字符集導(dǎo)致。遇到亂碼我第一步會看數(shù)據(jù)庫排序規(guī)則和文件編碼而不是改代碼。6. 驗證系統(tǒng)與后續(xù)擴展一臺電腦上把驗收流程走完最后這一步不是跑通一個登錄就算結(jié)束而是把狀態(tài)流轉(zhuǎn)在空白環(huán)境下完整走一遍。我通常會把下面這張流程表過一遍步驟操作預(yù)期結(jié)果1在全新環(huán)境執(zhí)行數(shù)據(jù)庫腳本無報錯表結(jié)構(gòu)齊全2登錄一個不存在的用戶拒絕進入3登錄管理員新增房型和房間房間列表出現(xiàn)4選擇空閑房間客戶入住房間變?nèi)胱∮唵慰刹?退房結(jié)賬金額正確房間變臟房6切換另一操作員登錄看不到與自己無關(guān)的訂單或按鈕灰化驗證時多留意兩個細節(jié)一是在退房后立刻再次入住同一個房間看狀態(tài)是否正常從“臟房”變成“空閑”再變成“入住”二是把系統(tǒng)時間改到昨天創(chuàng)建一筆訂單再退房看跨天計算小時數(shù)對不對。這兩個點能過濾掉80%的狀態(tài)機bug。后續(xù)擴展方面如果這套代碼要真正投入營業(yè)我會優(yōu)先做三件事打印小票PrintDocument System.Drawing.Printing幾行代碼就能把賬單內(nèi)容發(fā)給熱敏打印機、導(dǎo)出Excel報表用ClosedXML或NPOI避免Office COM依賴、按角色控制菜單在MainForm加載時根據(jù)CurrentUser.RoleId隱藏“系統(tǒng)設(shè)置”等按鈕。我自己做這類項目時最深刻的教訓(xùn)是在交付前把數(shù)據(jù)庫腳本在空白電腦上重新跑一遍而不要用開發(fā)環(huán)境的數(shù)據(jù)庫“演示成功”。因為換一臺機器后連接字符串、SQL Server實例、排序規(guī)則都可能翻車。把這些寫進一個readme比在群里發(fā)聊天記錄修復(fù)問題要省太多時間。希望幫到你。本文還有配套的精品資源點擊獲取