C# Winform影院售票系统数据库设计与并发锁座实战
简介这是一套面向C#初学者与课程设计者的Winform影院售票管理系统完整源码基于VS2012与SQL Server 2012开发适合作为毕业设计、课程大作业或Winform窗体编程练手项目。压缩包共93个文件约4.13MB以40个cs源码文件为核心配合15个resx与15个resources资源文件、3个exe可执行程序、3个config配置文件并附带MovieSystem.mdf与ldf数据库文件可直接附加到SQL Server运行省去建库建表环节。系统涵盖登录、影片管理、影厅排片、售票、会员与用户管理等多个窗体模块目录中按Model、Properties、images等分层组织结构清晰便于按功能模块阅读与二次修改。目前已有1405人学习下载读者可借此掌握Winform界面布局、数据库连接与增删改查的完整实现思路快速搭建可运行的售票业务原型。1. 影院售票管理系统从一张座位图说起影院售票这个场景看起来简单——选座、下单、出票三步走完。但真正动手做过的人都知道难点从来不在界面上那几个按钮而在“同一个座位不能卖给两个人”这件事上。我见过太多 Winform 项目界面做得漂漂亮亮结果两个人同时点同一个座位系统双双出票到了影院门口才发现重座。这不是代码写得丑是数据库设计和事务控制没跟上。这篇笔记围绕 C# Winform 影院售票管理系统把数据库齐全这件事拆开讲。所谓“数据库齐全”不是表多就行而是座位状态、场次排期、订单流水、会员折扣、退票记录这几条链路都能闭环。适合正在做 Winform 项目案例的在校生也适合拿 C# 上位机经验想转业务系统的朋友。下面从表结构设计一路讲到并发锁座、报表统计和打包部署中间踩过的坑都会标出来。2. 数据库表结构怎么设计才算“齐全”2.1 六张核心表与字段取舍影院售票系统的数据库我一般会拆成六张核心表影片表、影厅表、场次表、座位表、订单表、订单明细表。会员和退票可以各加一张但骨架就是这六张。很多人一上来就想着把座位做成一个字段存字符串比如“1,2,3,5,8”这种设计在查询“某场次还剩哪些座位”时非常痛苦后期做座位图渲染也要在 C# 里反复拆字符串性能和维护都是灾难。正确的做法是座位表按“场次 座位号”一行一条记录状态字段区分可售、已售、锁定、维修。场次表关联影厅和影片订单明细表关联订单和座位。这样查余票就是一条COUNT语句锁座就是UPDATE带条件逻辑清晰。表名核心字段说明MovieMovieId, Title, Duration, PosterPath, Status影片基础信息Status 控制上架下架HallHallId, HallName, RowCount, ColCount影厅行列数决定座位图尺寸ScheduleScheduleId, MovieId, HallId, StartTime, Price场次价格可以按场次浮动SeatSeatId, ScheduleId, SeatNo, Status每场次独立座位记录Status 是关键OrderOrderId, UserId, TotalAmount, PayStatus, CreateTime订单主表PayStatus 控制支付状态OrderDetailDetailId, OrderId, SeatId, Price订单与座位的关联退票按明细走座位表的 Status 我建议用 tinyint0 可售、1 已售、2 锁定、3 维修。不要用字符串比较和索引效率差很多。场次表的 StartTime 加索引因为查“今天有哪些场次”是高频操作。2.2 建表 SQL 与索引安排下面这段 SQL 可以直接在 SQL Server 或 MySQL 里跑字段类型按 SQL Server 写MySQL 把NVARCHAR换成VARCHAR即可。注意座位表的唯一索引这是防止重座的最后一道防线。CREATE TABLE Schedule ( ScheduleId INT IDENTITY(1,1) PRIMARY KEY, MovieId INT NOT NULL, HallId INT NOT NULL, StartTime DATETIME NOT NULL, Price DECIMAL(10,2) NOT NULL, CONSTRAINT FK_Schedule_Movie FOREIGN KEY (MovieId) REFERENCES Movie(MovieId), CONSTRAINT FK_Schedule_Hall FOREIGN KEY (HallId) REFERENCES Hall(HallId) ); CREATE INDEX IX_Schedule_StartTime ON Schedule(StartTime); CREATE TABLE Seat ( SeatId INT IDENTITY(1,1) PRIMARY KEY, ScheduleId INT NOT NULL, SeatNo NVARCHAR(10) NOT NULL, Status TINYINT NOT NULL DEFAULT 0, CONSTRAINT FK_Seat_Schedule FOREIGN KEY (ScheduleId) REFERENCES Schedule(ScheduleId), CONSTRAINT UQ_Seat_Schedule_SeatNo UNIQUE (ScheduleId, SeatNo) ); CREATE INDEX IX_Seat_Schedule_Status ON Seat(ScheduleId, Status);UQ_Seat_Schedule_SeatNo这个唯一约束很关键。即使 C# 层并发控制出了漏洞数据库也会直接拒绝重复插入相当于给座位上了物理锁。IX_Seat_Schedule_Status覆盖索引让“查某场次可售座位”走索引扫描几千个座位也是毫秒级。订单表的主键用自增整数还是 GUID看场景。单机影院用自增就够了如果以后要做多影院数据同步GUID 更稳。订单号可以单独生成一个业务编号比如“日期 场次 随机数”方便对账。2.3 从选座到下单的数据流用户点座位图上的一个座位C# 端不是直接改数据库而是先发一条“锁定”请求。锁定成功后座位状态变成 2同时启动一个定时器比如 15 分钟没支付就自动释放。支付回调回来再把状态从 2 改成 1写入订单明细。退票则把状态改回 0订单明细标记退款。这个流程里座位状态流转必须和订单状态严格对应。我见过有人锁定和支付共用一个状态结果用户锁了座没付钱系统以为已售座位白白空着。状态机要画清楚0→2→1 是正常路径0→2→0 是超时释放1→0 是退票。每一步都要有日志不然出了问题查都没法查。3. 用 C# Winform 把选座界面跑起来3.1 座位图控件的动态生成Winform 里做座位图最直接的方式是用Panel加动态生成的Button或Label。每个座位一个控件点击切换选中状态。座位数量多的时候比如 20 排 × 20 列400 个控件会让界面卡顿这就是热词里说的“控件多致 Winform 卡”。解决办法是用TableLayoutPanel或者自己画。我一般用Panel的Paint事件直接画座位鼠标点击时根据坐标反算座位号。这样只有一两个控件性能好很多。下面是一个简化的绘制逻辑private void seatPanel_Paint(object sender, PaintEventArgs e) { Graphics g e.Graphics; int seatW 30, seatH 30, gap 5; for (int row 0; row rows; row) { for (int col 0; col cols; col) { Rectangle rect new Rectangle( col * (seatW gap) 20, row * (seatH gap) 20, seatW, seatH); // 根据座位状态选颜色 Color fill seatStatus[row, col] switch { 0 Color.LightGreen, // 可售 1 Color.Gray, // 已售 2 Color.Orange, // 锁定 _ Color.DarkGray // 维修 }; g.FillRectangle(new SolidBrush(fill), rect); g.DrawString(${row 1}-{col 1}, this.Font, Brushes.Black, rect); } } }seatStatus是一个二维数组从数据库加载后填充。点击事件里用e.X和e.Y反算行列注意减去偏移量 20。这种画法在 400 个座位时依然流畅因为 GDI 绘制比创建 400 个控件轻量得多。如果要做 hover 效果在MouseMove里重绘即可。3.2 锁座事务与并发控制锁座的核心是一条带条件的UPDATEUPDATE Seat SET Status 2 WHERE ScheduleId sid AND SeatNo seatNo AND Status 0;执行后检查ROWCOUNT如果返回 1说明锁定成功返回 0说明座位已经被别人抢先锁定或售出。C# 里用SqlCommand.ExecuteNonQuery()的返回值判断即可。这个操作必须在事务里和插入锁定日志一起提交。using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { var cmd new SqlCommand( UPDATE Seat SET Status2 WHERE ScheduleIdsid AND SeatNono AND Status0, conn, tran); cmd.Parameters.AddWithValue(sid, scheduleId); cmd.Parameters.AddWithValue(no, seatNo); int affected cmd.ExecuteNonQuery(); if (affected 1) { // 记录锁定日志写入 LockTime tran.Commit(); return true; } tran.Rollback(); return false; } }这里没有用SELECT再UPDATE的两步走因为那中间有窗口期两个线程可能都查到 Status0。直接UPDATE ... WHERE Status0是原子操作数据库层面保证只有一个能成功。这就是为什么座位表的唯一约束和状态条件缺一不可。3.3 订单写入与支付状态回写锁定成功后用户进入支付页面。支付完成回调里把座位状态从 2 改成 1同时插入订单和订单明细。订单明细的SeatId直接关联座位表主键这样退票时能精确定位到具体座位。// 支付回调 using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { // 更新座位状态 var updateSeat new SqlCommand( UPDATE Seat SET Status1 WHERE SeatIdseatId AND Status2, conn, tran); updateSeat.Parameters.AddWithValue(seatId, seatId); if (updateSeat.ExecuteNonQuery() 0) { tran.Rollback(); throw new Exception(座位状态异常可能已超时释放); } // 插入订单 var insertOrder new SqlCommand( INSERT INTO [Order](UserId,TotalAmount,PayStatus,CreateTime) VALUES(uid,amt,1,GETDATE()); SELECT SCOPE_IDENTITY();, conn, tran); insertOrder.Parameters.AddWithValue(uid, userId); insertOrder.Parameters.AddWithValue(amt, amount); int orderId Convert.ToInt32(insertOrder.ExecuteScalar()); // 插入明细 var insertDetail new SqlCommand( INSERT INTO OrderDetail(OrderId,SeatId,Price) VALUES(oid,sid,price), conn, tran); insertDetail.Parameters.AddWithValue(oid, orderId); insertDetail.Parameters.AddWithValue(sid, seatId); insertDetail.Parameters.AddWithValue(price, price); insertDetail.ExecuteNonQuery(); tran.Commit(); } }SCOPE_IDENTITY()拿当前作用域的自增 ID比IDENTITY安全不会受触发器影响。支付回调必须做幂等同一个订单号重复回调时先查订单是否已存在存在就直接返回成功不要重复插单。4. 避坑排查售票系统上线后最容易翻车的五件事4.1 座位状态和订单状态不一致现象用户支付成功但座位图上显示还是锁定状态或者订单已退款但座位没释放。原因支付回调和超时释放任务并发执行两个事务交叉后提交的覆盖了先提交的。比如超时任务把座位从 2 改成 0支付回调同时把 2 改成 1最终状态取决于谁后提交。解决所有状态变更都带条件。释放时用WHERE Status2支付时也用WHERE Status2谁先执行谁成功后执行的ROWCOUNT为 0 就放弃。再加一张状态变更日志表每次变更记录前后状态和时间戳排查时有据可查。4.2 高并发下锁座超卖现象同一场次同一座位两个用户同时支付成功数据库里出现两条已售记录。原因锁座逻辑用了SELECT查状态再UPDATE中间有间隙。或者 C# 端用了多线程但没加锁两个线程同时读到 Status0。解决锁座必须用UPDATE ... WHERE Status0的原子写法靠数据库行锁保证。如果业务要求先查再锁那就用SELECT ... WITH (UPDLOCK)在查询时就加更新锁。另外座位表的唯一约束是最后防线即使逻辑有漏洞重复插入也会被数据库拒绝。4.3 Winform 界面卡顿与进度条不更新现象加载座位图或批量导入影片时界面假死进度条不动。原因耗时操作跑在 UI 线程上Thread.Sleep或同步数据库查询阻塞了消息循环。解决用BackgroundWorker或async/await把数据库操作放到后台线程通过Invoke更新进度条。热词里提到的“winform 如何更新状态栏与进度条”核心就是跨线程更新控件必须Invoke。不要直接在后台线程里改Label.Text会抛跨线程异常。private async void btnLoad_Click(object sender, EventArgs e) { progressBar.Value 0; var seats await Task.Run(() LoadSeatsFromDb(scheduleId)); // 回到 UI 线程更新 seatStatus seats; seatPanel.Invalidate(); progressBar.Value 100; }Task.Run把查询放到线程池await之后自动回到 UI 上下文直接更新控件不会报错。进度条可以在LoadSeatsFromDb里通过IProgressint回调更新。4.4 数据库连接未释放导致连接池耗尽现象系统运行一段时间后所有数据库操作都超时重启后恢复。原因SqlConnection没有用using包裹或者SqlDataReader没关闭连接池被占满。解决所有SqlConnection、SqlCommand、SqlDataReader一律用using。连接池默认最大 100Winform 客户端如果频繁开连接很容易打满。另外查询用完SqlDataReader要显式Close()或者用using包住。4.5 打包安装后数据库连不上现象开发机上一切正常打包成安装程序部署到影院电脑后提示“无法连接数据库”。原因连接字符串写死了开发机的localhost或.\SQLEXPRESS目标机器没有这个实例。或者 SQL Server 没开 TCP/IP 协议防火墙拦了 1433 端口。解决连接字符串放到App.config里安装时让用户填服务器地址。如果目标机器没装 SQL Server可以用 LocalDB 或 SQLite 做单机版。热词里提到的“winform 打包成安装程序”建议用 Inno Setup 或 Visual Studio 的 Setup Project把数据库脚本作为安装步骤执行。注意 SQLite 不需要安装服务适合单机影院但并发写入性能不如 SQL Server。5. 进阶技巧用事务日志和报表把系统做扎实5.1 状态变更日志表的设计与查询前面反复提到状态一致性真正落地靠的是一张SeatStatusLog表。每次座位状态变更都往这张表插一条记录座位 ID、旧状态、新状态、操作类型、操作时间、操作人。这张表只增不改相当于座位的黑匣子。CREATE TABLE SeatStatusLog ( LogId BIGINT IDENTITY(1,1) PRIMARY KEY, SeatId INT NOT NULL, OldStatus TINYINT NOT NULL, NewStatus TINYINT NOT NULL, ActionType NVARCHAR(20) NOT NULL, -- Lock, Pay, Release, Refund ActionTime DATETIME NOT NULL DEFAULT GETDATE(), Operator NVARCHAR(50) NULL ); CREATE INDEX IX_SeatStatusLog_SeatId ON SeatStatusLog(SeatId, ActionTime);排查“这个座位为什么被卖了两次”时直接按SeatId查日志时间线一目了然。日志表数据量大后可以按月分表或者定期归档到历史库。查询时注意ActionTime索引不然全表扫描很慢。5.2 日票房统计的 SQL 与缓存影院经理最关心的是日票房。一条 SQL 就能出结果SELECT CONVERT(DATE, o.CreateTime) AS SaleDate, COUNT(DISTINCT o.OrderId) AS OrderCount, SUM(o.TotalAmount) AS TotalRevenue, COUNT(od.DetailId) AS TicketCount FROM [Order] o JOIN OrderDetail od ON o.OrderId od.OrderId WHERE o.PayStatus 1 AND o.CreateTime startDate AND o.CreateTime endDate GROUP BY CONVERT(DATE, o.CreateTime) ORDER BY SaleDate DESC;PayStatus1过滤掉未支付和已退款的订单。COUNT(DISTINCT o.OrderId)防止 JOIN 导致订单数重复计算。如果数据量大可以建一个按天的汇总表每天凌晨跑一次定时任务报表直接查汇总表响应更快。5.3 退票与座位释放的幂等处理退票操作必须幂等。用户点一次退票网络超时又点一次不能退两次钱。做法是在订单表加一个RefundStatus字段退票时先UPDATE [Order] SET RefundStatus1 WHERE OrderIdoid AND RefundStatus0检查ROWCOUNT只有返回 1 才继续释放座位和退款。座位释放同样带条件WHERE Status1确保只释放已售座位。var cmd new SqlCommand( UPDATE [Order] SET RefundStatus1 WHERE OrderIdoid AND RefundStatus0, conn, tran); cmd.Parameters.AddWithValue(oid, orderId); if (cmd.ExecuteNonQuery() 0) { // 已经退过了直接返回 return; } // 继续释放座位这套幂等逻辑配合状态日志退票流程基本不会出问题。我自己的习惯是任何涉及钱和座位状态的操作先写日志再改状态日志插入失败就回滚宁可操作失败也不能状态不明。做影院售票系统数据库齐全不只是表多而是每条状态流转都有记录、有约束、有后悔药。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Spring AI模块停更不可怕,Java开发者AI落地实战路线全解析

