
简介这是一套基于C#与Windows Forms开发的图书管理系统完整源码包含配套数据库脚本。系统覆盖图书入库、出库、借阅、归还等日常流通环节前端界面与后端数据操作均已实现适合C#初学者、Winform开发者以及需要快速搭建小型图书管理项目的技术人员参考学习。压缩包共332个文件大小约17.08MB其中cs源文件123个resx与resources资源文件86个还包含SQL脚本、dll库、pdb调试文件等。源码采用典型三层结构BookBLL业务逻辑层负责借阅规则与库存管理BookDAL数据访问层封装增删改查操作BookEntity实体层定义图书、借阅者等数据模型DB目录提供数据库建表与初始化脚本可直接执行生成运行环境。已有2286人学习下载。通过研读该项目可以掌握Winform界面布局、ADO.NET数据库交互、三层架构分工及面向对象设计思路无论是课程设计、毕业设计还是入门实战都具备实用参考价值。1. 一个 C# 图书管理系统源码到底能给你什么期末实训答辩前夜还在电脑前改 bug——借书按钮按下去程序不但没把库存减一反而抛出一个 SqlException。这个场景做过 C# winform 图书管理系统的人基本都遇到过。所谓“图书管理系统源码含数据库脚本”本质上就是把管理员登录、图书维护、借书还书、逾期查询这一整套业务用 WinForms 做界面、用 SQL Server 做存储再配上一份能直接建库建表的脚本让一个项目从零到能跑、能演示、能答辩的完整闭环。这套东西适合三类人做课程设计的学生、想拿 winform 项目案例练手的新手、还有接手别人老系统要快速上手的开发者。源码本身不稀奇稀有的是帮你把表和代码一次对上号的那份数据库脚本。2. 系统设计与数据库脚本先钉死五张表再谈功能2.1 一张借书时序图背后业务闭环没设计清楚代码全是摆设拿到任何一套图书管理系统源码别急着打开 .sln 看窗体先画一遍业务闭环。常见做法是用例图或时序图把“谁、在什么条件下、做什么、产生什么数据”标清楚。这类系统的核心闭环只有三个图书维护管理员增删改查书目、读者管理办证、信息维护、借还流程借书、还书、续借、逾期统计。三个闭环之间靠外键关联比如借阅记录必须同时引用读者 ID 和图书 ID否则还书时根本不知道要更新哪本书的库存。我在做这类项目时习惯先用表格把“页面 → 操作 → 影响哪张表”列一遍再回头看窗体设计器里拖了多少控件。这样做的价值在于很多源码表面功能齐全但借书时不扣库存、还书时不写 ReturnTime这就是典型的业务闭环没理顺。你照着源码改三天不如先花两小时把这个闭环补上。另外还书操作里有个小细节——续借本质是更新 DueTime而不是删掉原记录重新插入很多人在这写了个“先删后插”的逻辑导致统计全乱。2.2 五张核心表字段、类型、外键这样定图书管理系统的数据库脚本通常围绕五张表展开我把常用字段和设计意图整理成一张表你在建库时可以对照着取舍。表名核心字段说明AdminAdminID、Account、PassWord、RealName管理员账户密码存 MD5 哈希不存明文CategoryCategoryID、CategoryName图书分类给 Book 做外键BookBookID、ISBN、BookName、Author、CategoryID、Stock、TotalCountStock 是当前可借数TotalCount 是馆藏总量ReaderReaderID、CardNo、ReaderName、Phone、RegTime读者卡号建议唯一索引办证时验证重卡BorrowBorrowID、ReaderID、BookID、BorrowTime、DueTime、ReturnTime、StatusStatus 用 tinyint1 借出、0 已还字段类型上凡是涉及中文的都用 nvarchar长度按实际给BookName 给 50 到 100 都行别一上来就 nvarchar(max)。ID 列用 int identity(1,1) 足够图书管理系统到不了千万级数据用 bigint 纯属给自己添麻烦。Stock 用 int 是常识但有人为了省事把书架上每一本都建一条记录然后用“在借状态”字段区分这种设计不是不行而是在统计“库存还剩多少”时要写 count 聚合比直接 stock-1 慢且容易被并发问题搞挂。外键在这类实训项目里一定要加理由不是规范而是能在数据录错时少背一口锅。我见过一套源码把 Borrow 表做了外键结果测试时录了一本不存在的书去借界面没报错报表却查不到数据后来发现外键被作者注释掉了。外键的代价是插入和更新多一次约束检查但图书系统的量级完全感知不到。索引方面Borrow 表上建 ReaderID 和 Status 两个索引还书查询、逾期列表都靠它们加速。2.3 一次跑通的数据库脚本建库、建表、种子数据全在这数据库脚本的核心价值是“换一台电脑也能一键还原环境”。我看到太多人把脚本写成 SSMS 图形界面里点出来的那一大坨带一堆无用 GO 和 USE master交付时根本不敢从头跑。我一般会把脚本按顺序写好先建库、再建表、后插种子数据全程保证可重复执行。下面是一份我可以直接抄进项目的建库建表脚本第一段解决“库里没有就建”的问题。-- 如果数据库不存在先创建 IF DB_ID(BookManager) IS NULL BEGIN CREATE DATABASE BookManager; END GO -- 切换上下文避免把表建到 master 里 USE BookManager; GO -- 管理员表 IF OBJECT_ID(dbo.Admin, U) IS NOT NULL DROP TABLE dbo.Admin; GO CREATE TABLE dbo.Admin ( AdminID INT IDENTITY(1,1) PRIMARY KEY, Account VARCHAR(32) NOT NULL UNIQUE, PassWord CHAR(32) NOT NULL, -- 32 位 MD5 哈希串 RealName NVARCHAR(20) NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); GO这段脚本里有两个容易被忽略的点。DB_ID 判断要放在 CREATE DATABASE 外面否则第一次执行时数据库不存在直接 USE 会报错DROP 表语句写在每个 CREATE 之前是为了你改表结构重跑时不会撞上“对象已存在”。注意IF OBJECT_ID(dbo.Admin, U) IS NOT NULL里第二个参数是 U代表用户表很多人写成别的参数查不到东西。继续往下Book 和 Borrow 表是核心它们把图书库存和借阅记录串起来外键和索引也在这里定义。CREATE TABLE dbo.Book ( BookID INT IDENTITY(1,1) PRIMARY KEY, ISBN VARCHAR(13) NOT NULL, BookName NVARCHAR(50) NOT NULL, Author NVARCHAR(20) NULL, CategoryID INT NOT NULL, Stock INT NOT NULL DEFAULT 0, TotalCount INT NOT NULL DEFAULT 0, CONSTRAINT FK_Book_Category FOREIGN KEY (CategoryID) REFERENCES dbo.Category(CategoryID) ); GO CREATE TABLE dbo.Reader ( ReaderID INT IDENTITY(1,1) PRIMARY KEY, CardNo VARCHAR(16) NOT NULL UNIQUE, ReaderName NVARCHAR(20) NOT NULL, Phone VARCHAR(20) NULL, RegTime DATETIME NOT NULL DEFAULT GETDATE() ); GO CREATE TABLE dbo.Borrow ( BorrowID INT IDENTITY(1,1) PRIMARY KEY, ReaderID INT NOT NULL, BookID INT NOT NULL, BorrowTime DATETIME NOT NULL DEFAULT GETDATE(), DueTime DATETIME NOT NULL, ReturnTime DATETIME NULL, Status TINYINT NOT NULL DEFAULT 1, -- 1 借出0 已还 OperatorID INT NULL, CONSTRAINT FK_Borrow_Reader FOREIGN KEY (ReaderID) REFERENCES dbo.Reader(ReaderID), CONSTRAINT FK_Borrow_Book FOREIGN KEY (BookID) REFERENCES dbo.Book(BookID) ); GO CREATE INDEX IX_Borrow_ReaderID ON dbo.Borrow(ReaderID); CREATE INDEX IX_Borrow_Status ON dbo.Borrow(Status); GOBook 表里 Stock 和 TotalCount 分开是有讲究的。TotalCount 是馆藏总量Stock 是当前可借数量还书时只动 Stock 不动 TotalCount这样才能统计出“借出去多少本”。Borrow 表的 Status 配合 ReturnTime 使用还书时把 Status 改成 0并填入 ReturnTime比直接删记录更适合保留历史借阅数据。索引加到 Borrow 表上是因为读操作大多按读者查借阅列表、按状态筛逾期记录两个索引覆盖两种高频查询。种子数据不能省。没有管理员账号的图书管理系统交付后第一件事就是让用户去改数据库密码。常见的做法是在脚本末尾插入初始分类和一个默认管理员账号 admin、密码 123456 的 MD5 是e10adc3949ba59abbe56e057f20f883e。INSERT INTO dbo.Category(CategoryName) VALUES (N计算机), (N文学), (N历史); INSERT INTO dbo.Admin(Account, PassWord, RealName) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, N系统管理员);种子数据里最容易翻车的坑是字符串前缀。SQL Server 里字符串默认是 varchar中文会变成乱码所以 N计算机 里的 N 是必须的它告诉 SQL Server 按 Unicode 存储。PassWord 列是 CHAR(32)正好存 32 位小写 MD5长度多一位少一位都会让登录永远失败——这是很多人把源码跑起来却登不进去的真正原因。3. 数据访问与核心业务连接、事务与参数化一个都不能少3.1 连接字符串与 App.config第一行配置决定程序能不能起来WinForm 图书管理系统最典型的翻车点不在窗体设计而在连接字符串。你辛辛苦苦把界面拖完一运行就弹“建立与服务器的连接时出错”原因几乎都出在这一行。我拿到源码第一件事就是检查 App.config看连接串指向的服务器名和登录方式对不对。?xml version1.0 encodingutf-8 ? configuration startup useLegacyV2RuntimeActivationPolicytrue supportedRuntime versionv4.0 sku.NETFramework,Versionv4.7.2 / /startup connectionStrings add nameBookManager connectionStringServer.;DatabaseBookManager;User IDsa;Passwordyour_password;EncryptFalse;TrustServerCertificateTrue providerNameSystem.Data.SqlClient / /connectionStrings /configurationServer 后面的(local)或者点号都指向本机默认实例如果你装的是 SQL Server Express要写成.\SQLEXPRESS。Encrypt 和 TrustServerCertificate 是 SQL Server 2019 之后的新版驱动要求开启的老项目的连接串没有这两项放到新环境就会被强制加密策略拦住这是近年常见的低级坑。数据库名要和脚本里的 BookManager 完全一致大小写无所谓但拼错一定挂。密码里有特殊字符时小心 XML 转义要写成amp;。3.2 用 ADO.NET 封装一个小巧的 SqlHelper为什么不用 Entity Framework图书管理系统这类实体关系简单、交互逻辑固定的项目EF 的 DbContext 生命周期管理反而比 ADO.NET 更费心。ADO.NET 的 SqlConnection、SqlCommand、SqlDataAdapter 写两层封装性能可控、依赖少源码拿到手在任何机器上都能编译不需要额外 NuGet 包还原。下面是几乎每个这类项目都会用的 SqlHelper 核心部分。public static class SqlHelper { private static readonly string connStr ConfigurationManager.ConnectionStrings[BookManager].ConnectionString; public static int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (ps ! null ps.Length 0) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } public static DataTable ExecuteDataTable(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { if (ps ! null ps.Length 0) da.SelectCommand.Parameters.AddRange(ps); DataTable dt new DataTable(); da.Fill(dt); return dt; } } }逻辑说明两段 using 保证了 SqlConnection 和 SqlCommand 用完后自动 Dispose连接池资源立刻回收。ExecuteDataTable 用 SqlDataAdapter 而不是手动 Open 连接是因为 Fill 内部会自己管理连接开关你手动 Open 反而可能留下未关闭的会话。参数说明params SqlParameter[] 允许你调用时传任意个参数不传就是非参数化查询传了就由 ADO.NET 自动做类型检查和转义。注意这里我没写 ExecuteScalar但借书时查库存、登录时查记录数都会用到你可以在该类里补一个同款实现返回 object 类型再转换。3.3 借书事务库存和借阅记录必须同生共死借书操作如果只做“插入一条借阅记录”不做“库存减一”或者做了但中途失败就会出现账目对不上的情况。这两件事必须放在同一个数据库事务里要么都成功要么都回滚。这是图书管理系统数据一致性最核心的一道防线。public static void BorrowBook(int readerId, int bookId, int operatorId) { string checkSql SELECT Stock FROM dbo.Book WHERE BookID BookID; string updateSql UPDATE dbo.Book SET Stock Stock - 1 WHERE BookID BookID AND Stock 0; string insertSql INSERT INTO dbo.Borrow(ReaderID, BookID, DueTime, OperatorID) VALUES(ReaderID, BookID, DATEADD(DAY, 30, GETDATE()), OperatorID); using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { SqlCommand checkCmd new SqlCommand(checkSql, conn, tran); checkCmd.Parameters.AddWithValue(BookID, bookId); int stock (int)checkCmd.ExecuteScalar(); if (stock 0) throw new Exception(库存不足不能借出。); SqlCommand updateCmd new SqlCommand(updateSql, conn, tran); updateCmd.Parameters.AddWithValue(BookID, bookId); if (updateCmd.ExecuteNonQuery() 0) throw new Exception(扣库存失败请检查图书状态。); SqlCommand insertCmd new SqlCommand(insertSql, conn, tran); insertCmd.Parameters.AddWithValue(ReaderID, readerId); insertCmd.Parameters.AddWithValue(BookID, bookId); insertCmd.Parameters.AddWithValue(OperatorID, operatorId); insertCmd.ExecuteNonQuery(); tran.Commit(); } catch { tran.Rollback(); throw; } } }事务代码的关键是每个 SqlCommand 都要传入同一个 tran 对象三个命令绑到同一连接同一事务Commit 前任何一步抛异常都会触发 Rollback。UPDATE 语句里带了AND Stock 0这是防止并发场景下管理员同时借出同一本书的最后库存跟前面 SELECT 检查形成双保险。这里我用了 AddWithValue 让代码看起来简洁但它在 SqlParameter 类型推断模糊时有性能隐患更稳的写法是new SqlParameter(ReaderID, SqlDbType.Int) { Value readerId }。你在真实项目里尽量用后者避免参数被推断成 NVARCHAR 导致索引失效。3.4 参数化查询把 SQL 注入挡在图书系统门外图书管理系统不是银行但只要有文本框拼接 SQL注入就是真实风险。登录框输入 OR 11这种字符串在很多老源码里能直接绕过密码校验。这类问题的根源不是 SQL Server 不安全而是你把用户输入直接拼进了 SQL 文本。参数化查询不是可选项是最低要求。// 反面示例绝对不要用 string sql SELECT COUNT(1) FROM dbo.Admin WHERE Account txtUser.Text.Trim() AND PassWord Md5(txtPass.Text.Trim()) ; // 正确写法 string sql SELECT COUNT(1) FROM dbo.Admin WHERE AccountAccount AND PassWordPassWord; SqlParameter p1 new SqlParameter(Account, SqlDbType.VarChar, 32) { Value txtUser.Text.Trim() }; SqlParameter p2 new SqlParameter(PassWord, SqlDbType.Char, 32) { Value Md5(txtPass.Text.Trim()) }; int count (int)SqlHelper.ExecuteScalar(sql, p1, p2);参数化之后输入的单引号、分号都会被当成普通字符串处理永远不会改变 SQL 语句结构。两个参数的类型和长度也要跟表结构对齐p2 用 Char(32) 而不是 VarChar是为了跟 Admin 表里 PassWord 列的类型完全匹配避免隐式转换让查询走不上索引。作为习惯凡是带用户输入的查询我都强迫自己写成参数化版本一句例外都不留。4. 界面实现与交互细节DataGridView 之外的体验功夫4.1 DataGridView 与 ListView列表控件选型与数据绑定图书主列表用 DataGridView左侧图书分类导航用 ListView这是这类系统最顺手的组合。DataGridView 适合展示多列、可排序、可编辑的数据表ListView 适合少量条目的快速导航自带“详细信息”视图模式配合 ImageList 还能给分类加图标。别把一本书的所有字段全堆到 ListView 里那不是它擅长的事。private void LoadBookGrid() { string sql SELECT b.BookID, b.BookName, c.CategoryName, b.Author, b.Stock, b.TotalCount FROM dbo.Book b INNER JOIN dbo.Category c ON b.CategoryID c.CategoryID ORDER BY b.BookID DESC; DataTable dt SqlHelper.ExecuteDataTable(sql); dgvBooks.DataSource null; // 先断开绑定避免残留旧数据 dgvBooks.AutoGenerateColumns true; dgvBooks.DataSource dt; }绑定前把 DataSource 置空是防止重复查询时界面上的列头和数据错位。AutoGenerateColumns默认是 true按查询字段自动生成列如果你想要中文列名要么改 SQL 里的别名要么关闭自动生成后在设计器里手动配 DataPropertyName。DataGridView 行高和列宽这种看起来琐碎的设置决定了演示时第一眼好感——用 winform 控件属性大全里的老办法把AllowUserToAddRows设为 falseSelectionMode设 FullRowSelect表格就不会出现最后一行多出来的空行和难看的单元格高亮。4.2 状态栏与进度条耗时操作怎么不把界面卡死批量导入图书、生成逾期报表这类操作如果直接在按钮事件里同步执行窗体就会白屏假死标题栏出现“未响应”。本质原因是 UI 线程被数据库调用阻塞了。c# winform 更新状态栏与进度条的标准做法是异步执行耗时逻辑然后通过 Invoke 或 async/await 回到主线程更新控件。我一般用 async/await代码比 BackgroundWorker 干净。private async void btnImport_Click(object sender, EventArgs e) { toolStripStatusLabel1.Text 正在导入图书数据...; toolStripProgressBar1.Minimum 0; toolStripProgressBar1.Maximum 100; toolStripProgressBar1.Value 0; await Task.Run(() { for (int i 1; i 100; i) { Thread.Sleep(30); // 模拟耗时步骤实际这里放批量写入逻辑 this.Invoke(new Action(() { toolStripProgressBar1.Value i; toolStripStatusLabel1.Text $已完成 {i}%; })); } }); toolStripStatusLabel1.Text 导入完成; LoadBookGrid(); }这个写法里有两个关键点。Task.Run 把耗时操作放到线程池线程await 让 UI 线程在等待期间继续处理界面消息这就是不卡界面的原因。但跨线程更新控件必须用 Invoke 或 BeginInvoke直接改进度条 Value 会抛跨线程异常。进度条不需要每次循环都刷实际项目里可以每隔 5% 更新一次否则高频 Invoke 会把 UI 线程累垮。注意 btnImport 用了 async void事件处理程序可以这样写普通方法则一定要返回 Task。4.3 界面美化皮肤、自绘与控件属性调优的取舍winform 界面美化是个两极分化的话题。第三方皮肤组件比如 IrisSkin 这类拖上去马上好看但换来的是打包体积变大和杀毒软件误报率上升。我看到不少源码集成了皮肤 DLL结果客户电脑上 exe 直接被删解释成本极高。我现在的习惯是不用皮肤靠控件属性和自绘把界面做到“干净整洁”就行。// 让 DataGridView 表头变色的小技巧比整包皮肤便宜得多 dgvBooks.EnableHeadersVisualStyles false; dgvBooks.ColumnHeadersDefaultCellStyle.BackColor Color.FromArgb(43, 87, 154); dgvBooks.ColumnHeadersDefaultCellStyle.ForeColor Color.White; dgvBooks.ColumnHeadersDefaultCellStyle.Font new Font(微软雅黑, 9F, FontStyle.Bold); dgvBooks.RowTemplate.Height 30;EnableHeadersVisualStyles 必须设 false否则 DataGridView 会忽略你给表头设置的背景色这是很多人改了没反应的原因。字体统一用微软雅黑 9pt 或 10pt按钮默认高度调成 30窗体背景色用浅灰240, 240, 240就是一套能看的默认风格。别花时间在阴影、圆角这些特效上图书管理系统的用户要的是信息密度不是视觉冲击。5. 常见问题与排查换台机器跑不起来的几个深坑5.1 现象运行时报“用户 sa 登录失败”原因SQL Server 默认安装时只开了 Windows 身份验证模式sa 账户要么没启用要么没有设置密码。程序里连接字符串写了 User IDsa却没有任何凭据可用数据库自然把你拒之门外。解决打开 SSMS服务器属性 → 安全性 → 选择“SQL Server 和 Windows 身份验证模式”。然后到安全性 → 登录名 → sa → 状态 → 启用并在“常规”里重设一个强密码。改完必须重启 SQL Server 服务才能生效重启前别忘了先确认连接字符串里的密码跟新密码一致。5.2 现象换电脑后报“未能加载文件或程序集”或程序闪退原因源码的项目目标框架跟新机器的 .NET Framework 版本不匹配。VS2015 时代的项目很多默认 .NET Framework 4.5新电脑装的是 4.8理论上向下兼容但如果你引用的某个组件是用老版本编译的或者项目里存在 x86 和 AnyCPU 混用就会出现这种玄学问题。解决右键项目 → 属性 → 目标框架统一改成 .NET Framework 4.7.2 或 4.8然后清理、重新生成。如果用了第三方控件确认对应 DLL 也是同一目标框架编译的。还有一处要检查项目里有引用关系时所有项目的目标平台要一致别一个 AnyCPU 一个 x86运行时加载就会翻车。5.3 现象新增图书后 DataGridView 不显示新数据原因数据库写入成功了但界面上的 DataTable 是旧引用重新查询后没有同步到 DataSource。也有人是直接复用同一个 DataTable 实例调用 Merge 或者重新 Fill 后控件不会主动刷新。解决每次加载先赋 null 再赋新 DataTable让控件彻底重建绑定。如果你用了 BindingSource还要调用bindingSource.ResetBindings(false)强制刷新。检查顺序是先断点看查询结果有没有数据再确认 dgv.DataSource 指向的是不是这个新对象。5.4 现象杀毒软件把生成的 exe 当病毒删除原因WinForms 程序编译出的 exe 如果没签名又带了压缩壳、混淆器或者引用了某些皮肤 DLL很容易被杀毒软件的启发式引擎误判。这类系统交付给客户时特别常见往往不是代码有问题而是文件特征太像未知程序。解决不建议直接关杀毒软件去白名单正确做法是发布时给程序做代码签名证书EV 或 OV 证书都行签名后的 exe 信任度大幅提升。内部使用时可以提交误报申诉。至少要做的是剔除那些不必要的第三方 DLL 和壳WinForms 原生态 exe 的误报率比加壳版本低得多。5.5 现象数据库脚本拷到别的机器执行报错提示文件路径或排序规则问题原因脚本里硬编码了原作者电脑的绝对路径比如FILENAMED:\DB\BookManager.mdf目标机器没有 D 盘或该目录不存在就会失败。中文字段排序相异还会导致索引创建失败或查询结果顺序乱掉。解决创建数据库时不写 FILENAME 参数让 SQL Server 走默认数据目录中文字段统一用 NVARCHAR/NCHAR 类型脚本开头固定IF DB_ID(...)判断避免重复执行报错。执行前先把脚本里的 GO 语句看一遍确认没有悬空的 USE master 把表建错地方。6. 从源码到交付部署打包与数据备份的收尾功夫6.1 用 SSMS 生成脚本把数据库变一份“后悔药”源码交付时不附带数据库备份文件.bak最稳的配套是数据库脚本。SSMS 里右键数据库 → 任务 → 生成脚本在“设置脚本选项”页把“要编写的数据类型”改成“架构和数据”脚本就会把建表和种子数据一次性打出来。把这份脚本存到项目的 Database 文件夹README 里写清楚“新建查询 → 执行”。别人拿到源码再也不用问你“密码是多少”因为脚本里已经写死了初始管理员账号。这也给你自己留了后悔药——数据搞坏时重建一个干净库一分钟的事。6.2 打包成安装程序ClickOnce 与安装项目怎么选winform 打包成安装程序常见解法有两种。项目简单、用户能联网用发布功能走 ClickOnce更新方便缺点是不能安装到 Program Files需要管理员权限需要装到服务器或给客户一次性完整包的用 Visual Studio Installer Projects 扩展做安装项目把主输出设置好系统必备里勾上 .NET Framework 和 SQL Server Express。两种方式发布前都要把配置文件的连接字符串从 sa 密码改成可配置项否则客户装上程序连不上数据库还得回去改代码。我现在的习惯是拿到任何一套 C# winform 图书管理系统源码第一件事不是打开主窗体而是先把数据库脚本从头到尾跑一遍再去看代码。这个动作帮我躲过了无数次“界面好好的、数据进不去”的深夜翻车。希望帮到你。本文还有配套的精品资源点击获取