ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C# WinForms医院挂号管理系统实战:数据库设计、号源锁定与避坑指南

C# WinForms医院挂号管理系统实战:数据库设计、号源锁定与避坑指南 简介一套基于C#与WinForm开发的医院挂号管理系统采用C/S架构与MVC分层模式涵盖用户、科室、医生管理及门急诊挂号、查询、打印等核心模块适合学习桌面应用开发的初学者或需要快速搭建同类管理系统的开发者参考。压缩包共185个文件、约10.03MB以cs源码、sql数据库脚本、exe可运行程序及chm帮助文档为主同时包含界面图片与资源文件结构完整便于直接打开项目研读。这套系统已有1262人学习/下载数据访问层使用存储过程操作数据库用户密码采用MD5加密并附带图文统计报表与医生照片上传功能。项目源码中包含注册窗体、主界面、管理窗体等设计文件配合可执行程序与帮助文档可以快速对照运行效果、追踪业务调用流程。整体上是一份适合WinForm初学者系统学习的完整范例兼顾前台挂号与后台管理实用性较强。1. 医院挂号管理系统C# WinForms 是需求最实在的桌面端选择用 C# 语言配合 WinForms 做医院挂号管理系统是很多开发者的第一套业务系统。它要处理的是窗口挂号高峰期的真实请求患者排队、医生排班、号源不能多卖、退号必须留痕。这类系统用 Web 重了用控制台不现实WinForms 恰好能把 Form、DataGridView、ComboBox 组合成一个可交付给门诊护士使用的桌面软件。下面按我做过的一版挂号系统拆解数据库怎么建、界面怎么组织、号源怎么锁、上线前要排掉哪些坑。适合正在做课程设计、医院内部小工具或想转行业应用的 C# 开发者。2. 分层落地与数据库设计先想想这个挂号系统会被谁改三遍挂号系统的需求不是“挂一个号”那么简单。过俩月会加一个“退号原因”再过半年会加“线上预约”如果一开始就把 SQL 写在窗体按钮里后面每一次改动都是在火堆上浇油。所以第一步不是拖控件而是把解决方案拆成责任明确的三层。2.1 用“三圈”解耦WinForms 窗体、Service 服务、DAL 数据访问医院需求变化很典型今天只要门诊挂号明天要按科室统计工作量。我见过把注册 SQL 写在按钮 Click 里的项目最后换数据库时几乎重写。正确做法是 Form 只负责收集输入和展示Service 负责业务规则DAL 只负责访问数据库。项目结构如下HospitalRegister/ ├─ HospitalRegister.UI // WinForms 窗体 │ ├─ Forms/ │ │ ├─ MainForm.cs │ │ ├─ RegisterForm.cs │ │ └─ DailyReportForm.cs ├─ HospitalRegister.Service // 业务层 │ ├─ RegisterService.cs │ ├─ ScheduleService.cs │ └─ PatientService.cs └─ HospitalRegister.DAL // 数据访问层 ├─ DbHelper.cs ├─ PatientDal.cs ├─ ScheduleDal.cs └─ RegisterDal.cs这是同一个解决方案里的三个项目UI 引用 ServiceService 引用 DALUI 不直接引用 DAL。原因很实际号源扣减、退号逻辑要在多处复用将来换成 WinForms 的另一个壳或者加一个预约入口业务层完全不用动。逻辑说明UI 层直接调 DAL 会让业务规则散落在按钮事件里等你要加“同一身份证当天只能挂两个号”这种规则时会非常痛苦。Service 层作为中介把所有规则收拢到方法里比如TryRegister、CancelRegister每个窗体只管调用。2.2 数据表设计号源别做成“抢完就删”的记录挂号系统表设计的核心是排班表和挂号订单表。门诊要的不是“能存”而是“还能对账”。典型的最小集合如下CREATE TABLE t_department ( dept_id INT PRIMARY KEY IDENTITY(1,1), dept_name NVARCHAR(50) NOT NULL, sort_order INT NOT NULL DEFAULT 0 ); CREATE TABLE t_doctor ( doc_id INT PRIMARY KEY IDENTITY(1,1), dept_id INT NOT NULL REFERENCES t_department(dept_id), doc_name NVARCHAR(50) NOT NULL, title NVARCHAR(20) NULL, -- 主任/副主任/主治 is_active BIT NOT NULL DEFAULT 1 ); CREATE TABLE t_schedule ( schedule_id INT PRIMARY KEY IDENTITY(1,1), doc_id INT NOT NULL REFERENCES t_doctor(doc_id), work_date DATE NOT NULL, am_pm TINYINT NOT NULL, -- 1上午 2下午 total_count INT NOT NULL, -- 放号数 used_count INT NOT NULL DEFAULT 0, -- 已挂号数 version_no INT NOT NULL DEFAULT 0, -- 乐观锁版本 CONSTRAINT uq_schedule UNIQUE (doc_id, work_date, am_pm) ); CREATE TABLE t_registration ( reg_id BIGINT PRIMARY KEY IDENTITY(1,1), schedule_id INT NOT NULL REFERENCES t_schedule(schedule_id), patient_name NVARCHAR(50) NOT NULL, patient_id_no VARCHAR(18) NOT NULL, phone VARCHAR(11) NULL, reg_status TINYINT NOT NULL DEFAULT 1, -- 1已挂号 2已退号 3已过号 4已就诊 create_time DATETIME2 NOT NULL DEFAULT SYSDATETIME(), cancel_time DATETIME2 NULL ); CREATE NONCLUSTERED INDEX idx_reg_schedule_status ON t_registration (schedule_id, reg_status);表设计逻辑t_schedule的used_count用于判断是否还有号不能靠“统计 t_registration 里状态为 1 的记录数”回填那样并发下容易算错。t_registration记录每一笔挂号作为财务对账的依据。不要为了省事把多个主任医师的号合并成一张表否则排班查询全是OR条件三个月后数据量上来就会变慢。参数说明used_count是int挂号成功后执行UPDATE ... SET used_count used_count 1 WHERE used_count total_count这一步天然防超卖。reg_status是状态机字段退号时不能物理删除要改成 2便于后续计算退号率和复诊率。非聚集索引加在schedule_id reg_status上正好覆盖“查某一天某医生还有几个有效号”的查询。2.3 连接字符串与参数化 SQL第一行代码就从 SqlConnection 开始连接数据库的写法里最重要的是“用完就释放”。我一般会写一个小封装但不封事务事务留在 Service 里显式创建否则事务扩散到 UI 层会变得很难看。public static class DbHelper { public static string ConnStr { get; set; } public static DataTable Query(string sql, params SqlParameter[] parameters) { using var conn new SqlConnection(ConnStr); using var cmd new SqlCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); var dt new DataTable(); using (var da new SqlDataAdapter(cmd)) { da.Fill(dt); } return dt; } public static int Execute(string sql, params SqlParameter[] parameters) { using var conn new SqlConnection(ConnStr); using var cmd new SqlCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } }参数说明params SqlParameter[]让调用处可以直接写new SqlParameter(docId, docId)。using var是 C# 8 的语法如果用 .NET Framework改成using (var conn ...)也一样。这个封装只做两件事查询返回DataTable执行返回影响行数。事务不要放在这里因为挂号流程里要先扣号源再插入记录多个命令必须共享同一个连接和事务。调用示例var sql SELECT total_count, used_count FROM t_schedule WHERE schedule_id sid; var dt DbHelper.Query(sql, new SqlParameter(sid, scheduleId));逻辑说明参数化 SQL 有两个实际作用防止把或日期格式直接拼进 SQL给数据库计划缓存提供稳定文本避免每次拼接新 SQL 导致内存和 CPU 大量消耗。在挂号系统里患者姓名和证件号都可能带中文一旦拼接出错窗口护士就会看到一句“语法错误”是很典型的翻车现场。提示DbHelper.ConnStr不要写在代码里。放在App.config的ConnectionStrings节部署时让医院信息科改链接地址否则换一台服务器就得重新编译。2.4 选型ADO.NET 还是 EF Core挂号系统我会优先用 ADO.NET很多新手纠结要不要用 EF Core。真实门诊机房里经常是内网机器.NET 版本停留在 4.8不允许随便装新运行时。ADO.NET 是 .NET Framework 自带的几乎没有额外依赖。EF Core 虽然写起来方便但遇到排班这类复杂查询可能出现延迟加载导致的 N1 问题反而不如一条 SQL 直观。如果你是全新机房、可以装 .NET 6/8那用 EF Core 也能做。但我自己的经验是医院信息科更愿意维护“看得懂的 SQL”而不是 ORM 生成的动态语句。ADO.NET 写出来的 SQL 可以原样交给 DBA 审排错成本低很多。这个选择不是技术高低问题是维护成本问题。3. 挂号主窗体的关键控件DataGridView、ComboBox 与实时号源刷新这一章是 WinForms 的“门面”。挂号窗口的护士不关心架构她关心科室下拉能不能和医生排班联动、点一个医生能不能看到剩余号、输完姓名证件号点挂号能不能马上成功。所以窗体布局要围绕这几件高频操作设计。3.1 布局左边科室中间医生排班表格右边挂号操作区一个可用的挂号主窗体可以这样组织顶部 Panel患者姓名、证件号、手机号中间 SplitContainer左侧 TreeView 或 ComboBox 列科室右侧 DataGridView 显示该科室某一天的医生排班底部 Panel剩余号源显示以及“挂号”“退号”按钮实现要点主窗体 Load 事件里加载科室列表用异步方式加载不要在 Load 里同步查询大表。窗体宽度按 1280x720 设计右侧操作区固定 320 像素宽这样护士在小笔记本上也不会挤。Form 属性建议StartPosition CenterScreenAutoScaleMode FontMinimizeBox可以保留但把MaximizeBox关掉防止窗口被用户拉到过大导致右侧面板错位。别小看这几个属性很多 WinForms 系统上线后被吐槽“窗口乱跑”大多是这里没设置。3.2 科室与医生排班的联动ComboBox 的 SelectedIndexChanged 不要重复刷新新手最容易踩的坑是在SelectedIndexChanged里查询并重新绑定 DataSource结果绑定动作又触发一次SelectedIndexChanged导致同一个操作发两次 SQL。处理手法是加一个loading标志并在SelectionChangeCommitted里处理用户选择private bool _loading; private async void cmbDept_SelectionChangeCommitted(object sender, EventArgs e) { if (_loading) return; var dept cmbDept.SelectedItem as Department; if (dept null) return; _loading true; try { var list await _scheduleService.GetDeptScheduleAsync(dept.DeptId, dtpWorkDate.Value.Date); BindDoctorGrid(list); } finally { _loading false; } } private void BindDoctorGrid(ListScheduleView list) { dgvSchedule.DataSource null; dgvSchedule.DataSource list; dgvSchedule.ClearSelection(); }逻辑说明SelectionChangeCommitted只在我们通过下拉列表选择时触发代码赋值不会触发配合_loading标志可以杜绝 DataSource 赋值造成的级联刷新。BindDoctorGrid先置空再赋值是为了强制 DataGridView 重新绑定否则修改集合后界面不刷新。ScheduleView是业务层返回的视图模型包含排班日期、医生姓名、职称、剩余号数。参数说明ComboBox 的DisplayMember设为DeptNameValueMember设为DeptId。GetDeptScheduleAsync在 Service 层内部用异步查询避免async void事件处理器里又同步阻塞线程池。3.3 DataGridView 中把 List 的 0/1 显示为 CheckBox窗口护士不看 0 和 1她们要看“有没有号”。常见的 WinForms 做法是加一列DataGridViewCheckBoxColumn绑定数据源后通过CellFormatting把 int 值转换成布尔显示private void dgvSchedule_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (dgvSchedule.Columns[e.ColumnIndex].Name colAvailable) { if (e.Value ! null) { int state Convert.ToInt32(e.Value); e.Value state 1; e.FormattingApplied true; } } }为了让这一列显示为勾选框在设计器里添加一列DataGridViewCheckBoxColumn列名叫colAvailable并把它的DataPropertyName留空靠格式化事件填充。查询出来的排班数据里有一个available字段1 表示有号0 表示无号。逻辑说明FormattingApplied true表示我们已格式化DataGridView 不会再用原始 int 去覆盖显示。这个事件每次滚动都会触发所以里面绝对不能查库只做类型转换。ReadOnly要设为true防止护士误点击勾选框把状态改掉状态修改必须走退号或过号流程。3.4 日期选择组件DateTimePicker 控制排班日期排班必须区分日期。把DateTimePicker放在医生列表上方选择日期后重新查询。因为挂号系统通常要提前看未来一周的号MinDate设为今天MaxDate设为排班接口返回的最远日期。private async void dtpWorkDate_ValueChanged(object sender, EventArgs e) { if (_loading) return; _loading true; try { var list await _scheduleService.GetDeptScheduleAsync(_currentDeptId, dtpWorkDate.Value.Date); BindDoctorGrid(list); } finally { _loading false; } }参数说明ValueChanged不仅由用户改日期触发代码里设置Value也会触发所以统一用_loading控制。.Date会去掉时间部分传给 SQL 时避免2025-06-02 00:00:00和2025-06-02 23:59:59的边界问题。3.5 WinForms 界面美化不用第三方控件也能做得干净很多课程设计会用沉重的皮肤库我反而不推荐。挂号系统图标少、树多、表格多用系统自带控件做适当配置就够主窗体放一张 1920x1080 的浅色背景图注意把图片压缩到几百 KB否则打开窗体明显卡顿。按钮设置FlatStyle FlatFlatAppearance.BorderColor用浅灰比默认的 3D 边框好看。DataGridView 开启AlternatingRowsDefaultCellStyle.BackColor Color.FromArgb(245, 247, 250)交替行色能让长列表更容易读。字体统一用微软雅黑 10pt标题列文字用 9pt 加粗。这些零散配置不耽误时间但能决定护士愿不愿意用。4. 号源锁定与状态流转挂号不是 INSERT而是一串 UPDATE挂号系统最容易出问题的就是并发。两个窗口同时挂最后一个号如果实现是“先 SELECT 剩余号数若大于 0 再 INSERT”那几乎必然超卖。这一章讲清楚事务、锁和状态机是最核心的代码部分。4.1 用条件 UPDATE 扣减号源判断和扣减合成一条 SQL一个稳妥的扣号实现如下。注意这里必须显式打开连接并且整个操作共用一个事务public bool TryRegister(int scheduleId, string patientName, string idNo, string phone) { using var conn new SqlConnection(DbHelper.ConnStr); conn.Open(); using var tran conn.BeginTransaction(); try { var updateSql UPDATE t_schedule SET used_count used_count 1, version_no version_no 1 WHERE schedule_id sid AND used_count total_count; using (var cmd new SqlCommand(updateSql, conn, tran)) { cmd.Parameters.Add(sid, SqlDbType.Int).Value scheduleId; int rows cmd.ExecuteNonQuery(); if (rows 0) { tran.Rollback(); return false; // 已经没有号源 } } var insertSql INSERT INTO t_registration (schedule_id, patient_name, patient_id_no, phone) VALUES (sid, name, idno, phone); using (var cmd new SqlCommand(insertSql, conn, tran)) { cmd.Parameters.Add(sid, SqlDbType.Int).Value scheduleId; cmd.Parameters.Add(name, SqlDbType.NVarChar, 50).Value patientName; cmd.Parameters.Add(idno, SqlDbType.VarChar, 18).Value idNo; cmd.Parameters.Add(phone, SqlDbType.VarChar, 11).Value phone; cmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch { tran.Rollback(); throw; } }逻辑说明UPDATE ... WHERE used_count total_count是关键。这条 UPDATE 本身会对t_schedule里的对应行加排他锁两个窗口同时执行时第二个会等第一个提交后继续此时used_count已经等于total_count影响行数为 0于是干净地返回失败。这就是“以 UPDATE 行锁为主”的悲观锁方案代码量最少也最容易让 DBA 看懂。参数说明SqlDbType.Int、NVarChar这些显式类型最好写全不要只靠.AddWithValue避免字符串长度和类型推断不一致导致参数嗅探问题。used_count条件更新成功后version_no顺手加一是为将来做乐观锁预留的现在不查也没关系。4.2 状态机挂号单不能删只能改状态挂号记录的状态我用四个值1 已挂号、2 已退号、3 已过号、4 已就诊。退号不是DELETE而是public bool CancelRegister(long regId) { using var conn new SqlConnection(DbHelper.ConnStr); conn.Open(); using var tran conn.BeginTransaction(); try { // 先确认状态可退 var checkSql SELECT reg_status FROM t_registration WHERE reg_id rid; // 如果状态不是 1直接返回 false防止重复退号 var updateReg UPDATE t_registration SET reg_status 2, cancel_time SYSDATETIME() WHERE reg_id rid AND reg_status 1; int rows ExecuteInTran(conn, tran, updateReg, new SqlParameter(rid, SqlDbType.BigInt) { Value regId }); if (rows 0) { tran.Rollback(); return false; } // 对应排班的 used_count 减一 var updateSchedule UPDATE t_schedule SET used_count used_count - 1 WHERE schedule_id (SELECT schedule_id FROM t_registration WHERE reg_id rid); ExecuteInTran(conn, tran, updateSchedule, new SqlParameter(rid, SqlDbType.BigInt) { Value regId }); tran.Commit(); return true; } catch { tran.Rollback(); throw; } }状态流转只有这几条合法路径已挂号到已退号就诊前可退已挂号到已就诊医生开始叫号时修改已挂号到已过号叫号三次未到。已经变成退号、过号或就诊后的记录不允许再改回已挂号。这个规则要同时写在业务层和数据库约束里数据库加约束是最后一道保险。4.3 用委托和事件把“挂完了”推给 UIWinForms 界面是事件驱动的Service 层不应该直接调用窗体方法。我会给 Service 暴露事件窗体订阅后刷新列表。这是 C# 委托和事件最实在的应用场景public class RegisterService { public event EventHandler? RegistrationChanged; private void OnRegistrationChanged() { RegistrationChanged?.Invoke(this, EventArgs.Empty); } public bool TryRegister(...) { // 挂号和退号成功的地方都调用 OnRegistrationChanged bool ok ...; if (ok) OnRegistrationChanged(); return ok; } }窗体订阅_service.RegistrationChanged (s, e) { if (this.InvokeRequired) this.Invoke(new Action(LoadCurrentRegisterList)); else LoadCurrentRegisterList(); };逻辑说明Service 层的挂号和退号可能发生在后台线程直接操作 DataGridView 会报跨线程异常。这里用InvokeRequired判断然后Invoke回到 UI 线程再刷新列表。事件的好处是窗体和业务层完全解耦以后加一个叫号屏同样可以订阅这个事件不用改 Service。4.4 换号操作先退再挂但必须包成一个事务窗口护士常常要改号患者说这个医生不合适要换邻科室。实现上不能拆成两次独立操作否则中间崩溃会导致只退号未挂号。正确做法是做组合方法public bool ChangeRegister(long oldRegId, int newScheduleId, string patientName, string idNo, string phone) { using var conn new SqlConnection(DbHelper.ConnStr); conn.Open(); using var tran conn.BeginTransaction(); try { // 退旧号并确认退成功 // 挂新号并确认号源扣减成功 // 成功才提交 tran.Commit(); return true; } catch { tran.Rollback(); throw; } }换号失败时旧号已经被释放新号没挂上患者会觉得是系统吞掉了号。这种操作日志要单独记录至少能回答“护士到底点了什么”。5. 避坑清单WinForms 挂号系统上线前必须排掉的 5 个雷这里每一条都是门诊现场真实出现过的坑。按“现象 → 原因 → 解决”写出来调试时可以逐个对照。5.1 UI 卡死护士连点三下挂号按钮程序像冻住现象点击挂号后整个窗体失去响应过几秒才弹出提示甚至在高峰时段直接白屏。原因在按钮 Click 里同步调用了ExecuteNonQuery数据库在批量操作时卡住了 UI 线程同时按钮没有禁用护士重复点击造成多个请求排队。解决把耗时操作放到Task.Run里并立刻禁用按钮private async void btnRegister_Click(object sender, EventArgs e) { btnRegister.Enabled false; try { var ok await Task.Run(() _service.TryRegister(_currentScheduleId, txtName.Text.Trim(), txtIdNo.Text.Trim(), txtPhone.Text.Trim())); MessageBox.Show(ok ? 挂号成功 : 当前号源已用完); } finally { btnRegister.Enabled true; } }Enabled false配合await Task.Run一来 UI 不卡二来防止重复提交。这个简单处理能救回很多“连点”事故。5.2 号源超卖明明显示有 1 个号两个人都能挂上现象两个窗口同时操作都查到剩余 1 个号最后系统里出现 2 条成功挂号记录。原因先 SELECT 判断号源再 INSERT 挂号记录两步之间没有事务和条件更新第二个窗口读到的是旧值。解决用 4.1 的条件 UPDATE 扣减号源让数据库来判断used_count total_count。再加一层数据库兜底ALTER TABLE t_schedule ADD CONSTRAINT ck_schedule_count CHECK (used_count total_count AND used_count 0);这个约束能保证即使业务层写错数据库也不会接受超卖数据。5.3 日期边界错乱查上午的号下午的号也出现在列表里现象选择某一天“上午”查询结果却混入下午时段的排班。原因把DateTimePicker的值直接拼进 SQL或者用 date和 date比较datetime字段时秒级边界处理不当。解决表里work_date用DATE类型查询统一写成WHERE work_date date业务层传参时用dtpWorkDate.Value.Date不要传带时间的DateTime。如果一定要用datetime字段就用半开区间WHERE work_date startDate AND work_date DATEADD(day, 1, startDate)5.4 高 DPI 显示器上窗体文字模糊、按钮错位现象在 150% 缩放的笔记本上打开挂号窗体标题栏正常但 DataGridView 列宽偏窄按钮挤成一团。原因WinForms 默认按 96 DPI 设计没有声明 DPI 感知Windows 对窗体做位图拉伸导致模糊和错位。解决在app.manifest中启用 PerMonitorV2 DPI 感知并把每个窗体的AutoScaleMode设为Font。如果医院里既有 1080P 台式机又有 2K 笔记本重点保证挂号窗体的MinimumSize宁可留白也不要让右侧操作面板被压缩。5.5 连接池爆满挂号半小时后所有浏览器窗口都在转圈现象系统运行一会儿后所有查询超时DBA 看到数据库连接数打满。原因某条查询路径没有释放SqlConnection连接池里的连接一直处于占用状态。最常见的是 DataAdapter 填充 DataTable 时异常using没覆盖到整个连接生命周期。解决所有连接走 2.3 的封装全局创建和释放不要在大方法里自行new SqlConnection。调试时打开 SQL Server 的sys.dm_exec_sessions查看登录名对应的会话数能快速定位是哪个模块没释放。连接字符串里另加Max Pool Size200只是缓解根治是确保using覆盖完整。6. 进阶技巧日志、统计与部署让挂号系统真的能进门诊6.1 用 NLog 记录每笔挂号的“后悔药”退号对账时最怕“说不出谁在几点退的”。我习惯用 NLogNuGet 装包后写一个NLog.config文件日志按日期滚动。关键操作只记几行Logger.Info($挂号成功 RegId{regId} ScheduleId{scheduleId} Patient{patientName}); Logger.Info($退号成功 RegId{regId} Operator{currentUser});日志里不要写完整 SQL 语句避免敏感数据落盘范围过大。6.2 一天的账怎么对统计 SQL 看这几个数门诊要的是挂号总数、退号数、按科室分布。下面这条查询能出当天日报SELECT d.dept_name, COUNT(r.reg_id) AS reg_count, SUM(CASE WHEN r.reg_status 2 THEN 1 ELSE 0 END) AS cancel_count FROM t_registration r JOIN t_schedule s ON r.schedule_id s.schedule_id JOIN t_doctor d ON s.doc_id d.doc_id WHERE r.create_time startDate AND r.create_time endDate GROUP BY d.dept_name ORDER BY reg_count DESC; startDate AND endDate这个半开区间写法能覆盖整天不会漏掉 23:59:59 之后的记录。导出用 DataGridView 全选复制到 Excel 就够不建议引 Office 互操作组件。6.3 部署到门诊机器绿色包优先ClickOnce 做更新门诊电脑通常不联网也不一定装全 .NET SDK。我先把项目发布成单文件夹压缩后解压到固定目录配好 App.config 里的连接字符串再创建一个快捷方式。后期要更新时用 ClickOnce 发布到内网共享目录客户端双击一个.application文件就能升级。结束前说一个习惯我经手挂号系统后会在t_registration旁边加一张t_register_audit把谁在什么时间挂、退、改全记下来。别指望人不出错只有日志能给你后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表