物流系統(tǒng):ADO.NET分層實(shí)踐與WinForm性能優(yōu)化)
簡(jiǎn)介這是一套基于C#開(kāi)發(fā)的物流信息管理系統(tǒng)完整源碼工程面向計(jì)算機(jī)專(zhuān)業(yè)本科生及初學(xué)者適用于課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或期末大作業(yè)實(shí)踐。系統(tǒng)采用分層架構(gòu)MODEL/BLL/DAL包含123個(gè)C#核心業(yè)務(wù)邏輯文件、55個(gè)Vue前端頁(yè)面組件、52個(gè)依賴(lài)DLL庫(kù)及配套數(shù)據(jù)庫(kù)文件mdf/ldf/sql支持基礎(chǔ)物流單據(jù)管理、客戶(hù)信息維護(hù)、運(yùn)單跟蹤等核心功能模塊。壓縮包共415個(gè)文件總大小14.02MB涵蓋項(xiàng)目解決方案sln、工程配置csproj/props、編譯緩存cache/pdb、靜態(tài)資源png/svg/js/json及開(kāi)發(fā)配置文件editorconfig/.babelrc/.gitignore目錄結(jié)構(gòu)規(guī)范便于理解MVC模式與前后端協(xié)同開(kāi)發(fā)流程。已有1332人學(xué)習(xí)下載讀者可直接導(dǎo)入Visual Studio運(yùn)行調(diào)試快速掌握C#桌面應(yīng)用開(kāi)發(fā)、SQL Server數(shù)據(jù)庫(kù)設(shè)計(jì)與簡(jiǎn)單Web前端集成的關(guān)鍵技能。1. 這不是又一個(gè)“增刪改查”DemoC#物流信息管理系統(tǒng)源碼的真實(shí)價(jià)值在于三層架構(gòu)落地與數(shù)據(jù)庫(kù)耦合控制很多同學(xué)拿到“C#物流信息管理系統(tǒng)源碼數(shù)據(jù)庫(kù)”壓縮包第一反應(yīng)是解壓、雙擊.sln、F5運(yùn)行——結(jié)果卡在登錄頁(yè)報(bào)SqlException: 無(wú)法打開(kāi)登錄所請(qǐng)求的數(shù)據(jù)庫(kù)或者主界面空白、訂單列表始終顯示0條。這不是代碼寫(xiě)錯(cuò)了而是這套源碼刻意保留了課程設(shè)計(jì)最核心的教學(xué)斷點(diǎn)它用標(biāo)準(zhǔn)的Logistics.BLL、Logistics.DAL、Logistics.MODEL分層結(jié)構(gòu)把數(shù)據(jù)訪問(wèn)邏輯與業(yè)務(wù)邏輯的邊界切得足夠清晰但又沒(méi)封裝到“開(kāi)箱即用”的程度。它不依賴(lài)Entity Framework Code First自動(dòng)建庫(kù)也不用Dapper簡(jiǎn)化SQL拼接而是用原生ADO.NET 存儲(chǔ)過(guò)程或硬編碼SQL直連SQL Server強(qiáng)制你手動(dòng)配置連接字符串、校驗(yàn)數(shù)據(jù)庫(kù)表結(jié)構(gòu)、理解事務(wù)邊界。適合需要交課程設(shè)計(jì)報(bào)告、答辯時(shí)能講清“為什么用三層不用單文件”“DAL層如何屏蔽SQL Server差異”“BLL層怎么實(shí)現(xiàn)訂單狀態(tài)機(jī)流轉(zhuǎn)”的本科生和剛轉(zhuǎn)C#的初級(jí)開(kāi)發(fā)者。如果你正被“WinForm界面卡頓”“DataGridView加載1000條物流記錄延遲明顯”“修改發(fā)貨狀態(tài)后UI不刷新”這類(lèi)問(wèn)題卡住這套源碼里藏著可調(diào)試、可打斷點(diǎn)、可逐行跟蹤的完整鏈路。2. 三層架構(gòu)拆解從Logistics_information.csproj到Logistics.DAL中ConnectionManager的職責(zé)劃分2.1 項(xiàng)目結(jié)構(gòu)解析為什么.csproj文件名暗示了編譯依賴(lài)順序解壓后看到的.csproj文件并非隨意命名Logistics_information.csproj是主啟動(dòng)項(xiàng)目WinForm窗體它引用Logistics.BLLLogistics.BLL再引用Logistics.DAL和Logistics.MODEL。這種顯式依賴(lài)關(guān)系決定了編譯順序和運(yùn)行時(shí)加載路徑。關(guān)鍵點(diǎn)在于Logistics_information.csproj不直接引用System.Data.SqlClient所有數(shù)據(jù)庫(kù)操作必須經(jīng)由Logistics.DAL暴露的接口。查看Logistics.DAL.csproj內(nèi)容會(huì)發(fā)現(xiàn)它明確包含ItemGroup Reference IncludeSystem.Data / Reference IncludeSystem.Xml / /ItemGroup而Logistics.BLL.csproj中則沒(méi)有這些引用——這正是分層意圖BLL層只處理“訂單是否超時(shí)”“庫(kù)存是否充足”等業(yè)務(wù)規(guī)則不碰Connection、Command、DataReader。若你在BLL層寫(xiě)了new SqlConnection()說(shuō)明架構(gòu)已破壞。驗(yàn)證方法在BLL項(xiàng)目中嘗試輸入SqlConnectionVS會(huì)報(bào)錯(cuò)“未找到類(lèi)型或命名空間”除非你手動(dòng)添加引用——這恰恰是設(shè)計(jì)者設(shè)置的第一道檢查點(diǎn)。2.2 MODEL層實(shí)體類(lèi)從數(shù)據(jù)庫(kù)表到C#對(duì)象的映射契約Logistics.MODEL目錄下存放所有實(shí)體類(lèi)如OrderInfo.cs、CargoInfo.cs。以O(shè)rderInfo為例其屬性與數(shù)據(jù)庫(kù)Orders表字段嚴(yán)格對(duì)應(yīng)namespace Logistics.MODEL { public class OrderInfo { public int OrderID { get; set; } // 對(duì)應(yīng) Orders.OrderID (INT, PK) public string OrderNo { get; set; } // 對(duì)應(yīng) Orders.OrderNo (NVARCHAR(20)) public DateTime CreateTime { get; set; } // 對(duì)應(yīng) Orders.CreateTime (DATETIME) public int Status { get; set; } // 對(duì)應(yīng) Orders.Status (TINYINT)1待發(fā)貨,2運(yùn)輸中,3已簽收 // 注意無(wú)public SqlConnection Connection屬性MODEL只承載數(shù)據(jù)不承載行為 } }提示MODEL層禁止出現(xiàn)任何數(shù)據(jù)庫(kù)操作方法如SaveToDB()、禁止繼承IDataRecord、禁止包含DataTable字段。它的唯一職責(zé)是作為BLL與DAL之間傳遞數(shù)據(jù)的“信封”。若你在MODEL中看到public void UpdateStatus(int newStatus)這是典型的設(shè)計(jì)污染需重構(gòu)為BLL層的OrderService.ChangeStatus(OrderInfo order, int newStatus)。2.3 DAL層核心ConnectionManager與SqlHelper的協(xié)同機(jī)制Logistics.DAL中最關(guān)鍵的不是OrderDAL.cs而是ConnectionManager.cs和SqlHelper.cs。前者管理連接字符串后者封裝CRUD模板。查看ConnectionManager.cspublic static class ConnectionManager { // 從App.config讀取非硬編碼 public static string GetConnectionString() { return ConfigurationManager.ConnectionStrings[LogisticsDB].ConnectionString; // 注意此處要求App.config中存在connectionStringsadd nameLogisticsDB ...//connectionStrings } }而SqlHelper.cs提供泛型執(zhí)行方法public static class SqlHelper { public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (var conn new SqlConnection(ConnectionManager.GetConnectionString())) { using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); // 返回影響行數(shù)用于判斷插入/更新是否成功 } } } }2.3.1 參數(shù)化查詢(xún)防注入的關(guān)鍵實(shí)踐在OrderDAL.UpdateStatus()中你會(huì)看到string sql UPDATE Orders SET Status status WHERE OrderID id; int rows SqlHelper.ExecuteNonQuery(sql, new SqlParameter(status, status), new SqlParameter(id, orderID)); if (rows 0) throw new InvalidOperationException($未找到OrderID{orderID}的訂單);這里status和id是參數(shù)占位符而非字符串拼接。若誤寫(xiě)成UPDATE Orders SET Status status WHERE OrderID orderID則面臨SQL注入風(fēng)險(xiǎn)如傳入status3; DROP TABLE Orders--。參數(shù)化查詢(xún)強(qiáng)制SQL語(yǔ)句結(jié)構(gòu)與數(shù)據(jù)分離是課程設(shè)計(jì)答辯時(shí)必須能解釋清楚的安全基線。2.4 BLL層業(yè)務(wù)編排OrderService中的狀態(tài)流轉(zhuǎn)與事務(wù)控制Logistics.BLL中的OrderService.cs是業(yè)務(wù)邏輯中樞。以“確認(rèn)收貨”為例public class OrderService { private readonly OrderDAL _orderDAL new OrderDAL(); public bool ConfirmReceipt(int orderID, string operatorID) { // 開(kāi)啟數(shù)據(jù)庫(kù)事務(wù)確保狀態(tài)更新與操作日志寫(xiě)入原子性 using (var transaction SqlHelper.BeginTransaction()) { try { // 1. 檢查當(dāng)前狀態(tài)是否允許收貨只能從運(yùn)輸中變?yōu)橐押炇?var currentStatus _orderDAL.GetStatus(orderID); if (currentStatus ! 2) // 2運(yùn)輸中 throw new BusinessException($訂單{orderID}當(dāng)前狀態(tài){currentStatus}不可執(zhí)行收貨操作); // 2. 更新訂單狀態(tài) _orderDAL.UpdateStatus(orderID, 3, transaction); // 3已簽收 // 3. 寫(xiě)入操作日志 var logDAL new LogDAL(); logDAL.Insert(new LogInfo { OrderID orderID, OperatorID operatorID, Action ConfirmReceipt, TimeStamp DateTime.Now }, transaction); transaction.Commit(); // 全部成功才提交 return true; } catch { transaction.Rollback(); // 任一失敗則回滾 throw; } } } }注意SqlHelper.BeginTransaction()返回SqlTransaction對(duì)象必須顯式傳給所有DAL方法如UpdateStatus(..., transaction)。若遺漏各DAL操作將使用獨(dú)立連接無(wú)法保證事務(wù)一致性。這是課程設(shè)計(jì)中最高頻的扣分點(diǎn)——學(xué)生常忘記在DAL方法簽名中增加SqlTransaction transaction null可選參數(shù)。3. 數(shù)據(jù)庫(kù)部署與連接配置從.mdf文件到App.config的完整鏈路3.1 數(shù)據(jù)庫(kù)文件定位與附加策略壓縮包中通常包含.mdf主數(shù)據(jù)文件和.ldf日志文件如LogisticsDB.mdf。直接雙擊.mdf無(wú)法打開(kāi)必須通過(guò)SQL Server Management StudioSSMS附加打開(kāi)SSMS連接本地實(shí)例如.\SQLEXPRESS右鍵【數(shù)據(jù)庫(kù)】→【附加】→【添加】→選擇LogisticsDB.mdfSSMS自動(dòng)識(shí)別.ldf路徑若提示日志文件丟失勾選【自動(dòng)增長(zhǎng)】并指定新日志路徑附加后數(shù)據(jù)庫(kù)名為L(zhǎng)ogisticsDB與連接字符串中名稱(chēng)一致。驗(yàn)證表結(jié)構(gòu)是否存在Orders、Cargo、Users等表并檢查Orders.Status列是否為T(mén)INYINT類(lèi)型——若為VARCHAR則DAL層UpdateStatus()中status參數(shù)類(lèi)型需同步改為SqlDbType.VarChar否則拋出類(lèi)型不匹配異常。3.2 App.config連接字符串配置詳解主項(xiàng)目Logistics_information.csproj的App.config文件中connectionStrings節(jié)必須與數(shù)據(jù)庫(kù)實(shí)例匹配configuration connectionStrings !-- 方案1本地SQL Server Express -- add nameLogisticsDB connectionStringData Source.\SQLEXPRESS;AttachDbFilename|DataDirectory|\LogisticsDB.mdf;Integrated SecurityTrue;User InstanceTrue providerNameSystem.Data.SqlClient / !-- 方案2命名實(shí)例如安裝時(shí)指定實(shí)例名為MSSQLSERVER -- !-- add nameLogisticsDB connectionStringData SourceYOUR-PC-NAME\MSSQLSERVER;Initial CatalogLogisticsDB;Integrated SecurityTrue providerNameSystem.Data.SqlClient / -- /connectionStrings /configuration關(guān)鍵參數(shù)說(shuō)明Data Source.\SQLEXPRESS.\表示本地機(jī)器SQLEXPRESS是SQL Server默認(rèn)免費(fèi)實(shí)例名。若安裝的是完整版SQL Server實(shí)例名可能是MSSQLSERVER此時(shí)Data Source.即可AttachDbFilename|DataDirectory|\LogisticsDB.mdf|DataDirectory|是占位符運(yùn)行時(shí)被替換為應(yīng)用程序目錄如bin\Debug\。確保.mdf文件復(fù)制到該目錄下或修改路徑為絕對(duì)路徑如C:\LogisticsDB.mdfIntegrated SecurityTrue使用Windows當(dāng)前用戶(hù)身份登錄無(wú)需用戶(hù)名密碼。若SQL Server配置為混合模式可改為User IDsa;Passwordyour_password3.3 連接測(cè)試用獨(dú)立代碼驗(yàn)證數(shù)據(jù)庫(kù)可達(dá)性在Logistics_information項(xiàng)目中新建TestConnection.cs粘貼以下代碼驗(yàn)證連接using System; using System.Data.SqlClient; class TestConnection { static void Main() { string connStr System.Configuration.ConfigurationManager .ConnectionStrings[LogisticsDB].ConnectionString; try { using (var conn new SqlConnection(connStr)) { conn.Open(); Console.WriteLine(? 數(shù)據(jù)庫(kù)連接成功當(dāng)前數(shù)據(jù)庫(kù) conn.Database); // 查詢(xún)Orders表記錄數(shù) using (var cmd new SqlCommand(SELECT COUNT(*) FROM Orders, conn)) { int count (int)cmd.ExecuteScalar(); Console.WriteLine($? Orders表共{count}條記錄); } } } catch (SqlException ex) { Console.WriteLine($? SQL錯(cuò)誤{ex.Message}); Console.WriteLine($錯(cuò)誤號(hào){ex.Number}); // 例如18456登錄失敗53服務(wù)器不存在 } catch (Exception ex) { Console.WriteLine($? 其他錯(cuò)誤{ex.Message}); } } }編譯運(yùn)行此程序輸出?表示環(huán)境就緒若報(bào)錯(cuò)Error Number 53說(shuō)明Data Source地址錯(cuò)誤若報(bào)Error Number 18456檢查SQL Server服務(wù)是否啟動(dòng)services.msc中查找SQL Server (SQLEXPRESS)及登錄模式。3.4 用戶(hù)權(quán)限配置避免“拒絕訪問(wèn)數(shù)據(jù)庫(kù)”陷阱即使連接字符串正確仍可能報(bào)Cannot open database LogisticsDB requested by the login. The login failed.。這是因?yàn)閃indows當(dāng)前用戶(hù)如DESKTOP-ABC\John未被授予數(shù)據(jù)庫(kù)訪問(wèn)權(quán)限。解決步驟在SSMS中展開(kāi)【安全性】→【登錄名】右鍵當(dāng)前Windows用戶(hù)→【屬性】左側(cè)選【用戶(hù)映射】勾選LogisticsDB數(shù)據(jù)庫(kù)下方勾選db_datareader和db_datawriter角色賦予讀寫(xiě)權(quán)限點(diǎn)擊【確定】提示不要勾選db_owner課程設(shè)計(jì)要求體現(xiàn)最小權(quán)限原則。若使用sa賬戶(hù)需在SQL Server配置管理器中啟用“SQL Server和Windows身份驗(yàn)證模式”并為sa設(shè)置強(qiáng)密碼。4. WinForm界面卡頓優(yōu)化DataGridView虛擬模式與異步加載實(shí)戰(zhàn)4.1 卡頓根源分析同步加載1000物流記錄的性能瓶頸當(dāng)MainForm.cs中LoadOrders()方法直接調(diào)用OrderService.GetAllOrders()并綁定到DataGridView時(shí)若訂單量達(dá)2000條界面會(huì)凍結(jié)3-5秒。根本原因在于GetAllOrders()在UI線程同步執(zhí)行數(shù)據(jù)庫(kù)查詢(xún)阻塞主線程DataGridView默認(rèn)啟用AutoSizeColumnsMode對(duì)每列內(nèi)容計(jì)算最佳寬度O(n2)復(fù)雜度每個(gè)OrderInfo對(duì)象綁定觸發(fā)DataGridViewCell.Value屬性的getter引發(fā)大量反射調(diào)用4.2 異步加載方案BackgroundWorker ProgressChanged事件改造MainForm.cs中的訂單加載邏輯使用BackgroundWorker脫離UI線程private BackgroundWorker _bgWorker; private ListOrderInfo _allOrders; private void LoadOrdersAsync() { _bgWorker new BackgroundWorker(); _bgWorker.WorkerReportsProgress true; _bgWorker.DoWork (s, e) { // 在后臺(tái)線程執(zhí)行耗時(shí)操作 var service new OrderService(); _allOrders service.GetAllOrders(); // 此處不操作UI控件 // 分批報(bào)告進(jìn)度可選 for (int i 0; i _allOrders.Count; i 100) { _bgWorker.ReportProgress(Math.Min(100, (i * 100) / _allOrders.Count)); } }; _bgWorker.ProgressChanged (s, e) { // 在UI線程更新進(jìn)度條 progressBar1.Value e.ProgressPercentage; }; _bgWorker.RunWorkerCompleted (s, e) { if (e.Error ! null) { MessageBox.Show($加載失敗{e.Error.Message}); } else { // 在UI線程綁定數(shù)據(jù) dataGridView1.AutoGenerateColumns false; // 關(guān)鍵禁用自動(dòng)生成列 dataGridView1.DataSource _allOrders; // 手動(dòng)設(shè)置列寬避免AutoSize計(jì)算 dataGridView1.Columns[OrderNo].Width 120; dataGridView1.Columns[CreateTime].Width 150; dataGridView1.Columns[StatusText].Width 100; } progressBar1.Visible false; }; progressBar1.Visible true; _bgWorker.RunWorkerAsync(); }4.3 虛擬模式進(jìn)階應(yīng)對(duì)萬(wàn)級(jí)物流數(shù)據(jù)的終極方案當(dāng)訂單量超過(guò)10000條即使異步加載ListOrderInfo全量?jī)?nèi)存占用也達(dá)數(shù)十MB。此時(shí)啟用DataGridView.VirtualMode true按需加載可見(jiàn)行// MainForm.Designer.cs 中設(shè)置 this.dataGridView1.VirtualMode true; this.dataGridView1.CellValueNeeded DataGridView1_CellValueNeeded; // 在MainForm.cs中實(shí)現(xiàn) private void DataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) { // 僅當(dāng)單元格在視圖內(nèi)時(shí)才查詢(xún) if (_allOrders ! null e.RowIndex _allOrders.Count) { var order _allOrders[e.RowIndex]; switch (e.ColumnIndex) { case 0: e.Value order.OrderNo; break; case 1: e.Value order.CreateTime.ToString(yyyy-MM-dd HH:mm); break; case 2: e.Value GetStatusText(order.Status); break; // 狀態(tài)碼轉(zhuǎn)文字 } } } // 配合滾動(dòng)條優(yōu)化只緩存當(dāng)前頁(yè)前后200條 private ListOrderInfo _visibleOrdersCache; private void dataGridView1_Scroll(object sender, ScrollEventArgs e) { if (e.Type ScrollEventType.EndScroll) { int firstVisible dataGridView1.FirstDisplayedScrollingRowIndex; int visibleCount Math.Min(50, dataGridView1.DisplayedRowCount(false)); int start Math.Max(0, firstVisible - 20); int count Math.Min(_allOrders.Count - start, 40); _visibleOrdersCache _allOrders.Skip(start).Take(count).ToList(); } }注意虛擬模式下DataSource屬性失效必須通過(guò)CellValueNeeded事件提供值。DataGridView不再持有全部數(shù)據(jù)引用內(nèi)存占用從O(n)降至O(1)但需自行管理緩存策略。這是課程設(shè)計(jì)高分項(xiàng)——能講清“為什么虛擬模式比BindingSource更省內(nèi)存”。5. 狀態(tài)碼與UI刷新聯(lián)動(dòng)解決“點(diǎn)擊發(fā)貨后狀態(tài)不變”的調(diào)試技巧5.1 狀態(tài)碼映射表統(tǒng)一管理業(yè)務(wù)狀態(tài)語(yǔ)義物流系統(tǒng)中狀態(tài)碼散落在多處數(shù)據(jù)庫(kù)Orders.Status列、DAL層SQL語(yǔ)句、BLL層條件判斷、UI層ComboBox選項(xiàng)。為避免不一致建立集中映射// 在Logistics.MODEL中創(chuàng)建StatusMap.cs public static class StatusMap { public static readonly Dictionaryint, string StatusText new Dictionaryint, string { { 0, 已取消 }, { 1, 待發(fā)貨 }, { 2, 運(yùn)輸中 }, { 3, 已簽收 }, { 4, 退貨中 } }; public static readonly Dictionarystring, int TextToCode StatusText .ToDictionary(kvp kvp.Value, kvp kvp.Key); }在MainForm.cs中綁定狀態(tài)選擇框private void InitStatusComboBox() { cmbStatus.DataSource new BindingSource(StatusMap.StatusText, null); cmbStatus.DisplayMember Value; cmbStatus.ValueMember Key; }5.2 UI刷新斷點(diǎn)調(diào)試法三步定位“狀態(tài)不更新”問(wèn)題當(dāng)點(diǎn)擊“發(fā)貨”按鈕后數(shù)據(jù)庫(kù)Orders.Status已更新為2但DataGridView仍顯示1按以下順序排查步驟1驗(yàn)證BLL層返回值在OrderService.ShipOrder()方法末尾添加斷點(diǎn)檢查返回值public bool ShipOrder(int orderID, string courierNo) { // ... 更新邏輯 bool result _orderDAL.UpdateStatus(orderID, 2); // 斷點(diǎn)在此行 Debug.WriteLine($UpdateStatus返回: {result}); // 查看是否為true return result; }若返回false說(shuō)明ExecuteNonQuery()影響行數(shù)為0檢查WHERE OrderID id條件是否匹配。步驟2檢查DataGridView數(shù)據(jù)源是否重綁定在按鈕點(diǎn)擊事件中確認(rèn)是否重新賦值DataSourceprivate void btnShip_Click(object sender, EventArgs e) { var service new OrderService(); service.ShipOrder(selectedOrderID, txtCourier.Text); // ? 錯(cuò)誤以為更新數(shù)據(jù)庫(kù)就自動(dòng)刷新UI // ? 正確強(qiáng)制刷新數(shù)據(jù)源 _allOrders service.GetAllOrders(); // 重新查詢(xún)最新數(shù)據(jù) dataGridView1.DataSource null; // 先置空 dataGridView1.DataSource _allOrders; // 再綁定 }步驟3監(jiān)聽(tīng)BindingSource事件推薦方案使用BindingSource替代直接綁定ListT利用其ListChanged事件private BindingSource _orderBindingSource new BindingSource(); private void SetupBinding() { _orderBindingSource.DataSource _allOrders; dataGridView1.DataSource _orderBindingSource; // 當(dāng)?shù)讓覮ist變更時(shí)自動(dòng)刷新 _orderBindingSource.ListChanged (s, e) { if (e.ListChangedType ListChangedType.Reset) Debug.WriteLine(數(shù)據(jù)源已重置UI將刷新); }; } // 在ShipOrder后調(diào)用 private void RefreshOrders() { _allOrders new OrderService().GetAllOrders(); _orderBindingSource.ResetBindings(false); // 觸發(fā)ListChanged事件 }5.3 狀態(tài)變更審計(jì)在Logistics.DAL中添加日志鉤子為追蹤狀態(tài)更新源頭在OrderDAL.UpdateStatus()中插入日志public int UpdateStatus(int orderID, int newStatus, SqlTransaction trans null) { string sql UPDATE Orders SET Status status, LastUpdateTime GETDATE() WHERE OrderID id; // 記錄誰(shuí)在何時(shí)修改了什么 string auditSql INSERT INTO StatusAudit (OrderID, OldStatus, NewStatus, Operator, UpdateTime) VALUES (oid, (SELECT Status FROM Orders WHERE OrderIDoid), nstatus, op, GETDATE()); using (var conn trans?.Connection ?? new SqlConnection(ConnectionManager.GetConnectionString())) { using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(status, newStatus); cmd.Parameters.AddWithValue(id, orderID); if (trans ! null) cmd.Transaction trans; var rows cmd.ExecuteNonQuery(); // 同事務(wù)內(nèi)寫(xiě)入審計(jì)日志 if (rows 0 trans ! null) { using (var auditCmd new SqlCommand(auditSql, conn)) { auditCmd.Parameters.AddWithValue(oid, orderID); auditCmd.Parameters.AddWithValue(nstatus, newStatus); auditCmd.Parameters.AddWithValue(op, Environment.UserName); // 簡(jiǎn)單標(biāo)識(shí)操作人 auditCmd.Transaction trans; auditCmd.ExecuteNonQuery(); } } return rows; } } }此審計(jì)表可幫助課程設(shè)計(jì)答辯時(shí)展示“系統(tǒng)具備操作追溯能力”是加分項(xiàng)。本文還有配套的精品資源點(diǎn)擊獲取