news 2026/10/11 3:13:54

C# WinForms医院挂号管理系统:三层架构与并发事务实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# WinForms医院挂号管理系统:三层架构与并发事务实战

简介:这是一份基于C#语言与WinForms技术实现的医院挂号管理系统,采用C/S架构和MVC分层设计,适合正在学习桌面应用开发或需要完成课程设计、毕业设计的读者参考。系统覆盖用户管理、科室管理、医生管理、门急诊挂号、挂号查询、修改口令、打印挂号单及帮助文档等模块,借助存储过程封装数据库操作,并使用MD5加密保护用户密码,同时以报表形式直观展示统计信息,还支持医生照片上传与详情查看。压缩包内含185个文件,10.03MB,其中包含64个C#源码文件、19个资源及resx配置、多个SQL脚本与报表文件,另有CHM帮助文档和可执行的exe程序,方便直接运行与二次开发。已有1263人学习下载,项目结构清晰,适合初学者对照源码理解业务分层和数据库访问逻辑,也能为相关系统的快速搭建提供参考。

1. C# WinForms 医院挂号管理系统:窗口场景里最该被复现的那套流程

在一堆管理系统选题里,医院挂号管理系统是 C# WinForms 课程设计和内部系统里出现频率最高的一类,但它真正难的不是画几个窗体,而是“窗口 A 刚挂完号,窗口 B 显示的还是旧余号”这种一致性问题。这套资源按三层架构把科室、医生、排班、挂号、退号串成了一条完整流程,覆盖按日期查余号、生成挂号单、退号回补、当日统计和简单权限控制。适合正在做相关课程设计的人,也适合想快速搭一套内部挂号后台的开发者。下文所有 SQL 和 C# 代码都按可直接复现的方式给到,照着顺序跑就能搭出一个能用的版本。

2. 先把地基打对:三层架构与五张核心表,表结构这样设计才不返工

2.1 三层架构的选型理由与项目目录

很多人在做这类系统时,习惯把所有 SQL 直接写在 Form 的按钮事件里。窗体少的时候没问题,一旦开始加退号、加统计、加权限,代码就全缠在一起,改一个查询要翻三个窗体。我一般会按三层拆:UI 层只放 WinForms 窗体,BLL 层负责挂号、退号、统计这类业务规则,DAL 层只做数据访问。这样窗口换样式、数据库换位置都不会动业务逻辑。

模拟项目X里的目录结构是按这样分的:

HospitalRegSystem/ ├── HospitalReg.UI/ # WinForms 窗体 │ ├── FrmLogin.cs │ ├── FrmMain.cs │ ├── FrmRegister.cs │ └── FrmStatistics.cs ├── HospitalReg.BLL/ # 业务逻辑 │ ├── RegistrationService.cs │ ├── DoctorService.cs │ └── UserService.cs ├── HospitalReg.DAL/ # 数据访问 │ ├── BaseDAL.cs │ └── SqlHelper.cs ├── HospitalReg.Models/ # 实体类 │ ├── Doctor.cs │ ├── Schedule.cs │ └── Registration.cs └── HospitalReg.Common/ # 通用工具 ├── Session.cs # 当前登录用户 └── SecurityHelper.cs # 密码哈希

这段结构里需要注意的点:Models 里不要写数据库字段相关的字符串常量,否则 BLL 和 DAL 都会依赖它;Common 里的 Session 是静态类,整个进程内全局可读,适合单机窗口模式,但如果系统以后要改成 Web 或分布式,就要换成注入方式。

2.2 五张表的设计与关键字段

挂号系统的核心数据其实就五张表:科室、医生、排班、挂号记录、系统用户。很多初学者会漏掉排班表,直接把号源数量写在医生表里,结果是所有日期共用一份余号,完全没法做“按日期挂号”。

核心表设计如下:

表名关键字段作用
DepartmentId, DeptName, Sort科室列表,挂号第一步先选科室
DoctorId, DeptId, DoctorName, Title, IsEnabled医生,关联科室
DoctorScheduleId, DoctorId, WorkDate, TotalSlots, UsedSlots某医生某天的号源总量与已用量
RegistrationId, ScheduleId, PatientName, PatientPhone, Status, CreateTime挂号记录,Status 存 0/1/2
SysUserId, UserName, PwdHash, Role登录账号,Role 区分管理员与窗口操作员