Spring AI模块停更不可怕,Java开发者AI落地实战路线全解析

前几天好几个技术群都在转同一个话题:Spring AI Alibaba 已经停更了,Java 还有希望吗? 我明白这种焦虑从哪来。人工智能这两年的话语权几乎被 Python 占满,Java 开发者好不容易等到一个能对接大模型的官方框架,结果还…

2026/10/14 7:06:41 阅读更多 →
B站图形化抢票脚本实战:纯接口下单与验证码预演全解析

B站图形化抢票脚本实战:纯接口下单与验证码预演全解析

简介:面向具备一定Python基础的开发者和技术学习者,这套压缩包完整收录了B站会员购漫展抢票自动化项目的源码、配置与示例脚本。项目以纯接口交互为主线,并提供图形化界面封装,同时专门设计了验证码预演练习模块,便于理…

2026/10/14 7:06:44 阅读更多 →
FCMADDPG:多智能体编队控制的深度强化学习奖励塑形实战

FCMADDPG:多智能体编队控制的深度强化学习奖励塑形实战

简介:基于MADDPG算法的多智能体编队控制源码包,面向机器人、无人机及自动驾驶等领域的研究者和开发者,重点解决多智能体协同编队的队形生成、保持与动态避障问题。包内共18个文件,包含13个Python核心模块、4个编队状态文本文件及1…

