简介这份资源是面向软件开发初学者与课程实践者的校园一卡通系统完整项目源码基于 Visual Studio 2005 与 SQL Server 2005 构建覆盖卡片管理、用户信息管理、交易记录查询等核心模块可作为理解基础开发流程与数据库设计的实战案例。压缩包共 87 个文件约 2.5MB以 30 个 C# 源码文件、12 个 resx 资源文件及若干配置文件、可执行文件为主另附需求说明书、详细设计说明书、数据字典等 docx 文档以及 SQL 建库脚本、mdf/ldf 数据库文件、Power Designer 物理与概念数据模型文件便于对照学习表结构设计与建模思路。目前已有 362 人学习。读者可从中获取需求分析、界面构建、前后端交互、事务处理与权限管理等环节的参考实现适合用于课程设计参考与知识点查漏补缺。1. 校园一卡通系统一个还在用 VS2005 和 SQL2005 的老项目为什么值得拆前阵子帮一个做教务信息化的朋友看他们机房里的老系统屏幕上赫然是 Visual Studio 2005 的启动画面数据库连的是 SQL Server 2005。他问我这套校园一卡通系统还能不能救我打开解决方案一看几十个窗体、一堆存储过程、串口读卡器调用逻辑全在 UI 层里。说实话这种项目在不少学校后勤和信息化部门里还真实存在而且往往还在跑着最核心的消费、充值和门禁数据。这套源码的价值不在于技术多新而在于它把「一卡通」这个业务闭环——发卡、充值、消费、挂失、补卡、报表——用最直白的方式写全了。适合两类人一是要接手或维护同类老系统的开发者二是想拿一个完整业务系统练手、理解 C/S 架构和数据库设计的学生或初级工程师。下面我按「能干什么、怎么跑、怎么改」的顺序把这份源码拆开讲。2. 技术栈与整体架构WinForms 三层结构到底怎么落的2.1 技术选型为什么是 VS2005 SQL2005 这套组合这套系统是典型的 .NET Framework 2.0 时代的产物。客户端用 WinForms 做界面数据访问走 ADO.NET数据库是 SQL Server 2005。放到今天看确实老但它的好处是依赖极简一台装 Windows Server 2003/2008 的机器装 SQL2005客户端机器装 .NET Framework 2.0 运行时就能跑不需要浏览器、不需要额外中间件。对于校园里那种内网环境、机器配置参差不齐的场景这种「胖客户端 集中数据库」的模式反而省事。从工程结构看解决方案里一般会拆成几个项目界面层主窗体、各业务窗体、业务逻辑层BLL、数据访问层DAL再加一个公共类库放实体和工具方法。这是当年最流行的「三层架构」写法虽然很多老项目会把逻辑直接写在窗体事件里但这份源码如果结构完整应该能看到 BLL 和 DAL 的分离。判断方法很简单打开解决方案资源管理器看有没有独立的 BLL、DAL、Model 项目或者至少是三个命名空间清晰的文件夹。提示如果你拿到的版本窗体代码里直接拼 SQL 字符串别急着骂先确认是不是「简化版」。很多教学或外包项目为了赶工期会把三层压成两层改造时第一件事就是把数据访问抽出来。2.2 目录结构拿到源码先看这几个地方一份典型的校园一卡通系统源码目录大致长这样不同版本命名会有差异但职责对应CampusCard/ ├── CampusCard.sln # 解决方案文件VS2005 打开入口 ├── CampusCard.UI/ # 界面层登录、主界面、各业务窗体 │ ├── FrmLogin.cs │ ├── FrmMain.cs │ ├── CardManage/ # 发卡、挂失、补卡 │ ├── Consume/ # 消费、充值 │ └── Report/ # 报表查询 ├── CampusCard.BLL/ # 业务逻辑层 ├── CampusCard.DAL/ # 数据访问层SQL 和存储过程调用 ├── CampusCard.Model/ # 实体类对应数据库表 ├── DB/ # 建库脚本、初始数据 │ └── CampusCard.sql └── Config/ # 连接字符串、读卡器参数拿到源码后先看DB目录有没有建库脚本这是能不能跑起来的关键。再看Config或App.config里的连接字符串确认数据库实例名和账号。最后看CampusCard.UI的启动窗体是哪个通常是Program.cs里的Application.Run(new FrmLogin())。2.3 数据库设计一卡通的核心表就那几张一卡通的业务本质是「账户 流水 卡片状态」。核心表一般包括表名作用关键字段CardInfo卡片档案CardNo、StudentNo、Status、BalanceAccount账户余额AccountID、CardNo、BalanceConsumeLog消费流水LogID、CardNo、Amount、ConsumeTime、TerminalNoRechargeLog充值流水LogID、CardNo、Amount、OperIDOperator操作员OperID、OperName、Password、Role这里有个设计细节值得注意余额到底存在CardInfo还是Account规范做法是单独一张账户表卡片只是介质挂失补卡时换卡不换账户余额跟着账户走。如果源码里余额直接挂在卡片表上补卡逻辑就会很别扭这是二次开发时要留意的点。3. 把项目跑起来从建库到登录的完整步骤3.1 环境准备别在 Win10 上硬装 VS2005VS2005 在 Win10/Win11 上安装经常卡在兼容性检查血泪经验是要么用虚拟机装 Windows XP/7要么直接用 VS2008 或更高版本打开会提示升级向导选「不升级」通常也能编译 .NET 2.0 项目。SQL Server 2005 同理新系统上装不上就换 SQL Server 2008 R2 或 2012建库脚本基本兼容注意把连接字符串里的实例名改对。需要准备的东西Visual Studio 2005或能打开 .NET 2.0 项目的更高版本SQL Server 2005 及以上.NET Framework 2.0 运行时如果只跑不编译读卡器驱动没有硬件也能跑消费和查询功能不依赖读卡器3.2 建库与初始化数据打开 SQL Server Management Studio新建一个数据库比如CampusCardDB然后执行DB/CampusCard.sql。如果脚本里没有CREATE DATABASE语句就先手动建库再执行表结构部分。-- 建库如果脚本里没有 CREATE DATABASE CampusCardDB; GO USE CampusCardDB; GO -- 核心表账户表余额跟账户走不跟卡片走 CREATE TABLE Account ( AccountID INT IDENTITY(1,1) PRIMARY KEY, CardNo VARCHAR(20) NOT NULL, Balance DECIMAL(10,2) NOT NULL DEFAULT 0, Status TINYINT NOT NULL DEFAULT 0 -- 0正常 1挂失 2注销 ); GO -- 消费流水表TerminalNo 记录消费终端便于对账 CREATE TABLE ConsumeLog ( LogID INT IDENTITY(1,1) PRIMARY KEY, CardNo VARCHAR(20) NOT NULL, Amount DECIMAL(10,2) NOT NULL, ConsumeTime DATETIME NOT NULL DEFAULT GETDATE(), TerminalNo VARCHAR(20) NULL ); GO执行完表结构后检查有没有初始操作员数据。登录一般要查Operator表如果没有初始账号手动插一条INSERT INTO Operator (OperName, Password, Role) VALUES (admin, admin123, 1);密码如果是明文存储先别改等跑通再考虑加密否则登录逻辑对不上会白折腾半天。3.3 配置连接字符串并编译运行打开App.config或Config目录下的配置文件找到连接字符串connectionStrings add nameConnStr connectionStringData Source.;Initial CatalogCampusCardDB;User IDsa;Password你的密码 providerNameSystem.Data.SqlClient / /connectionStringsData Source.表示本机默认实例如果是命名实例就写.\SQLEXPRESS。改完后用 VS 打开解决方案先「生成解决方案」看有没有缺引用。老项目常见的报错是System.Windows.Forms引用丢失或第三方读卡器 DLL 找不到前者重新添加引用即可后者把 DLL 放到输出目录或去掉相关调用。编译通过后按 F5 启动出现登录窗体说明链路通了。用刚才插入的 admin 账号登录进主界面后先测「查询余额」和「消费」两个不依赖硬件的功能确认数据库读写正常。注意如果登录后主界面一片空白或报「未将对象引用设置到对象的实例」多半是某个初始化方法里读了不存在的配置项用断点跟一下FrmMain_Load就能定位。4. 核心模块拆解消费、充值和挂失的代码链路4.1 消费扣款一个事务里必须做完的三件事消费是一卡通最高频的操作逻辑看着简单——查余额、扣钱、写流水——但顺序和事务处理错了就会出对账问题。规范的写法是把三步放在同一个事务里public bool Consume(string cardNo, decimal amount, string terminalNo) { using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { // 1. 查余额并加行锁防止并发重复扣款 string sqlCheck SELECT Balance FROM Account WITH (UPDLOCK) WHERE CardNoCardNo AND Status0; SqlCommand cmdCheck new SqlCommand(sqlCheck, conn, tran); cmdCheck.Parameters.AddWithValue(CardNo, cardNo); object balanceObj cmdCheck.ExecuteScalar(); if (balanceObj null) { tran.Rollback(); return false; } decimal balance Convert.ToDecimal(balanceObj); if (balance amount) { tran.Rollback(); return false; } // 2. 扣减余额 string sqlUpdate UPDATE Account SET BalanceBalance-Amount WHERE CardNoCardNo; SqlCommand cmdUpdate new SqlCommand(sqlUpdate, conn, tran); cmdUpdate.Parameters.AddWithValue(Amount, amount); cmdUpdate.Parameters.AddWithValue(CardNo, cardNo); cmdUpdate.ExecuteNonQuery(); // 3. 写消费流水 string sqlLog INSERT INTO ConsumeLog(CardNo,Amount,TerminalNo) VALUES(CardNo,Amount,TerminalNo); SqlCommand cmdLog new SqlCommand(sqlLog, conn, tran); cmdLog.Parameters.AddWithValue(CardNo, cardNo); cmdLog.Parameters.AddWithValue(Amount, amount); cmdLog.Parameters.AddWithValue(TerminalNo, terminalNo); cmdLog.ExecuteNonQuery(); tran.Commit(); return true; } catch { tran.Rollback(); throw; } } } }这段代码的关键在WITH (UPDLOCK)消费终端可能多台同时刷同一张卡比如食堂两个窗口不加锁会出现两个线程都读到 100 元、各扣 80 元、最后余额变成 -60 的情况。UPDLOCK在读取时就持有更新锁第二个事务会等待保证串行扣款。参数terminalNo用来记录哪台机器扣的后期对账时能定位问题终端。4.2 充值先写流水再改余额还是反过来充值的顺序和消费相反但同样要事务。常见做法是先插充值流水拿到流水号再更新余额这样即使更新失败流水也能作为审计依据。不过更稳妥的是反过来先更新余额成功后再写流水因为流水是「结果记录」余额才是「事实」。两种都能接受关键是整个操作原子化。public bool Recharge(string cardNo, decimal amount, int operId) { using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { // 更新余额同时校验账户状态 string sql UPDATE Account SET BalanceBalanceAmount WHERE CardNoCardNo AND Status0; SqlCommand cmd new SqlCommand(sql, conn, tran); cmd.Parameters.AddWithValue(Amount, amount); cmd.Parameters.AddWithValue(CardNo, cardNo); int rows cmd.ExecuteNonQuery(); if (rows 0) { tran.Rollback(); return false; } // 卡不存在或已挂失 // 写充值流水OperID 记录操作员便于追责 string sqlLog INSERT INTO RechargeLog(CardNo,Amount,OperID) VALUES(CardNo,Amount,OperID); SqlCommand cmdLog new SqlCommand(sqlLog, conn, tran); cmdLog.Parameters.AddWithValue(CardNo, cardNo); cmdLog.Parameters.AddWithValue(Amount, amount); cmdLog.Parameters.AddWithValue(OperID, operId); cmdLog.ExecuteNonQuery(); tran.Commit(); return true; } catch { tran.Rollback(); throw; } } } }rows 0这个判断很重要如果卡已挂失Status1UPDATE 不会命中任何行直接回滚避免给挂失卡充值。operId从登录会话里取别从界面传防止被篡改。4.3 挂失与补卡换卡不换账户的实现挂失的本质是把账户状态置为挂失补卡则是给同一个账户绑定新卡号。如果数据库设计时余额挂在卡片表上这一步就得做「余额迁移」容易出错。正确做法是账户表独立补卡只改CardNo关联public bool ReportLoss(string cardNo) { string sql UPDATE Account SET Status1 WHERE CardNoCardNo AND Status0; // 执行后返回受影响行数0 表示卡不存在或已挂失 } public bool ReissueCard(string oldCardNo, string newCardNo) { // 补卡账户不变只换卡号余额自动跟随 string sql UPDATE Account SET CardNoNewCardNo, Status0 WHERE CardNoOldCardNo AND Status1; // 同时要在 CardInfo 表里登记新卡旧卡置为作废 }这里有个业务规则要确认补卡收不收工本费如果收就在补卡事务里再加一条消费流水类型标记为「补卡费」。源码里如果没有这个逻辑二次开发时可以按学校实际政策补上。5. 避坑与排查老项目跑不起来时先看这几条5.1 现象编译报「未能加载文件或程序集」→ 原因第三方 DLL 缺失或版本不对 → 解决统一放输出目录老项目经常引用读卡器厂商的 DLL这些 DLL 可能没随源码一起给。先看「引用」里有没有黄色感叹号有的话找到对应 DLL 重新添加或者把相关调用用#if条件编译暂时屏蔽先让主流程跑起来。另外注意平台目标VS2005 默认 AnyCPU但读卡器 DLL 可能是 32 位的要在项目属性里把目标平台改成 x86否则运行时报「BadImageFormatException」。5.2 现象登录提示「sa 登录失败」→ 原因SQL2005 默认禁用 sa 或没开混合认证 → 解决改认证模式或建专用账号SQL Server 2005 安装时如果选了「Windows 身份验证」sa 是禁用的。打开 SSMS右键服务器属性 → 安全性 → 选「SQL Server 和 Windows 身份验证模式」重启服务再启用 sa 并设密码。更规范的做法是建一个专用登录名只给CampusCardDB的读写权限别用 sa 连业务库。5.3 现象消费时余额偶尔变成负数 → 原因没加锁或事务隔离级别太低 → 解决查询加 UPDLOCK事务用默认隔离级别这就是 4.1 里说的并发问题。除了UPDLOCK还要确认连接没有设置ReadUncommitted否则脏读会让余额判断失效。排查方法开两个 SSMS 窗口模拟并发扣款看是否有一个失败回滚。如果两个都成功且余额为负就是锁没生效。5.4 现象报表查询特别慢 → 原因流水表没建索引全表扫描 → 解决在 CardNo 和 ConsumeTime 上建索引消费流水表数据量涨得很快一年几十万条很正常。查询某张卡的消费记录时如果CardNo没索引就是全表扫。补一个非聚集索引CREATE NONCLUSTERED INDEX IX_ConsumeLog_CardNo_Time ON ConsumeLog (CardNo, ConsumeTime DESC);按时间倒序查最近消费时这个索引能直接命中。注意别在流水表上建太多索引写入会变慢一两个覆盖常用查询就够了。5.5 现象换机器部署后中文乱码 → 原因数据库排序规则或连接字符串字符集不一致 → 解决统一用 Chinese_PRC_CI_AS建库时如果排序规则选了默认的SQL_Latin1_General中文会存成问号。检查方法SELECT SERVERPROPERTY(Collation)正常应该是Chinese_PRC_CI_AS。已经建错的库改排序规则比较麻烦简单办法是导出数据、重建库、再导入或者只对出问题的列改排序规则。6. 二次开发从「能跑」到「能用在真实校园」的几个改造点接手这种老系统我的习惯是先不动业务逻辑先做三件低风险、高收益的事再谈功能扩展。第一件是把连接字符串和读卡器参数从硬编码挪到配置文件。很多老项目在 DAL 里写死Data Source192.168.x.x换环境就要改代码重编译。统一收到App.config的connectionStrings和appSettings里部署时只改配置不改代码这是最省事的改造。第二件是给关键操作加日志。消费、充值、挂失这些涉及钱的接口在 BLL 层入口和出口各记一条日志写文件或写一张OperLog表都行。老系统出问题时最怕「黑匣子」有了日志对账差异能快速定位到是哪台终端、哪个操作员、哪个时间点。第三件是补一个数据校验脚本。每天凌晨跑一次检查「账户余额 当日消费 - 当日充值」是否和流水汇总一致不一致就告警。这个脚本不复杂但能提前发现并发扣款、事务回滚不彻底这类隐蔽问题。功能扩展方面最常见的需求是加「消费限额」和「报表导出 Excel」。限额可以在Consume方法里加一个当日累计判断查ConsumeLog当天该卡的消费总额超过阈值就拒绝。导出 Excel 在 .NET 2.0 下没有现成的 OpenXML常见做法是引用Microsoft.Office.Interop.Excel或者直接拼 CSV后者更轻兼容性也好。改造时有个原则先写测试用例再改代码。老系统没有单元测试至少给Consume、Recharge、ReportLoss三个方法各写一个控制台调用的验证程序改完跑一遍确认余额和流水都对得上。从那以后我每次动这种涉及金额的老代码都强制先跑一遍手工对账脚本再提交吃过一次「改完余额对不上、查了两天才发现是事务没提交」的亏。希望这些拆解能帮到你拿到源码后先从建库和登录跑通开始别一上来就想着重构。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
C++结构体内存对齐与填充,为什么sizeof结果和成员总和不一样? 很多人觉得 sizeof 就是把所有成员大小相加,写代码的时候经常被结构体对齐狠狠打脸。CPU访问内存的时候,对齐地址访问效率更高。所以编译器默认会给结构体做内存对齐、内存填充。对齐规则核心两条:1. 成员的起始地址,必须是自身大… · 2026/11/5 5:41:10
NC6X root密码重置实战:从备份到验证的完整操作指南 简介:面向NC6X系统运维人员与服务器管理员的一款root密码修改辅助工具,聚焦系统最高权限账户的安全管理场景。root账户在Linux/Unix体系中权限最高,密码一旦遗忘或需定期更换,往往涉及权限管理、密码策略、审计日志与应急恢复等多… · 2026/11/5 5:41:10
Leap Motion手势投篮:从坐标映射到得分判定 简介:面向Unity与体感交互初学者及VR/AR原型开发者的LeapMotion投篮案例,围绕手势识别驱动虚拟篮球的完整流程展开,可用于课堂演示、课程设计或个人练习。压缩包约35.43MB,文件总数显示为0且类型明细暂无数据,上游文件… · 2026/11/5 5:40:10
免费降ai软件实测:先免费降AI 1000字,满意后整篇降,维普AI率96.46%降到0% /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 8:03:55
老房翻新报价单怎么看,拆除清运和基层处理有没有单列 老房翻新报价单要看的不是总价,而是拆除清运和基层处理有没有单列出来,这两项一旦被混进综合项或干脆不写,后面拆开墙面、铲掉旧地面时,报价就会越变越长。 深圳市星居易装饰有限公司2017年起在深圳经营,专注房龄十年以… · 2026/11/5 8:03:55
七千亿排队入场,钱的关键在于穿过项目之后 《七千亿排队入场,钱的关键在于穿过项目之后》
——资金投放决定起点,订单与收入决定内需回暖的力度先说一个让人坐不住的数字:今年八千亿元的政策性金融工具,到四季度还有约七千亿元没有投出去。换句话说,全年额度的近… · 2026/11/5 8:03:55
嘎嘎aigc降重免费入口:降重降AI一起做,新用户免费降AI 1000字 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 8:03:55
三层交换机与路由器:核心概念与定位 一、核心概念与定位三层交换机(Layer 3 Switch)和路由器(Router)是构建现代网络的关键设备,但它们的设计目标和工作层级不同。三层交换机本质上是“带路由功能的二层交换机”,它主要工作在数据链路层&#… · 2026/11/5 8:03:55
【量化实战】VeighNa (vnpy) 回测“无结果”排雷指南:从 GUI 到 CLI 的完美对齐 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 8:02:55
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38