排班表是整个系统的节拍器,挂号时只改这张表里的 UsedSlots。Registration 里不直接存号源数字,而是通过 ScheduleId 关联。建表 SQL 可以直接用,主键统一用自增 int,日期字段用 datetime2 而不是 varchar,这样统计时不会被字符串格式坑到:

CREATE TABLE Department ( Id INT IDENTITY(1,1) PRIMARY KEY, DeptName NVARCHAR(50) NOT NULL, Sort INT NOT NULL DEFAULT 0 ); CREATE TABLE Doctor ( Id INT IDENTITY(1,1) PRIMARY KEY, DeptId INT NOT NULL REFERENCES Department(Id), DoctorName NVARCHAR(20) NOT NULL, Title NVARCHAR(20) DEFAULT '主治医师', IsEnabled BIT NOT NULL DEFAULT 1 ); CREATE TABLE DoctorSchedule ( Id INT IDENTITY(1,1) PRIMARY KEY, DoctorId INT NOT NULL REFERENCES Doctor(Id), WorkDate DATE NOT NULL, TotalSlots INT NOT NULL DEFAULT 30, UsedSlots INT NOT NULL DEFAULT 0, UNIQUE (DoctorId, WorkDate) ); CREATE TABLE Registration ( Id INT IDENTITY(1,1) PRIMARY KEY, ScheduleId INT NOT NULL REFERENCES DoctorSchedule(Id), PatientName NVARCHAR(20) NOT NULL, PatientPhone NVARCHAR(20) NULL, Status INT NOT NULL DEFAULT 0, OperatorId INT NULL, CreateTime DATETIME2 NOT NULL DEFAULT SYSDATETIME() );

这里有个容易被忽略的设计:Registration 表里没有冗余存医生和科室名。统计时需要连表查,但换来的是改医生科室不用回刷历史数据。如果担心查询性能,可以在 DoctorSchedule 上建 WorkDate 的索引,而不是把科室名直接塞进挂号记录。

2.3 连接串与 DAL 基础封装

连接串我建议直接用 SQL Server 身份验证而不是 Windows 身份验证,因为部署到别的机器时 Windows 账号不一定存在。下面这条连接串适合开发机直连默认实例:

string connStr = @"Server=.;Database=HospitalRegDB;User Id=sa;Password=你的密码;TrustServerCertificate=True;Connect Timeout=15;";

参数说明:TrustServerCertificate=True是给本机开发用的,连远程库且没有正规证书时,不加这个会直接报 SSL 错误;Connect Timeout=15是连接超时秒数,窗口环境下写上比默认 15 秒更短的值,比如 10,省得高峰期卡在连线阶段。

DAL 层不用上 EF 这类重量级框架,用原生 SqlConnection 加几个静态方法就够了。BaseDAL 里最常用的两个方法这样封装:

public static int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (var conn = new SqlConnection(connStr)) using (var cmd = new SqlCommand(sql, conn)) { if (ps != null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } public static DataTable ExecuteTable(string sql, params SqlParameter[] ps) { using (var conn = new SqlConnection(connStr)) using (var da = new SqlDataAdapter(sql, conn)) { if (ps != null) da.SelectCommand.Parameters.AddRange(ps); var dt = new DataTable(); da.Fill(dt); return dt; } }

using会把连接和命令一起释放,省去手写 finally 的麻烦。所有外部输入(患者姓名、手机号、查询条件)必须走 SqlParameter,不能拼字符串。参数化不仅能防注入,还能让 SQL Server 缓存执行计划,频繁挂号的场景下性能差别很明显。

3. 挂号主流程落地:科室联动、号源扣减与退号回滚的 WinForms 实现

3.1 界面结构与科室-医生联动

主窗体布局就三块:左侧一个 ListBox 放科室,右侧一个 DataGridView 放医生,下面一个 DataGridView 放选中医生的出诊排班。逻辑很直接——选科室→过滤医生→选医生→加载该医生未来 7 天的排班。这三步是三个独立的加载方法,不要写在一个事件里互相嵌套。

加载科室和医生的核心代码:

private void LoadDepartments() { DataTable dt = BaseDAL.ExecuteTable( "SELECT Id, DeptName FROM Department ORDER BY Sort"); listBoxDept.DataSource = dt; listBoxDept.DisplayMember = "DeptName"; listBoxDept.ValueMember = "Id"; } private void LoadDoctors(int deptId) { DataTable dt = BaseDAL.ExecuteTable( "SELECT Id, DoctorName, Title FROM Doctor WHERE DeptId = @deptId AND IsEnabled = 1", new SqlParameter("@deptId", deptId)); dataGridViewDoctor.DataSource = dt; }

这里要避一个新手坑:DataSource直接绑定 DataTable 后,再次查询时不能直接赋新的 DataTable,否则界面会花。稳妥做法是给 DataGridView 的DataSource先设null再去赋值。另外ValueMember绑定的是内存表里的列名,必须和 SQL 里的别名一致,用 Select 出来的Id而不是数据库原始列名,否则 SelectedValue 一直返回 null。

3.2 号源展示与挂号落库:一条 UPDATE 解决超卖

选中医生后,加载排班数据的 SQL 长这样:

SELECT s.Id, s.WorkDate, s.TotalSlots - s.UsedSlots AS RemainSlots, s.UsedSlots, s.TotalSlots FROM DoctorSchedule s WHERE s.DoctorId = @doctorId AND s.WorkDate >= CAST(GETDATE() AS DATE) ORDER BY s.WorkDate;

挂号窗体的设计比较关键:弹出的 FrmRegister 窗体里,只显示号源信息(医生、日期、余号),真正执行挂号时调用 BLL 层的一个方法。这个方法必须放在事务里,先锁住排班行做条件扣减,再插入挂号记录:

public static string Register(int scheduleId, string patientName, string patientPhone, int operatorId) { using (var conn = new SqlConnection(connStr)) { conn.Open(); var tx = conn.BeginTransaction(); try { // 1. 条件扣减号源:只有 UsedSlots < TotalSlots 才会执行成功 string updateSql = @" UPDATE DoctorSchedule SET UsedSlots = UsedSlots + 1 WHERE Id = @scheduleId AND UsedSlots < TotalSlots"; using (var cmd = new SqlCommand(updateSql, conn, tx)) { cmd.Parameters.AddWithValue("@scheduleId", scheduleId); int affected = cmd.ExecuteNonQuery(); if (affected == 0) { tx.Rollback(); return "该时段号源已满,请选择其他时段"; } } // 2. 插入挂号记录 string insertSql = @" INSERT INTO Registration(ScheduleId, PatientName, PatientPhone, OperatorId) VALUES(@scheduleId, @name, @phone, @operatorId)"; using (var cmd = new SqlCommand(insertSql, conn, tx)) { cmd.Parameters.AddWithValue("@scheduleId", scheduleId); cmd.Parameters.AddWithValue("@name", patientName); cmd.Parameters.AddWithValue("@phone", patientPhone); cmd.Parameters.AddWithValue("@operatorId", operatorId); cmd.ExecuteNonQuery(); } tx.Commit(); return "OK"; } catch (SqlException ex) { tx.Rollback(); return "挂号失败:" + ex.Message; } } }

这套写法和“先 SELECT 余号再 INSERT”最大的区别在于:扣号这个动作是原子的,两个窗口同时点挂号时,只有一个 UPDATE 能在UsedSlots < TotalSlots条件下成功,另一个受影响行数为 0 就直接回滚。很多人在这类系统里把超卖问题归咎于 SQL Server 并发配置,其实根子在业务代码没用条件更新。事务隔离级别这里用默认的即可,不需要调成 Serializable,因为单条 UPDATE 本身就带行级锁。

3.3 退号回补与改号:事务顺序决定了数据对不对

退号的 SQL 是挂号的逆操作,但有一个容易写反的顺序问题。正确做法是先把挂号记录状态改成已退号,再回补排班余号。如果你把回补放在前面,万一更新 Registration 时失败,余号已经加了回去,患者实际还挂着一个有效号,数据就对不上了:

public static string CancelRegistration(int regId, int scheduleId) { using (var conn = new SqlConnection(connStr)) { conn.Open(); var tx = conn.BeginTransaction(); try { // 1. 先把挂号记录置为已退号 string sql1 = @"UPDATE Registration SET Status = 2 WHERE Id = @regId AND Status = 0"; // 2. 再回补余号,判断 UsedSlots > 0 防止负数 string sql2 = @"UPDATE DoctorSchedule SET UsedSlots = UsedSlots - 1 WHERE Id = @scheduleId AND UsedSlots > 0"; // 执行两个命令后 Commit tx.Commit(); return "OK"; } catch (SqlException ex) { tx.Rollback(); return "退号失败:" + ex.Message; } } }

Status = 0这个条件很重要。如果挂号记录已经被医生看过(Status = 1),就不允许退;如果你不管状态直接改,患者凭一张旧单子可以反复退号。我在实际项目里都会在退号按钮的入口再加一道判断:只有状态为 0 的记录才弹确认框,从 UI 层就拦住一多半误操作。

4. 容易翻车的五个地方:并发超卖、刷新异常与日期格式排查记录

中间这个章节不聊理论,直接给排错记录。以下五个坑都是这套系统里实际踩过的,按“现象—原因—解决”写清楚。

4.1 并发与数据刷新类:余号超卖和 DataGridView 未提交异常

坑一:余号显示“还有 1 个”,两个窗口同时挂号却都成功了。

现象:高峰期窗口 A 和窗口 B 同时给同一个医生挂号,页面都显示余号 1,点完挂号后两个患者都拿到了号,排班表的 UsedSlots 只加了 1(因为第二次 UPDATE 条件不成立回滚了,但两个患者都收到了成功提示)。

原因:问题出在“先查余号再插入”的经典写法上。先 SELECT 余号,再执行 INSERT,这个间隙里另一个请求也读到了同样的余号,双方都认为有号可挂,两个 INSERT 都成功了。

解决:使用上一章的事务条件扣减方案,UPDATE ... WHERE UsedSlots < TotalSlots,以受影响行数判断是否成功。排查时先看 Registration 表里同一 ScheduleId 是否有多条记录,再看 OperatorId 是否是同一个窗口,区分是逻辑问题还是操作员重复点击。

坑二:DataGridView 绑定新数据时抛 ArgumentOutOfRangeException。

现象:挂完号后调用 LoadSchedule() 刷新排班表,不定时抛“索引超出范围”异常,偶尔界面卡住。

原因:原行处于编辑状态,直接改 DataSource 会导致绑定的位置索引和内部数据结构错位。有些机器上表现为偶发,特别容易在快速点击后触发。

解决:刷新前先提交正在编辑的单元格,再把 DataSource 置空:

private void RefreshGrid(DataGridView grid, DataTable dt) { grid.EndEdit(); // 提交未完成的编辑 grid.DataSource = null; // 断开绑定 grid.DataSource = dt; // 重新绑定 }

这条经验对所有 WinForms 列表页通用。凡是“点按钮刷新表格”的操作,都强制走这三行,别嫌麻烦。

4.2 日期、传值与连接类:查询少数据、用户切换和 sa 登录失败

坑三:按日期筛选挂号记录,某几天的数据查不到。

现象:统计 7 月 5 日的挂号量,结果比实际少十几条;换成“近 7 天”查询又正常。

原因:Registration.CreateTime 字段被存储成了 varchar,写入时用了yyyy-MM-dd HH:mm:ss,查询时用了yyyy-MM-dd这种短格式,SQL Server 自动转换时把时分秒按 00:00:00 处理,边界数据全被过滤掉。这类问题在开发时不容易暴露,因为本机数据量小,日期格式恰好一致。

解决:把 CreateTime 字段改成 datetime2,查询一律用 DateTime 参数传入。如果历史库已经存了字符串,先UPDATE Registration SET CreateTime = CONVERT(datetime2, CreateTime)刷一遍格式,再把列类型改掉。从那以后我建表凡是时间字段,一律 datetime2,不再用 varchar 存时间。

坑四:退出登录后重新登录,主窗体显示的还是上一个账号的菜单和数据。

现象:管理员退出,换窗口操作员登录,菜单栏的“统计报表”还在,双击还能打开。

原因:登录成功只是隐藏了登录窗体,FrmMain 实例没有重建,菜单权限只初始化一次。用户变了,界面状态没变。

解决:退出登录时直接关闭主窗体,重新new FrmMain()并传入新用户。不要在同一个主窗体实例上做状态重置,那只会越改越乱。Session 静态类的当前用户属性在登录时统一赋值:

public static class Session { public static int UserId { get; set; } public static string UserName { get; set; } public static string Role { get; set; } }

坑五:开发机连不上本地数据库,报“用户 sa 登录失败”。

现象:刚装的 SQL Server,代码里连接串写的是User Id=sa,一运行就报错;但 SSMS 里用 Windows 身份验证能登上。

原因:SQL Server 默认不开 sa 账号,安装时选的是 Windows 身份验证模式。

解决:用 Windows 身份验证登录 SSMS,执行ALTER LOGIN sa WITH PASSWORD = '新密码',再把服务器属性里的身份验证模式改为“混合模式”,重启 SQL 服务。如果还是失败,检查 SQL Server 配置管理器里的 TCP/IP 协议是否已启用。

5. 权限与日结统计:登录角色控制菜单、挂号量与退号率报表

5.1 登录验证与角色菜单显隐

登录窗体用参数化查询比对账号密码。密码在库里不能存明文,常见做法是 SHA256 加盐后存哈希:

public static string HashPassword(string password, string salt) { using (var sha = System.Security.Cryptography.SHA256.Create()) { var bytes = System.Text.Encoding.UTF8.GetBytes(salt + password); byte[] hash = sha.ComputeHash(bytes); return Convert.ToHexString(hash); } }

登录 SQL 这样写:

string sql = @"SELECT Id, UserName, Role FROM SysUser WHERE UserName = @name AND PwdHash = @hash"; DataTable dt = BaseDAL.ExecuteTable(sql, new SqlParameter("@name", userName), new SqlParameter("@hash", HashPassword(password, "固定盐值")));

查不到用户就提示账号或密码错误,不要区分“用户不存在”和“密码错误”,避免被探测。登录成功后把用户信息塞进 Session。

菜单权限按下发策略:管理员显示全部菜单,窗口操作员只显示“挂号管理”和“当日查询”,医生角色只能查看自己的排班。实现时用一个方法遍历菜单项,控制 Visible:

private void ApplyMenuPermission() { menuStatistics.Visible = Session.Role == "Admin"; menuDoctorSchedule.Visible = Session.Role == "Doctor"; menuLogout.Visible = true; }

菜单权限是“显示层面”的控制,如果系统会暴露接口给其他端,必须在 BLL 层再校验一次角色。WinForms 单机场景压力不大,但养成习惯没坏处。

5.2 当日挂号量统计与退号率

日结统计要回答三个问题:今天挂了多少号、每个科室多少、退号率多高。一条 SQL 就能全出:

SELECT d.DeptName, COUNT(r.Id) AS TotalCount, SUM(CASE WHEN r.Status = 2 THEN 1 ELSE 0 END) AS CancelCount, CAST(SUM(CASE WHEN r.Status = 2 THEN 1.0 ELSE 0 END) / COUNT(r.Id) * 100 AS DECIMAL(5,2)) AS CancelRate FROM Registration r JOIN DoctorSchedule s ON r.ScheduleId = s.Id JOIN Doctor doc ON s.DoctorId = doc.Id JOIN Department d ON doc.DeptId = d.Id WHERE r.CreateTime >= @start AND r.CreateTime < @end GROUP BY d.DeptName ORDER BY TotalCount DESC;

几个参数细节:@start传当天零点,@end传第二天零点,用>=加<包住一天,避免漏掉 23:59:59 的尾单。CASE WHEN Status = 2 THEN 1.0写成 1.0 是为了让除法的结果变成小数,DB 默认整数相除会直接截尾成整数百分比。COUNT(r.Id) 不会统计 NULL,如果要用 LEFT JOIN 补零科室,记得把 COUNT(r.Id) 换成 COUNT(r.Id) 并配合 ISNULL。

统计结果直接绑到 DataGridView 会连带显示小数位,最好再设置列格式,grid.Columns["CancelRate"].DefaultCellStyle.Format = "0.00"。窗口操作员看的日报,格式做到位比样式好看更重要,因为数据要复核。

5.3 报表导出 CSV:不引入第三方库

很多环境不能装 Excel COM 组件,导出 CSV 是代价最小的方案。生成 CSV 时有一个坑:Excel 打开 UTF-8 编码的 CSV 会乱码,必须在文件头写 BOM:

public static void ExportDataTableToCsv(DataTable dt, string filePath) { using (var sw = new System.IO.StreamWriter(filePath, false, new System.Text.UTF8Encoding(true))) { // 写表头 sw.WriteLine(string.Join(",", dt.Columns.Cast<DataColumn>().Select(c => c.ColumnName))); // 写行数据,字段含逗号时加双引号 foreach (DataRow row in dt.Rows) { var fields = row.ItemArray.Select(f => f.ToString().Contains(",") ? "\"" + f.ToString() + "\"" : f.ToString()); sw.WriteLine(string.Join(",", fields)); } } }

new UTF8Encoding(true)的true就是写入 BOM。字段里如果本身包含双引号,还需要替换成双引号再加引号包裹,这里为了篇幅只处理了逗号场景。导出后建议用“记事本打开看编码”和“Excel 打开看列数”两步验证,防止数字长串被 Excel 当科学计数法。

6. 交付前最后一件事:并发压测脚本与 DataGridView 卡顿优化

这套系统交付前,我最后做的不是功能测试,而是一个压测脚本,专门验证余号一致性。做法是从 UI 里抽出一个纯逻辑入口,模拟 10 个窗口同时抢最后 10 个号:

int successCount = 0; var tasks = Enumerable.Range(1, 12).Select(i => Task.Run(() => { var result = RegistrationService.Register( scheduleId: 5, patientName: "压测" + i, patientPhone: "", operatorId: 1); if (result == "OK") Interlocked.Increment(ref successCount); })); Task.WaitAll(tasks.ToArray()); Console.WriteLine($"实际成功 {successCount},号源 10,应成功 10");

如果脚本跑出 12 个全成功,说明事务条件扣减没生效,回去查 UPDATE 的 WHERE 条件;如果成功数小于 10,查是不是有死锁回滚。这个脚本的重点是“用并发去验证逻辑,而不是用眼睛看界面”。WinForms 的按钮事件在 UI 线程上跑,手动点几百次也触发不了真正的竞争条件,必须用 Task 把请求打进去。

第二个优化是 DataGridView 卡顿。排班表数据量不大,但挂号历史记录几十万条时,直接绑定 DataTable 会明显拖拽。先开双缓冲,一行代码解决大部分刷新闪烁:

typeof(DataGridView).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) .SetValue(dataGridViewHistory, true);

数据量继续变大,就去分页:SQL 里用ORDER BY Id OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY,不要一次性 Load 全部。尤其是“当前窗口挂号记录”这类页面,Date 倒序取前 100 条足够用,用户真的要查历史再按条件搜索。

还有一个值得提的显示类问题:WinForms 在 150% 缩放的屏幕上会糊,正式部署前在 app.manifest 里加<dpiAware>true</dpiAware>,并在 Main 方法开头调一次SetProcessDPIAware()。这个不作为主功能,但窗口操作员的电脑大多是办公本加外接屏,缩放比例各不一样,装上能少接很多“字看不清”的投诉。

整套系统我在模拟项目X上按“建表→封装 DAL→写挂号事务→做权限→压测”这个顺序走了一遍,真正耗时间的部分不是窗体拖拽,而是事务边界和数据格式。从那以后我每次交付这类管理系统,都强制先写并发压测脚本再交代码,至少能挡住一半以上运行期问题。希望帮到你。

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

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

FM020模块实战:DCS中PROFIBUS-DP转Modbus RTU协议转换配置指南

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

作者头像 李华
网站建设 2026/10/11 3:08:23

iText7高清PNG转PDF:DPI设置、无损编码与Alpha通道保留指南

简介&#xff1a;本资源是一份面向Java开发者的iText图片转PDF实战教程&#xff0c;聚焦解决将PNG等图像高质量生成PDF的常见需求&#xff0c;适用于需要文档导出、报告生成或打印适配的后端开发与工具类项目。压缩包共11个文件&#xff0c;含2个核心jar包&#xff08;含iText.…

作者头像 李华
网站建设 2026/10/11 3:08:01

MySQL日期格式化实战:DATE_FORMAT、STR_TO_DATE与时间戳互转全指南

做 MySQL 开发的人&#xff0c;早晚都要跟日期格式化打交道。今天查订单要按天分组&#xff0c;明天统计报表要按月汇总&#xff0c;后天同步数据又要把字符串翻回时间类型。这些场景绕来绕去&#xff0c;核心就是对DATE_FORMAT、STR_TO_DATE、UNIX_TIMESTAMP这几个函数要玩得转…

作者头像 李华
网站建设 2026/10/11 3:06:50

SpringBoot+Netty搭建WebSocket推送方案:从入门到避坑实践

简介&#xff1a;这份PDF格式的示例代码资源&#xff0c;完整演示了在SpringBoot项目中利用Netty作为后台服务端、前端通过WebSocket建立长连接的消息推送实现方案。资源面向具备一定Java基础、希望快速上手实时通信开发的读者&#xff0c;重点解决了服务端向全体用户广播以及按…

作者头像 李华
网站建设 2026/10/11 3:02:47

【数据集】地级市市场分割指数和市场一体化指数数据(2001-2024年)

数据简介&#xff1a;基于“八大类商品”计算市场分割与一体化指数&#xff0c;最经典且被广泛采用的方法是相对价格法。其核心逻辑是&#xff1a;如果市场是完全一体化的&#xff0c;同种商品在不同地区的价格应趋于一致。因此&#xff0c;地区间同类商品的价格差异&#xff0…

作者头像 李华