2026/10/14 4:14:19 阅读更多 →

最新新闻

AI编程教程为何爆火2万Star:从零基础到生产落地的完整路径

AI编程教程为何爆火2万Star:从零基础到生产落地的完整路径

1. 这个“2万Star”到底意味着什么:从GitHub数字看AI编程教程的真实分量很多人看到标题里“2万Star”第一反应是:哇,火了。但作为在开发者教育领域摸爬滚打十多年的老手,我得先泼一盆冷静的水——Star数从来不是教学内容质量的直接…

2026/10/14 7:19:37 阅读更多 →
DNN多分类任务核心设计:Softmax、交叉熵与PyTorch实战

DNN多分类任务核心设计:Softmax、交叉熵与PyTorch实战

2. 多分类任务的三个核心设计问题先说结论:DNN跑多分类,真正需要想清楚的只有三件事——输出层的神经元数量、输出层的激活函数、损失函数的选择。这三件事定下来,模型框架基本就定死了,后面做的都是调参和优化。2.1 输出层神经元…

2026/10/14 7:19:37 阅读更多 →
离线环境部署MySQL 5.7:tar.gz包安装与避坑指南

离线环境部署MySQL 5.7:tar.gz包安装与避坑指南

简介:本资源为 MySQL 5.7.40 在 Linux-glibc2.12 环境下的 x86_64 离线安装包,面向需要在无外网或内网环境中部署关系型数据库的运维与后端开发人员。压缩包共 379 个文件,约 646.59MB,以 h 头文件、so 动态库、xml 配置、sql 脚本…

