news 2026/10/4 1:13:49

C#+MySQL仓库管理系统实战:从表结构到事务部署避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#+MySQL仓库管理系统实战:从表结构到事务部署避坑

简介:一套基于C#与MySQL开发的仓库管理系统完整项目,面向C#初学者和数据库学习者,适合课程设计、毕业设计或项目实训。系统包含商品管理、库存管理、出入库记录、报表统计等功能,并采用数据访问层、业务逻辑层、表示层的分层结构,便于理解真实项目的组织方式。压缩包共135个文件,大小约1.2MB,主要包含49个C#源文件、资源文件、界面截图、SQL脚本、配置文件和可执行程序等,且附带数据库文件,可省去手动建表。项目工程文件齐全,可直接用Visual Studio打开调试。已有266人学习下载。通过分析源码,可掌握ADO.NET访问MySQL、异常与事务处理、WinForm界面设计,以及进销存类系统的常见模块划分,为后续开发提供可参考的代码模板。

1. 为什么C#+MySQL的仓库管理系统还能成为今天的常青项目

实际开发里,仓库管理系统常以压缩包形式在开发者和毕业生之间流传,大多是C#的WinForms或WPF项目,数据库脚本挂一个.sql文件,连上就能跑。这类项目最大的价值不是开箱即用,而是提供一套成熟的数据表结构和业务闭环——从MySQL里的商品档案、入库单、出库单到库存台账,每一步都有参照。拿到的压缩包里通常是一个Visual Studio工程加一个数据库文件,改改连接字符串就能登录后台,接下来是把别人逻辑变成自己的。

适合刚接手内部工具开发、要给工厂做进销存,或准备毕业设计的人。MySQL是地基,C#负责界面交互,连接、事务、字符集是后续坑的源头。下边按先看表、再跑通、再改造、再避坑的顺序讲。

2. 先拆仓库管理系统的表结构和数据流:从MySQL文件里读出业务

2.1 从数据库文件反推业务:核心表与字段设计

大多数C#版WMS压缩包的数据库文件里,表结构绕不开这几张:User(用户)、Product(商品档案)、Supplier(供应商)、InStock与InStockDetail(入库单及明细)、OutStock与OutStockDetail(出库单及明细)、Stock(实时库存)、Log(操作日志)。主外键关系很好认:入库主表对明细表一对多,明细表通过ProductId关联商品表,通过InStockId关联主表;库存表以商品ID为唯一索引,入库累加、出库扣减。

为什么要先看懂表再动代码?因为很多流传的项目业务逻辑写得随意,直接在C#代码里拼接SQL。我接手过一套压缩包,入库明细表里连“操作员”字段都没有,库存表干脆没有期初字段,导致上线第一天就不知道账面对不对。接手的第一步不是改界面,而是把每张表的主键、外键、索引过一遍,确认哪张表是库存快照、哪张表只做流水记录。

表名关键字段职责
UserId, UserName, Password, DisplayName登录与权限
ProductId, ProductCode, ProductName, Spec, Unit, Price商品档案
SupplierId, SupplierCode, SupplierName, Contact供应商
InStockId, InStockNo, SupplierId, Operator, InStockDate, Status入库单主表
InStockDetailId, InStockId, ProductId, Quantity, Price, TotalAmount入库明细
OutStockId, OutStockNo, CustomerId, Operator, OutStockDate, Status出库单主表
OutStockDetailId, OutStockId, ProductId, Quantity, Price, TotalAmount出库明细
StockProductId, Quantity, UpdateTime实时库存
OperationLogId, Operator, Action, TargetId, CreateTime操作留痕

表设计里最容易翻车的是Quantity字段类型。用int做库存数量,小仓库看着够用,一旦出现负库存或大批次盘点就溢出;凡是涉及数量、金额的列,建议统一decimal(18,2)。金额列绝对不要用float,等对账单里出现0.30000000000000004这种数,你就明白为什么了。另外Stock表里的Quantity默认值要设置成0而不是NULL,否则登录后看到的库存列表全是一堆空值,这是MySQL里很小却很烦的一个点。

2.2 数据流闭环:入库单、出库单、库存表怎么协作

业务主流程是个闭环:创建入库单→审核→写明细→累加库存→写日志;出库则反过来:创建出库单→校验库存→扣减库存→写明细→写日志。很多压缩包代码跳过了“审核”这一步,点保存就落库,录错数据没有后悔药。我改造时习惯加一个Status字段:0草稿、1已审核、2已作废,这才有挽回空间。

