简介:这是一套基于C#与SQL Server开发的B/S架构网上书店管理系统课程设计源码,面向计算机专业本科生及.NET初学者,用于实践ASP.NET Web Forms开发、数据库设计与前后端协同逻辑。系统完整实现用户购书、购物车管理、后台商品/新闻维护等核心电商功能,采用双模板页设计(前台用户页+后台员工页),含轮播广告、响应式导航与风格统一的UI界面。资源包共262个文件,涵盖107张界面截图(jpg)、38个交互脚本(js)、28个样式表(css)、26个图标资源(png/svg等)以及14个核心页面(aspx)和14个业务逻辑类(cs),另含SQL Server数据库文件(mdf/ldf)与说明文档(pdf/md),整体压缩后16.51MB。已有149人学习下载,提供开箱即用的可运行项目结构、清晰的前后台分离目录、典型三层架构代码组织方式及完整数据库建表脚本,便于理解电商系统开发全流程与.NET企业级项目规范。
1. 为什么一个“网上书店管理系统”要用 C# + SQL Server 而不是 Python + SQLite?
你手头有个课程设计、毕设选题,或者小团队接的内部系统需求——要做个能卖书、查库存、管用户、下订单的“网上书店”。但一搜,满屏是 Vue+Node+MySQL 的电商模板,再翻 GitHub,全是 Spring Boot 套路。可你的任务书白纸黑字写着:基于 C# 和 SQL Server,编号【100013231】。这不是怀旧,是现实约束:学校机房只装了 Visual Studio 2022 和 SQL Server 2019;甲方运维只认 Windows Server + IIS + SSMS;你实习的公司 ERP 系统底层全是 .NET Framework 4.7.2 + SQL Server 2016,新模块必须无缝集成。
C# 在这个场景里不是“语言选择”,而是环境绑定型技术栈:它天然吃透 Windows 权限模型(AD 用户集成)、IIS 发布零配置、SQL Server 的SqlClient驱动深度优化(比如SqlBulkCopy批量导入比 ORM 快 8 倍)、以及DataSet/DataTable这种“内存数据库”式数据结构,让 WinForms 管理端拖个 DataGridView 就能双向绑定——这在 Python 或 JS 生态里得自己写三层映射。而 SQL Server 不是“用着顺手”,它是唯一能原生支持Windows 身份验证、行级安全(RLS)、Always Encrypted 敏感字段加密、以及与 Reporting Services 直连生成 PDF 销售报表的关系型数据库。
这不是炫技。当你面对真实落地场景:书店管理员要导出“近 30 天未登录用户清单”并邮件群发;财务要按 ISBN 拆分出版社返点;库管要扫描枪直连更新库存——这些需求背后全是 C# 对 Windows API 的调用能力(SerialPort读扫码枪)、SQL Server 的FOR XML导出结构化数据、以及SqlDependency实现库存变更实时推送。本篇不讲“C# 多线程怎么写”,只聚焦一件事:如何用最简路径,把【100013231】这个编号对应的真实系统跑起来,且避开 90% 新人必踩的连接失败、权限报错、部署黑屏三连坑。
2. 从零建库:SQL Server 2019 安装、建库、初始化数据三步闭环
2.1 安装 SQL Server 2019(避开“服务启动失败”的 3 个硬核检查点)
别直接双击SQL2019-SSEI-Expr.exe。先做三件事:
- 关掉 Windows Defender 实时防护(临时)——它会拦截
sqlservr.exe的服务注册,报错Error 1053: 服务没有及时响应启动或控制请求; - 确认 .NET Framework 4.8 已预装(Win10/11 默认有,但 Server 2016 需手动补)——SQL Server 2019 安装程序依赖
System.Data.SqlClient的强签名,缺 4.8 会卡在“正在安装数据库引擎”不动; - 磁盘剩余空间 ≥ 6GB(不是安装包大小!)——
tempdb初始文件占 8MB,但安装过程会生成 2GB 临时日志,SSD 碎片多时易触发0x851A001F错误。
安装时勾选:
- ✅ Database Engine Services(必须)
- ✅ SQL Server Management Studio(SSMS 18.10+,别用老版,否则连不上 2019 的 TLS 1.2 加密)
- ❌ Reporting Services(本项目不用报表服务,省 1.2GB 空间)
- ❌ Full-Text and Semantic Extractions(全文检索对书店搜索非必需,开反而拖慢插入)
提示:安装完成后,打开 SSMS → “服务器类型”选Database Engine,“服务器名称”填
localhost\SQLEXPRESS(默认实例名),身份验证选Windows 身份验证。如果连不上,90% 是 SQL Server 服务没启动:打开services.msc→ 找到SQL Server (SQLEXPRESS)→ 右键“启动”,状态变“正在运行”再试。
2.2 创建 bookstore_db 数据库与核心表(含外键约束与索引实战)
执行以下 T-SQL(在 SSMS 新建查询窗口粘贴运行):
-- 1. 创建数据库(指定主文件路径,避免装在 C:\Program Files 下导致权限问题) CREATE DATABASE bookstore_db ON PRIMARY ( NAME = 'bookstore_data', FILENAME = 'D:\SQLData\bookstore_data.mdf', -- 强制改到 D 盘,防权限拦截 SIZE = 10MB, MAXSIZE = UNLIMITED, FILEGROWTH = 5MB ) LOG ON ( NAME = 'bookstore_log', FILENAME = 'D:\SQLData\bookstore_log.ldf', SIZE = 5MB, MAXSIZE = 50MB, FILEGROWTH = 2MB ); GO -- 2. 切换数据库 USE bookstore_db; GO -- 3. 创建用户表(关键:密码哈希用 SHA2_512,非 MD5) CREATE TABLE users ( user_id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, -- 存 SHA2_512 哈希值(64 字符 + 盐) email NVARCHAR(100), phone CHAR(11), -- 国内手机号固定 11 位,用 CHAR 比 VARCHAR 省空间 role TINYINT NOT NULL DEFAULT 0, -- 0:普通用户, 1:管理员, 2:编辑, 3:财务 created_at DATETIME2 DEFAULT GETDATE() ); GO -- 4. 创建图书表(ISBN 用 CHAR(13) 精确匹配,避免前导零丢失) CREATE TABLE books ( book_id INT IDENTITY(1,1) PRIMARY KEY, isbn CHAR(13) NOT NULL UNIQUE, -- EAN-13 标准,如 9787040523456 title NVARCHAR(200) NOT NULL, author NVARCHAR(100), publisher NVARCHAR(100), price DECIMAL(10,2) NOT NULL CHECK (price >= 0.01), stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0), category_id INT, cover_image VARBINARY(MAX) -- 存二进制封面图,小图 ≤ 2MB ); GO -- 5. 创建分类表(树形结构,parent_id 允许 NULL 表示根分类) CREATE TABLE categories ( category_id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50) NOT NULL, parent_id INT NULL, level TINYINT NOT NULL DEFAULT 1, -- 1:一级分类, 2:二级分类 CONSTRAINT FK_categories_parent FOREIGN KEY (parent_id) REFERENCES categories(category_id) ); GO -- 6. 创建订单表(关键:status 用 tinyint 编码,非字符串) CREATE TABLE orders ( order_id NVARCHAR(20) PRIMARY KEY, -- 格式:ORD202405200001,避免 INT 溢出 user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0:待支付, 1:已支付, 2:已发货, 3:已完成, 4:已取消 created_at DATETIME2 DEFAULT GETDATE(), updated_at DATETIME2 DEFAULT GETDATE(), CONSTRAINT FK_orders_user FOREIGN KEY (user_id) REFERENCES users(user_id) ); GO -- 7. 创建订单明细表(复合主键 + 联合索引加速查询) CREATE TABLE order_items ( order_id NVARCHAR(20) NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), unit_price DECIMAL(10,2) NOT NULL, PRIMARY KEY (order_id, book_id), -- 防止同一订单重复加同一本书 CONSTRAINT FK_order_items_order FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE, CONSTRAINT FK_order_items_book FOREIGN KEY (book_id) REFERENCES books(book_id) ); GO -- 8. 创建关键索引(提升搜索与关联性能) CREATE NONCLUSTERED INDEX IX_books_isbn_title ON books(isbn, title); -- 按 ISBN 查书 + 模糊搜标题 CREATE NONCLUSTERED INDEX IX_orders_user_status ON orders(user_id, status) INCLUDE (total_amount, created_at); -- 用户订单列表页 CREATE NONCLUSTERED INDEX IX_order_items_order ON order_items(order_id) INCLUDE (book_id, quantity); -- 订单详情页 GO参数说明与逻辑:
CHAR(13)存 ISBN:比VARCHAR(13)节省 2 字节/行,且强制 13 位,避免978704052345(少一位)入库;order_id用NVARCHAR(20):自定义格式ORDYYYYMMDDNNNN(如ORD202405200001),既保证全局唯一,又便于按日期分片归档;status用TINYINT:4 字节INT存 0~4 太浪费,TINYINT仅 1 字节,且 SQL Server 对TINYINT的IN查询比VARCHAR快 3 倍;INCLUDE索引:把total_amount和created_at包进IX_orders_user_status,让查询SELECT total_amount, created_at FROM orders WHERE user_id=123 AND status=1无需回表,直接索引覆盖。
2.3 初始化测试数据(用 INSERT SELECT + VALUES 批量注入)
别用 SSMS 的“编辑前 200 行”手动输——100 本书得敲到手抽筋。用以下脚本一次灌入 50 本图书、10 个用户、5 个订单:
-- 插入 5 个一级分类(文学、科技、少儿、教育、生活) INSERT INTO categories (name, level) VALUES ('文学', 1), ('科技', 1), ('少儿', 1), ('教育', 1), ('生活', 1); GO -- 插入 15 个二级分类(关联 parent_id) INSERT INTO categories (name, parent_id, level) VALUES ('小说', 1, 2), ('散文', 1, 2), ('诗歌', 1, 2), ('编程', 2, 2), ('人工智能', 2, 2), ('硬件', 2, 2), ('绘本', 3, 2), ('儿童文学', 3, 2), ('科普', 3, 2), ('大学教材', 4, 2), ('中小学教辅', 4, 2), ('考试用书', 4, 2), ('烹饪', 5, 2), ('健身', 5, 2), ('心理', 5, 2); GO -- 插入 50 本图书(用 UNION ALL 避免 50 次 round-trip) INSERT INTO books (isbn, title, author, publisher, price, stock, category_id) SELECT * FROM ( VALUES ('9787020012345', '平凡的世界', '路遥', '人民文学出版社', 58.00, 120, 1), ('9787508634567', '浪潮之巅', '吴军', '中信出版社', 69.00, 85, 4), ('9787535898765', '小王子', '安东尼·德·圣-埃克苏佩里', '湖南文艺出版社', 39.80, 200, 7), ('9787302567890', '深入理解计算机系统', 'Randal E. Bryant', '机械工业出版社', 139.00, 60, 4), ('9787513334567', '人类简史', '尤瓦尔·赫拉利', '中信出版社', 68.00, 95, 1) -- 此处省略 45 行,实际需补全 50 行 VALUES ) AS t(isbn, title, author, publisher, price, stock, category_id); GO -- 插入 10 个测试用户(密码用 SHA2_512 哈希,盐值固定为 'bookstore2024') INSERT INTO users (username, password_hash, email, phone, role) VALUES ('admin', 'A9E3F1B2C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3', 'admin@bookstore.com', '13800138000', 1), ('user01', 'B1C2D3E4F5G6H7I8J9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z5A6B7C8D9E0F1G2', 'u01@bookstore.com', '13800138001', 0), ('user02', 'C2D3E4F5G6H7I8J9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z5A6B7C8D9E0F1G2H3', 'u02@bookstore.com', '13800138002', 0); -- 补全至 10 行 GO -- 插入 5 个测试订单(注意:order_id 格式必须匹配业务规则) INSERT INTO orders (order_id, user_id, total_amount, status) VALUES ('ORD202405200001', 2, 127.80, 3), ('ORD202405200002', 3, 39.80, 3), ('ORD202405200003', 2, 139.00, 2), ('ORD202405200004', 3, 68.00, 1), ('ORD202405200005', 2, 58.00, 0); GO -- 插入订单明细(关联 order_id 和 book_id) INSERT INTO order_items (order_id, book_id, quantity, unit_price) VALUES ('ORD202405200001', 1, 1, 58.00), ('ORD202405200001', 2, 1, 69.00), ('ORD202405200002', 3, 1, 39.80), ('ORD202405200003', 4, 1, 139.00), ('ORD202405200004', 5, 1, 68.00), ('ORD202405200005', 1, 1, 58.00); GO执行后验证:
- 在 SSMS 对象资源管理器中展开
bookstore_db→Tables→ 右键books→ “选择前 1000 行”,确认数据存在; - 运行
SELECT COUNT(*) FROM books;应返回50; - 运行
SELECT order_id, status FROM orders WHERE user_id = 2;应看到ORD202405200001(已完成)和ORD202405200003(已发货)等记录。
3. C# 工程搭建:WinForms 管理端 + ASP.NET Core Web 前端双模开发
3.1 创建解决方案结构(.NET 6.0 + SQL Server 2019 兼容性锚点)
不要用 .NET 8 ——虽然新,但Microsoft.Data.SqlClient5.1.5 在 .NET 8 下对SqlBulkCopy的ColumnMappings支持有 bug(报InvalidOperationException: The given ColumnMapping does not match the data source.)。锁定 .NET 6.0 LTS(长期支持,2022.11~2024.11),这是当前最稳组合。
在 Visual Studio 2022 中:
- 文件 → 新建 → 项目 → 搜索 “Windows Forms App (.NET Framework)” →不选!
→ 改搜 “Windows Forms App” → 选“.NET 6.0 (Long Term Support)”→ 项目名BookStore.WinFormAdmin; - 右键解决方案 → 添加 → 新建项目 → 搜索 “ASP.NET Core Web API” → 选“.NET 6.0”→ 项目名
BookStore.WebApi; - 右键解决方案 → 添加 → 新建项目 → 搜索 “Class Library” → 选“.NET 6.0”→ 项目名
BookStore.Data(放实体类与 DbContext); - 右键
BookStore.Data→ 管理 NuGet 包 → 安装:Microsoft.EntityFrameworkCore.SqlServerv6.0.30(必须 v6.x,v7+ 不兼容 SQL Server 2016/2019 的某些 T-SQL 语法)Microsoft.Data.SqlClientv5.1.5(官方推荐,修复了 2019 的 TLS 1.2 连接问题)
提示:若 VS 提示 “找不到 .NET 6.0 SDK”,去 https://dotnet.microsoft.com/zh-cn/download/dotnet/6.0 下载.NET SDK 6.0.422(2024 年最新 LTS 补丁),安装后重启 VS。
3.2 在 BookStore.Data 中定义实体与 DbContext(EF Core 6.0 最简配置)
BookStore.Data/Models/Book.cs:
namespace BookStore.Data.Models; public class Book { public int BookId { get; set; } public string Isbn { get; set; } = null!; public string Title { get; set; } = null!; public string? Author { get; set; } public string? Publisher { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public int? CategoryId { get; set; } public byte[]? CoverImage { get; set; } // 导航属性(不映射到数据库字段) public virtual Category? Category { get; set; } }BookStore.Data/Models/Category.cs:
namespace BookStore.Data.Models; public class Category { public int CategoryId { get; set; } public string Name { get; set; } = null!; public int? ParentId { get; set; } public byte Level { get; set; } // 自引用导航 public virtual Category? Parent { get; set; } public virtual ICollection<Category> Children { get; set; } = new List<Category>(); public virtual ICollection<Book> Books { get; set; } = new List<Book>(); }BookStore.Data/Contexts/BookStoreContext.cs:
using Microsoft.EntityFrameworkCore; using BookStore.Data.Models; namespace BookStore.Data.Contexts; public class BookStoreContext : DbContext { public BookStoreContext(DbContextOptions<BookStoreContext> options) : base(options) { } public DbSet<Book> Books { get; set; } public DbSet<Category> Categories { get; set; } public DbSet<User> Users { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderItem> OrderItems { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 主键配置 modelBuilder.Entity<Book>().HasKey(b => b.BookId); modelBuilder.Entity<Category>().HasKey(c => c.CategoryId); modelBuilder.Entity<User>().HasKey(u => u.UserId); modelBuilder.Entity<Order>().HasKey(o => o.OrderId); modelBuilder.Entity<OrderItem>().HasKey(oi => new { oi.OrderId, oi.BookId }); // 外键关系 modelBuilder.Entity<Book>() .HasOne(b => b.Category) .WithMany(c => c.Books) .HasForeignKey(b => b.CategoryId); modelBuilder.Entity<Category>() .HasOne(c => c.Parent) .WithMany(p => p.Children) .HasForeignKey(c => c.ParentId); modelBuilder.Entity<Order>() .HasOne(o => o.User) .WithMany(u => u.Orders) .HasForeignKey(o => o.UserId); modelBuilder.Entity<OrderItem>() .HasOne(oi => oi.Order) .WithMany(o => o.Items) .HasForeignKey(oi => oi.OrderId); modelBuilder.Entity<OrderItem>() .HasOne(oi => oi.Book) .WithMany(b => b.OrderItems) .HasForeignKey(oi => oi.BookId); // 索引(对应 SQL Server 的 NONCLUSTERED INDEX) modelBuilder.Entity<Book>() .HasIndex(b => new { b.Isbn, b.Title }); modelBuilder.Entity<Order>() .HasIndex(o => new { o.UserId, o.Status }) .IncludeProperties(o => new { o.TotalAmount, o.CreatedAt }); } }3.3 配置连接字符串与依赖注入(绕过“驱动程序无法通过 SSL 加密建立安全连接”)
BookStore.WebApi/appsettings.json:
{ "ConnectionStrings": { "BookStoreDb": "Server=localhost\\SQLEXPRESS;Database=bookstore_db;Trusted_Connection=True;Encrypt=False;TrustServerCertificate=True;" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }关键参数解释:
Trusted_Connection=True:启用 Windows 身份验证,避免明文账号密码(比User ID=sa;Password=xxx安全);Encrypt=False:禁用 SSL 加密(开发机无证书);TrustServerCertificate=True:跳过证书链校验(解决[08001] certificate chain is issued by an untrusted authority错误);
BookStore.WebApi/Program.cs:
using BookStore.Data.Contexts; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); // 注册 DbContext(使用 SQL Server Provider) builder.Services.AddDbContext<BookStoreContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("BookStoreDb"), sqlServerOptions => sqlServerOptions.CommandTimeout(120) // 关键:长查询超时设为 120 秒 )); // 注册控制器 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();验证连接:
启动BookStore.WebApi→ 浏览器访问https://localhost:5001/swagger→ 点击GET /WeatherForecast(默认接口)→ 若返回 JSON,则 EF Core 连接成功;
若报错A network-related or instance-specific error occurred...,90% 是Server=localhost\SQLEXPRESS写错:
- 检查 SQL Server 配置管理器 → SQL Server 网络配置 → SQLEXPRESS 的 TCP/IP 是否“已启用”;
- 检查防火墙是否放行 TCP 1433 端口(或改用命名管道
Server=np:\\.\pipe\sql\query)。
4. 避坑:C# 连接 SQL Server 的 5 个血泪经验(从 ConnectionString 到 EntityFramework)
4.1 现象:SqlConnection.Open()报错 “Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'”
原因:IIS 或 Windows 服务以 Local System 账户运行,尝试用匿名令牌连接 SQL Server,但 SQL Server 拒绝匿名登录。
解决:
- 开发阶段:在
appsettings.json中改用User ID=sa;Password=YourStrongPass123!;(先用 SSMS 重置 sa 密码); - 生产阶段:在 IIS 应用池 → 高级设置 → “标识”改为
ApplicationPoolIdentity→ 在 SQL Server 中执行:CREATE LOGIN [IIS APPPOOL\BookStoreAppPool] FROM WINDOWS; USE bookstore_db; CREATE USER [IIS APPPOOL\BookStoreAppPool] FOR LOGIN [IIS APPPOOL\BookStoreAppPool]; EXEC sp_addrolemember 'db_owner', 'IIS APPPOOL\BookStoreAppPool';
4.2 现象:SqlBulkCopy.WriteToServer()上传 10 万条图书数据,耗时 8 分钟且内存爆到 2GB
原因:默认SqlBulkCopy使用DataTable作为源,每行创建对象实例,GC 压力大;且未启用TableLock。
解决:
- 改用
IDataReader流式读取(如CsvReader),避免全量加载; - 设置
SqlBulkCopy.BatchSize = 10000; - 启用
SqlBulkCopy.DestinationTableName = "books"+SqlBulkCopy.SqlRowsCopied += (s, e) => Console.WriteLine($"Copied {e.RowsCopied} rows");; - 关键:
bulkCopy.EnableStreaming = true;(.NET 6+ 支持流式传输,内存占用降至 50MB)。
4.3 现象:WinForms DataGridView 绑定List<Book>后,修改单元格内容不自动更新数据库
原因:List<T>不实现IBindingList,无法通知 UI 变更;EF Core 的DbSet<T>也不支持双向绑定。
解决:
- 用
BindingList<Book>替代List<Book>; - 在
BookStore.WinFormAdmin中:private BindingList<Book> _books = new(); private void LoadBooks() { var books = context.Books.ToList(); // 先查出来 _books.Clear(); foreach (var b in books) _books.Add(b); // 逐个添加触发 BindingList 事件 dataGridView1.DataSource = _books; } - 修改后调用
context.SaveChanges()提交。
4.4 现象:ASP.NET Core API 返回Book对象时,CoverImage字段变成空数组[]而非null
原因:byte[]序列化时,JSON.NET 默认将nullbyte[] 序列为[],前端解析失败。
解决:
- 在
BookStore.WebApi/Program.cs中配置 JSON 选项:builder.Services.AddControllers().AddJsonOptions(options => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; options.JsonSerializerOptions.Converters.Add(new JsonByteArrayConverter()); // 自定义转换器 }); - 自定义
JsonByteArrayConverter:public class JsonByteArrayConverter : JsonConverter<byte[]> { public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) return null; using var doc = JsonDocument.ParseValue(ref reader); return Convert.FromBase64String(doc.RootElement.GetString()); } public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) { if (value == null) writer.WriteNullValue(); else writer.WriteStringValue(Convert.ToBase64String(value)); } }
4.5 现象:部署到 Windows Server 2016 后,API 接口全部 500,日志显示System.DllNotFoundException: Unable to load DLL 'sni.dll'
原因:Microsoft.Data.SqlClient依赖sni.dll(SQL Server Native Client),但 Server 2016 默认无此 DLL。
解决:
- 在
BookStore.WebApi.csproj中强制包含:<PropertyGroup> <PublishTrimmed>false</PublishTrimmed> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" /> </ItemGroup> - 发布时选择 “框架依赖部署(FDD)” 而非 “独立部署(SCD)”,确保
sni.dll随包发布; - 或手动下载
sni.dll(从C:\Program Files\Microsoft SQL Server\150\Shared\sni.dll复制)到发布目录runtimes\win-x64\native\下。
5. 管理端实战:WinForms 图书 CRUD 与库存预警功能落地
5.1 设计图书管理界面(DataGridView + ToolStrip 实现零代码绑定)
新建 WinForms 窗体FrmBookManage.cs:
- 拖入
ToolStrip(顶部工具栏)→ 添加按钮:btnAdd(新增)、btnEdit(编辑)、btnDelete(删除)、btnRefresh(刷新); - 拖入
SplitContainer(主区域分割)→Panel1放DataGridView(dgvBooks),Panel2放GroupBox(图书详情); dgvBooks属性:AutoGenerateColumns=False,手动添加列:Name DataPropertyName HeaderText Width colIsbnIsbnISBN 120 colTitleTitle书名 200 colAuthorAuthor作者 150 colPricePrice价格 80 colStockStock库存 80 colCategoryCategory.Name分类 100
绑定代码(FrmBookManage.cs):
private BindingList<Book> _books = new(); private BookStoreContext _context; public FrmBookManage() { InitializeComponent(); _context = new BookStoreContext(new DbContextOptionsBuilder<BookStoreContext>() .UseSqlServer("Server=localhost\\SQLEXPRESS;Database=bookstore_db;Trusted_Connection=True;") .Options); LoadBooks(); } private void LoadBooks() { var books = _context.Books .Include(b => b.Category) .OrderBy(b => b.Isbn) .ToList(); _books.Clear(); foreach (var b in books) _books.Add(b); dgvBooks.DataSource = _books; }5.2 实现库存预警(当 stock < 5 时 DataGridView 行变红)
在dgvBooks的CellFormatting事件中:
private void dgvBooks_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.ColumnIndex == dgvBooks.Columns["colStock"].Index && e.Value != null) { if (int.TryParse(e.Value.ToString(), out int stock) && stock < 5) { e.CellStyle.BackColor = Color.LightCoral; e.CellStyle.ForeColor = Color.White; } } }为什么不用RowPrePaint?
CellFormatting在单元格渲染前触发,性能开销小;RowPrePaint会为每一行触发,即使该行未显示(滚动时),CPU 占用高 3 倍;CellFormatting可精准控制单个单元格样式,RowPrePaint需手动计算e.RowIndex对应数据,易出错。
5.3 新增图书窗体(带 ISBN 校验与封面图压缩)
新建窗体FrmBookAdd.cs:
TextBox txtIsbn:Leave事件中调用ValidateIsbn13(txtIsbn.Text);PictureBox pbCover+Button btnSelectCover:点击后打开OpenFileDialog,选图后执行压缩:
private void btnSelectCover_Click(object sender, EventArgs e) { using var ofd = new OpenFileDialog { Filter = "图片文件|*.jpg;*.jpeg;*.png", InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) }; if (ofd.ShowDialog() == DialogResult.OK) { var image = Image.FromFile(ofd.FileName); // 压缩到 800x1200 以内,质量 80% var resized = ResizeImage(image, 800, 1200); var bytes = ImageToBytes(resized, ImageFormat.Jpeg); pbCover.Image = resized; _coverBytes = bytes; // 存为 byte[] } } private Image ResizeImage(Image img, int maxWidth, <p> <a href="https://download.csdn.net/download/s1t16/88268021" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>