2026/10/14 7:19:37 阅读更多 →
华为HCIP-AI测试卷高效复习指南:从背答案到动手验证

华为HCIP-AI测试卷高效复习指南:从背答案到动手验证

简介:这份华为认证HCIP-AI测试卷与答案文档,面向备考华为人工智能高级认证的考生及希望系统检验AI基础知识的开发者,帮助梳理图像处理、语音合成、HiAI生态与快应用开发等核心考点。资源包共1个docx文件,约14KB,内容以…

2026/10/14 7:19:37 阅读更多 →
基于rSVD与软阈值的Matlab高效信号去噪方法

基于rSVD与软阈值的Matlab高效信号去噪方法

1. 项目背景与核心挑战谐波噪声是信号处理领域常见的干扰类型,广泛存在于电力系统监测、机械振动分析、生物医学信号采集等场景。传统去噪方法如傅里叶滤波、小波变换在面对高维大数据集时往往面临计算效率低下、噪声鲁棒性不足的问题。这个Matlab项目提出了一种融合…

2026/10/14 7:19:37 阅读更多 →
智慧农业最后一百米:破解乡村数字化落地的设备与运维难题

智慧农业最后一百米:破解乡村数字化落地的设备与运维难题

做了快八年农业数字化项目,我最深的一个体会是:智慧农业真正难的从来不是算法有多高级、传感器有多精密,而是怎么让技术在没有网线、没有专职IT人员、甚至不少农户只用老年机的村子里,持续稳定地转起来。前段时间复盘了一个扎根基…

2026/10/14 7:18:36 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/12 18:11:17 阅读更多 →