在这个闭环里,Stock不是业务流水表,而是快照表。期初数量只初始化一次,之后每次业务都通过更新来变化。写入库时要特别注意:不要在插入明细后先“SELECT Quantity FROM Stock WHERE ProductId=@pid”,再用取到的值在C#里算好新库存去UPDATE,这会在并发下丢更新。正确做法是让MySQL自己原子加减:

UPDATE Stock SET Quantity = Quantity + @delta, UpdateTime = NOW() WHERE ProductId = @pid;

逻辑说明:这条语句让数据库在锁行状态下完成增量更新,而不是应用程序先读再写。只要Stock表里已经存在该商品的期初行,每次入库都会在此基础上做加法,天然避免两个窗口互相覆盖。

整个闭环的核心是事务。一次入库涉及到主表插入、明细多行插入、库存更新、日志插入四件事,任何一步失败都要整体回滚,否则就会出现“单据没了、库存多了”的灵异现象。在C#里用MySqlConnection.BeginTransaction()包裹所有命令,最后统一Commit,catch里Rollback,第4章会给完整写法。这个做法不只在入库,出库、盘点、调拨都适用。

2.3 C#连接MySQL:驱动选型与连接字符串的关键参数

C#连MySQL最常见的是MySql.Data(官方Connector/NET),NuGet包名就是MySql.Data;另一条路是MySqlConnector,API几乎兼容、异步支持更好。压缩包自带的项目如果引用了MySql.Data.dll,优先保留它,改动最小;如果要新写连接层,我更倾向MySqlConnector,线程安全性和异步场景表现更好。

连接字符串是第一个卡点,最常见问题集中在SSL、字符集和版本匹配上。仓库系统部署在局域网,我常用这样一段:

Server=localhost;Port=3306;Database=wms;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;AllowPublicKeyRetrieval=True;

逻辑说明:Server和Port指向MySQL地址,本机部署用localhost:3306;Database填导入后的库名;Uid/Pwd是MySQL账号;CharSet=utf8mb4是为了中文不乱码;SslMode=None跳过握手期的SSL协商,适合纯内网部署;AllowPublicKeyRetrieval=True专治MySQL 8.0下“Public Key Retrieval is not allowed”的报错。

参数说明里最值得记的两项是CharSet和SslMode。很多项目跑起来中文显示成问号,不是代码问题,是连接串没声明utf8mb4;而MySQL 5.7的默认字符集可能是utf8,导入的sql文件头部如果没有SET NAMES、表结构还是老字符集,两边不一致就会出现乱码。驱动版本上,MySQL 5.7配MySql.Data 8.0.x通常没问题;反过来MySQL 8.0配很老的6.x驱动,会报“Character set 'utf8mb4' is not supported”,优先升驱动再排查代码。网上mysql安装教程很多,这里只看两个决策点:版本选5.7还是8.0,认证插件是否兼容——8.0的默认认证方式,第5章会专门说。

3. 从压缩包到能点开的登录框:初始化MySQL、导入数据库文件与还原C#工程

3.1 环境准备:MySQL 5.7/8.0选型和Visual Studio框架核实

动手前先确认两件事:MySQL版本和.NET框架版本。压缩包里的数据库文件,通常是用mysqldump导出的.sql,或者用Navicat for MySQL备份出来的备份包。前者直接命令行导入,后者需要用Navicat的还原功能。如果是.sql,MySQL 5.7和8.0基本都能导,但要注意sql文件里有没有MySQL 8.0特有的排序规则,比如utf8mb4_0900_ai_ci,5.7不认识,会报1273错误。我开发时习惯用5.7.44做测试,交付到客户那边再按其现有版本决定,避免导出文件自带新语法。

Visual Studio与.NET框架也要对齐。老压缩包大多是.NET Framework 4.5或4.6.2的WinForms工程,用Visual Studio 2019/2022打开时通常会自动重新定位到4.7.2。如果提示目标框架不受支持,就在项目属性里改成4.7.2再还原NuGet包。另一个隐藏变量是平台位数:MySQL驱动是32位的,生成平台要选x86;如果项目里引用了64位报表组件,则选x64。这个设置错了不会编译报错,而是运行时抛“未能加载文件或程序集”,是最迷惑人的错误之一。

3.2 导入数据库文件:命令行建库、导入与常见报错

拿到.sql文件后,我习惯先建空库、再重定向导入,不直接用source。这样不需要关心sql里有没有CREATE DATABASE,也能避免导入到错误的库。

# 先建库,再导入,字符集统一utf8mb4 mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS wms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p wms < D:\project\wms\db\wms.sql

逻辑说明:第一步建库时显式声明字符集和排序规则,避免继承MySQL默认的latin1;第二步把.sql文件重定向进wms库。如果sql文件内部带有USE wms或CREATE DATABASE语句,也不冲突,只是最终库名要跟连接串保持一致。

参数说明:-u指定用户,-p提示输密码,避免把密码留在cmd历史里;Windows下路径不要带中文,否则cmd代码页容易解析失败。导入遇到ERROR 1064时,先检查文件开头有没有BOM——用记事本另存为UTF-8无BOM再试;遇到ERROR 1366是字符集不匹配,字段里的中文无法转成目标字符集,解决思路是把库、表、字段三级字符集统一成utf8mb4。

3.3 修改连接字符串:App.config配置与第一次编译

导入成功后,打开Visual Studio里的App.config,把连接串填进connectionStrings节点。老项目常用这样的配置:

<!-- wms数据库连接,字符集utf8mb4,内网可关SSL --> <connectionStrings> <add name="WMSConnString" connectionString="Server=localhost;Port=3306;Database=wms;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;AllowPublicKeyRetrieval=True;" providerName="MySql.Data.MySqlClient"/> </connectionStrings>

逻辑说明:name是代码里读取时的键,用ConfigurationManager.ConnectionStrings["WMSConnString"].ConnectionString拿连接串。providerName要和实际引用驱动匹配;如果老项目用的是ODBC方式,连接串要换成Driver={MySQL ODBC 8.0 Unicode Driver};Server=...;Database=...的形式,两者不能混。

参数说明:Persist Security Info默认不开;连接池参数Connection Lifetime、Min Pool Size这些,等真出现连接数过多再调,不要一上来就堆一堆配置。编译期最常见的三类错误:一是找不到MySql.Data命名空间,在NuGet里重新安装MySql.Data;二是ConfigurationManager未引用,添加System.Configuration引用;三是SQL里的表名不存在,回第2章核对表名。第一次启动若能弹出登录框但点登录报错,多在Navicat里用同一账号测一次,先排除MySQL侧问题。

4. 把入库、出库、库存查询改成能上线的代码:事务、参数化与界面刷新

4.1 入库单保存逻辑:事务包裹主从表、原子更新库存

入库动作是仓库系统的第一个核心,也是坏代码重灾区。常见做法是用户在主界面填单头,DataGridView里录明细,点保存一次性写主表和明细表。我会把整个过程包进事务,确保主表和明细要么都在、要么都不在。这段代码可以直接照着改:

using (var conn = new MySqlConnection(connString)) { conn.Open(); var tx = conn.BeginTransaction(); try { // 主表插入,同时取回自增ID作为明细外键 string sqlMain = @"INSERT INTO InStock(InStockNo, SupplierId, Operator, InStockDate, Status) VALUES(@no, @supplierId, @operator, @date, @status); SELECT LAST_INSERT_ID();"; var cmdMain = new MySqlCommand(sqlMain, conn, tx); cmdMain.Parameters.AddWithValue("@no", "RK" + DateTime.Now.ToString("yyyyMMddHHmmss")); cmdMain.Parameters.AddWithValue("@supplierId", supplierId); cmdMain.Parameters.AddWithValue("@operator", currentUser); cmdMain.Parameters.Add("@date", MySqlDbType.DateTime).Value = inStockDate; cmdMain.Parameters.AddWithValue("@status", 1); int mainId = Convert.ToInt32(cmdMain.ExecuteScalar()); foreach (var item in details) { // 明细插入并原子更新库存,不依赖期初行是否存在 string sqlDetail = @"INSERT INTO InStockDetail(InStockId, ProductId, Quantity, Price, TotalAmount) VALUES(@inStockId, @productId, @quantity, @price, @totalAmount); INSERT INTO Stock(ProductId, Quantity, UpdateTime) VALUES(@productId, @quantity, NOW()) ON DUPLICATE KEY UPDATE Quantity = Quantity + @quantity, UpdateTime = NOW();"; var cmdDetail = new MySqlCommand(sqlDetail, conn, tx); cmdDetail.Parameters.AddWithValue("@inStockId", mainId); cmdDetail.Parameters.AddWithValue("@productId", item.ProductId); cmdDetail.Parameters.AddWithValue("@quantity", item.Quantity); cmdDetail.Parameters.AddWithValue("@price", item.Price); cmdDetail.Parameters.AddWithValue("@totalAmount", item.Price * item.Quantity); cmdDetail.ExecuteNonQuery(); } tx.Commit(); } catch { tx.Rollback(); throw; } finally { tx.Dispose(); } }

逻辑说明:两条SQL在一个事务里,先取主表自增ID作为明细外键,再逐行写入明细并更新库存。更新库存用了INSERT...ON DUPLICATE KEY UPDATE,不依赖期初数据是否存在;即使Stock表里没这商品的行,也会自动建行。事务对象单独声明而不放进using,确保Rollback和Commit的时序可控。

参数说明:日期参数显式指定了MySqlDbType.DateTime,而不是用AddWithValue靠驱动推断,避免“string到datetime转换失败”。单号用“RK+时间戳”生成,简单但会撞单,若同一秒保存两单,需要加随机后缀或使用数据库自增序列。这段代码里最值得记住的是:主表、明细、库存的写入全部挂在同一个tx上,任何一行抛异常,catch里的Rollback会把前面的操作全部撤销。

4.2 库存扣减的两种做法:先查再改是最容易翻车的写法

出库单的核心是扣库存。行业内对“扣减”有两条路线:先查再改、原子更新。先查再改是网上最常见也最容易翻车的写法:

string sqlCheck = "SELECT Quantity FROM Stock WHERE ProductId=@pid"; int qty = Convert.ToInt32(ExecuteScalar(sqlCheck, pid)); if (qty >= need) { ExecuteNonQuery("UPDATE Stock SET Quantity = Quantity - @need WHERE ProductId=@pid", pid, need); }

逻辑说明:这段逻辑在单用户试跑时完全正常,但两个操作员同时录出库单时,两人都查到库存够,然后都执行扣减,最后库存变负,账面全乱。原因是SELECT和UPDATE之间存在时间窗口,既没有事务保护,也没有行锁。

参数说明:需要need、pid两个参数,但无论怎么传参,这个写法本质上就是非原子的“检查-更新”。正确的替代方案是把条件写进UPDATE:UPDATE Stock SET Quantity = Quantity - @need WHERE ProductId = @pid AND Quantity >= @need,然后检查ExecuteNonQuery的影响行数,返回1才表示扣减成功;如果返回0,说明库存不足,整体回滚。更严格的做法是SELECT ... FOR UPDATE加事务,但对小仓库来说,带条件的UPDATE配合事务已经够用,代码也容易让别人看懂。

4.3 DataGridView刷新、跨线程更新与列名绑定的坑

界面层最常见的问题是:保存后列表不刷新、列头是英文、后台查询一卡界面就假死。我习惯把库存查询结果放进DataTable,再赋值给dataGridView.DataSource。保存结束必须重新查询,不要自己在界面上改单元格——那是在骗自己,下一次查询又会把旧数据盖回来。

绑定前处理列头有三条路:SQL别名写中文、DataGridView列头写中文、动态生成列。我倾向保留英文列名,在ColumnHeaderText里设置中文,这样SQL变动不影响界面。还有一个高频坑:AutoGenerateColumns默认打开时,SQL里只要多select一个字段,界面就会多出一列,排查界面多了列时先看SQL返回了哪些列。

跨线程更新控件是另一个让人抓狂的问题。一旦在BackgroundWorker或Task里执行查询,回填DataGridView必须用Invoke回到UI线程。很多做上位机开发的在这翻过车:后台线程直接操作控件,抛“跨线程操作无效”。仓库系统数据量虽小,但查询超过几千行就可能卡界面,提前写好Invoke封装比晚点补要省事得多。我一般会把“设置DataSource”和“刷新显示”封装成两个方法,后台线程统一调Invoke版本,界面就再没因为刷新闪退过。

5. 部署到内网后的高频避坑记录:现象、原因、处理

5.1 中文乱码:登录正常,商品名显示成问号

现象:功能都能用,但所有中文显示为??,或Navicat里看正常、软件里看乱码。

原因:字符集链路有一环不一致就会乱码:数据库表用utf8mb4、连接串CharSet用utf8、sql文件内容又是latin1,三个环节只要有一个错位,结果必乱。

处理:先确认三处:SHOW VARIABLES LIKE 'character_set_database';、sql文件头部的SET NAMES、连接串的CharSet。然后统一成utf8mb4:

ALTER DATABASE wms CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE Product CONVERT TO CHARACTER SET utf8mb4;

改完重连,再查一次,不要改了数据库忘了改连接串,也不能反过来。

5.2 MySQL 8.0认证插件导致C#连接失败

现象:命令行、Navicat都能连,唯独C#程序报“Authentication method 'caching_sha2_password' not supported”,或者“Public Key Retrieval is not allowed”。

原因:MySQL 8.0默认用户认证插件是caching_sha2_password,老驱动不认。

处理:两条路。优先升级驱动并加AllowPublicKeyRetrieval=True;无法升级驱动时,把应用账号改回老认证方式:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;

这里建议单独建一个wms_app账号,只授权wms库,不给整个实例权限,别拿root裸奔。

5.3 导入.sql文件报错:BOM、1064、1273

现象:命令行导入抛ERROR 1064 (42000),或者ERROR 1273 (HY000) Unknown collation。

原因:1064多半是文件被文本编辑器加过BOM,首条语句解析失败;1273是sql文件里的排序规则在低版本MySQL不存在。

处理:1273把utf8mb4_0900_ai_ci替换成utf8mb4_general_ci再导入;1064先把文件转成UTF-8无BOM。用Navicat备份出来的.bak文件不能命令行导入,用Navicat的备份还原功能,路径不要带中文。

5.4 杀毒软件把编译好的exe当木马

现象:Debug或Release目录下的exe一生成就被隔离,换台电脑拷贝过去同样报。

原因:.NET编译产物体积小、结构固定,容易被启发式引擎误判;某些旧版第三方dll被标记过威胁特征,整个exe连坐。

处理:先全量编译一次,关掉杀毒看是否只有exe被隔离;再把第三方dll逐个移除排查特征;最后给发布目录加白名单。发布时用强签名能降低误报率但不是100%杜绝。确认代码是自己的,再决定是否点信任,建议不要让杀毒软件长期关闭。

5.5 重复扣减:单据一张、库存少了两次

现象:用户连点两次保存,或网络卡顿后重试,数据库里出现两条相同单据,库存被扣两次。

原因:按钮没有防重复提交,事务各自独立,第一次事务还没提交完成时第二次操作又进来了。

处理:界面层保存时把按钮置灰,finally中恢复;更硬的办法是业务层加幂等控制,在单据主表加BusinessNo唯一索引,重复插入直接顺事务回滚。两个办法同时上,不要只靠界面置灰,因为异常抛太快时置灰不一定来得及恢复。日志表里记录每次保存的请求ID,排查时能顺着它把重复动作定位到秒级。

6. 上线前我最后会做的三处小改动:索引、备份和操作留痕

第一处是给高频查询加索引。库存查询页通常一打开就查全表,商品名模糊查询在上千行数据后会明显拖慢响应。我会在Product表ProductName上加普通索引,在出入库明细表的(InStockId, ProductId)上建联合索引,Stock表ProductId上建唯一索引。改完用EXPLAIN SELECT验证是否走索引。这个改动不动代码,但对体验提升最明显。

第二处是定备份策略。mysqldump每天凌晨导一次,保留近7天。Windows任务计划里可以放这样一条命令:

mysqldump -uroot -p --default-character-set=utf8mb4 --single-transaction --quick wms > D:\backup\wms_%date:~0,4%%date:~5,2%%date:~8,2%.sql

参数说明:--single-transaction在InnoDB下用一致性快照,不锁业务表;--quick逐行输出,减少内存占用;后面的%date:~0,4%是在cmd里截取系统日期的年、月、日,避免日期带斜杠导致文件名非法。恢复时用mysql -uroot -p wms < 备份文件,库名要和连接串一致,正好衔接第3章的导入步骤。

第三处是补操作留痕。很多压缩包项目只有登录日志,没有业务日志。我习惯补一张OperationLog,字段只要Id、Operator、Action、TargetId、CreateTime,在每个写操作后面插一行。这个习惯救过我一次:用户说库存对不上账,查到最后是误删了期初记录,没有日志根本无从定位。日志不用复杂,重在“每个写操作都记”,而不是只在登录时记一下。

说到底,这类压缩包项目的价值在表结构和业务闭环,不在那几行界面代码。先按第2章读懂表,第3章把它跑通,再谈改造。MySQL版本、字符集、驱动版本是三个隐藏变量,任何一个不对,代码再好也白搭。我自己的排查口令是“先查版本、后查连接、再查事务”,这套口令帮我把无数玄学问题变成了具体报错。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 1:12:37

Zynq-7020嵌入式ISP图像处理实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:12:14

Python循环结构实验全解析:7个实战关卡攻克for与while

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:11:33

企业级ELK日志系统设计与落地实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:11:12

FPGA数字钟综合实验:从Quartus II到硬件落地的全链路工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:10:26

CTF中Sylvester结式法实战:多项式公共根快速判定与求解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:10:07

C#宿舍管理系统开发实战:数据库设计、登录权限与入住退宿

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华