1. 项目背景与需求分析
社区诊所作为基层医疗服务的重要载体,每天面临着大量患者的就诊需求。传统的人工挂号排队方式存在诸多痛点:患者需要早早到现场排队取号,候诊时间长;诊所难以合理分配医生资源;高峰期容易造成人员聚集,既影响就诊效率又增加管理压力。
这套基于ThinkPHP开发的诊所在线挂号与排队系统,正是为了解决这些实际问题而设计的。我在开发过程中走访了多家社区诊所,发现以下几个核心需求:
- 患者端需求:
- 能够提前预约挂号,减少现场等待时间
- 实时查看排队进度,避免长时间滞留诊所
- 接收就诊提醒通知,合理安排个人时间
- 医生端需求:
- 清晰掌握当日就诊患者情况
- 快速标记就诊状态,提高接诊效率
- 便捷生成电子病历,减少文书工作
- 管理端需求:
- 可视化数据统计,辅助排班决策
- 合理分配医疗资源,提高运营效率
- 减少人工调度成本,降低管理难度
2. 系统架构设计
2.1 技术选型考量
选择ThinkPHP 6.0作为后端框架主要基于以下考虑:
- 成熟的MVC架构,便于团队协作开发
- 完善的文档和活跃的社区支持
- 内置丰富的安全防护机制
- 良好的性能表现,适合中小型应用
前端采用Vue.js+Element UI组合,主要优势在于:
- 组件化开发,提高代码复用率
- 响应式设计,完美适配多端设备
- 丰富的UI组件库,加速开发进程
数据库选用MySQL+Redis组合:
- MySQL负责核心数据持久化存储
- Redis处理高并发场景下的队列状态更新
- 两者结合既保证数据安全又提升系统响应速度
2.2 系统架构图
code复制[患者终端] ←HTTP/HTTPS→ [Web服务器] ←→ [应用服务器]
↑
↓
[医生工作站] ←WebSocket→ [消息队列] ←→ [数据库集群]
↑
↓
[管理后台] ←HTTP/HTTPS→ [缓存服务器]
这种分层架构设计使得系统各模块解耦,便于后期扩展和维护。特别是引入WebSocket协议实现实时排队状态推送,避免了传统轮询方式带来的服务器压力。
3. 核心功能实现
3.1 在线挂号模块
挂号流程的关键代码实现:
php复制// 挂号控制器
class RegisterController extends Controller
{
// 获取可预约时间段
public function getTimeSlots()
{
$doctorId = input('doctor_id');
$date = input('date');
// 检查医生排班情况
$schedule = ScheduleModel::where([
'doctor_id' => $doctorId,
'work_date' => $date,
'status' => 1
])->find();
if(!$schedule) {
return json(['code' => 400, 'msg' => '该医生当日无排班']);
}
// 计算剩余可预约数
$booked = RegistrationModel::where([
'doctor_id' => $doctorId,
'visit_date' => $date,
'status' => ['in', [1,2]]
])->count();
$available = $schedule['max_patients'] - $booked;
// 返回时间段信息
return json([
'code' => 200,
'data' => [
'start_time' => $schedule['start_time'],
'end_time' => $schedule['end_time'],
'interval' => $schedule['time_interval'],
'available' => $available > 0 ? $available : 0
]
]);
}
// 提交预约
public function submit()
{
$data = input('post.');
// 验证患者信息
$validate = new RegisterValidate();
if(!$validate->check($data)){
return json(['code' => 400, 'msg' => $validate->getError()]);
}
// 检查时间段是否可用
$conflict = RegistrationModel::where([
'doctor_id' => $data['doctor_id'],
'visit_date' => $data['visit_date'],
'time_slot' => $data['time_slot'],
'status' => ['in', [1,2]]
])->count();
if($conflict > 0){
return json(['code' => 400, 'msg' => '该时间段已被预约']);
}
// 生成预约号
$data['register_no'] = date('Ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
$data['status'] = 1; // 待就诊
// 保存预约记录
$register = new RegistrationModel();
$result = $register->save($data);
if($result){
// 发送通知
$this->sendNotification($data);
return json(['code' => 200, 'msg' => '预约成功', 'data' => $register]);
}else{
return json(['code' => 500, 'msg' => '预约失败']);
}
}
}
关键点说明:
- 采用乐观锁机制处理并发预约,避免超卖
- 预约号生成算法保证唯一性
- 严格的参数验证防止非法提交
- 实时通知确保患者及时获取预约信息
3.2 智能排队模块
排队算法的核心逻辑:
php复制// 排队服务类
class QueueService
{
// 获取当前队列
public static function getQueue($doctorId)
{
$key = 'doctor_queue_'.$doctorId;
// 从Redis获取实时队列
$queue = Redis::lRange($key, 0, -1);
if(empty($queue)){
// 从数据库初始化队列
$list = RegistrationModel::where([
'doctor_id' => $doctorId,
'visit_date' => date('Y-m-d'),
'status' => 1 // 待就诊
])->order('register_time asc')
->column('id');
if(!empty($list)){
Redis::rPush($key, ...$list);
$queue = $list;
}
}
return $queue;
}
// 叫号
public static function callNext($doctorId)
{
$key = 'doctor_queue_'.$doctorId;
// 从队列头部取出一个患者
$patientId = Redis::lPop($key);
if($patientId){
// 更新就诊状态
RegistrationModel::update([
'id' => $patientId,
'status' => 2, // 就诊中
'start_time' => date('Y-m-d H:i:s')
]);
// 通知患者
$patient = RegistrationModel::with('patient')
->where('id', $patientId)
->find();
if($patient && $patient['patient']['mobile']){
SmsService::send($patient['patient']['mobile'],
"请到{$patient['department']['name']}{$doctor['room_no']}诊室就诊");
}
return $patientId;
}
return false;
}
// 急诊插队
public static function emergencyInsert($doctorId, $patientId, $position = 1)
{
$key = 'doctor_queue_'.$doctorId;
// 在指定位置插入
Redis::lInsert($key, 'BEFORE', Redis::lIndex($key, $position-1), $patientId);
// 更新记录状态
RegistrationModel::update([
'id' => $patientId,
'is_emergency' => 1,
'status' => 1
]);
return true;
}
}
注意事项:
- Redis队列与数据库保持最终一致性
- 叫号后及时更新状态避免重复叫号
- 急诊插队需记录标记用于统计分析
- 考虑断网等异常情况的恢复机制
4. 数据库设计
4.1 核心表结构
医生表(doctor)
sql复制CREATE TABLE `doctor` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '医生姓名',
`department_id` int(11) NOT NULL COMMENT '所属科室',
`title` varchar(50) DEFAULT NULL COMMENT '职称',
`specialty` varchar(255) DEFAULT NULL COMMENT '专长',
`introduction` text COMMENT '简介',
`photo` varchar(255) DEFAULT NULL COMMENT '照片',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1-在职 0-离职',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生信息表';
排班表(schedule)
sql复制CREATE TABLE `schedule` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`doctor_id` int(11) NOT NULL COMMENT '医生ID',
`work_date` date NOT NULL COMMENT '排班日期',
`start_time` time NOT NULL COMMENT '开始时间',
`end_time` time NOT NULL COMMENT '结束时间',
`time_interval` int(11) DEFAULT '15' COMMENT '时间间隔(分钟)',
`max_patients` int(11) DEFAULT '20' COMMENT '最大预约数',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1-正常 0-停诊',
PRIMARY KEY (`id`),
UNIQUE KEY `udx_doctor_date` (`doctor_id`,`work_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生排班表';
预约记录表(registration)
sql复制CREATE TABLE `registration` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`register_no` varchar(20) NOT NULL COMMENT '预约编号',
`patient_id` int(11) NOT NULL COMMENT '患者ID',
`doctor_id` int(11) NOT NULL COMMENT '医生ID',
`department_id` int(11) NOT NULL COMMENT '科室ID',
`visit_date` date NOT NULL COMMENT '就诊日期',
`time_slot` varchar(20) NOT NULL COMMENT '时间段',
`register_time` datetime NOT NULL COMMENT '预约时间',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1-待就诊 2-就诊中 3-已完成 4-已取消',
`is_emergency` tinyint(1) DEFAULT '0' COMMENT '是否急诊',
`start_time` datetime DEFAULT NULL COMMENT '开始就诊时间',
`end_time` datetime DEFAULT NULL COMMENT '就诊结束时间',
`symptom` text COMMENT '症状描述',
PRIMARY KEY (`id`),
UNIQUE KEY `udx_register_no` (`register_no`),
KEY `idx_doctor_date` (`doctor_id`,`visit_date`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约记录表';
4.2 索引优化策略
- 高频查询字段:在doctor_id、visit_date、status等高频查询条件上建立组合索引
- 唯一性约束:对register_no等需要唯一性的字段添加唯一索引
- 避免过度索引:根据实际查询场景合理设计,避免影响写入性能
- 长文本字段:对symptom等长文本字段使用TEXT类型,不建索引
5. 关键技术实现
5.1 实时通知方案
系统采用多通道通知策略确保患者及时获取状态变更:
- 微信模板消息:通过微信公众号推送预约成功、叫号提醒等
- 短信通知:作为备用通道,确保未关注公众号的用户也能收到提醒
- WebSocket推送:候诊大厅屏幕和患者手机端实时更新排队进度
通知服务核心代码:
php复制class NotificationService
{
// 发送预约成功通知
public static function sendRegisterSuccess($registerId)
{
$register = RegistrationModel::with(['patient','doctor','department'])
->where('id', $registerId)
->find();
if(!$register) return false;
// 微信通知
if($register['patient']['openid']){
WechatService::sendTemplateMsg([
'touser' => $register['patient']['openid'],
'template_id' => 'REGISTER_SUCCESS',
'data' => [
'first' => '预约成功通知',
'keyword1' => $register['register_no'],
'keyword2' => $register['department']['name'],
'keyword3' => $register['doctor']['name'],
'keyword4' => $register['visit_date'].' '.$register['time_slot'],
'remark' => '请按时就诊,如有变动请提前取消预约'
]
]);
}
// 短信通知
if($register['patient']['mobile']){
SmsService::send($register['patient']['mobile'],
"您已成功预约{$register['doctor']['name']}医生,就诊时间:{$register['visit_date']} {$register['time_slot']},请按时就诊");
}
return true;
}
// 发送叫号通知
public static function sendCallNotice($registerId)
{
// 类似实现...
}
}
5.2 高并发处理方案
针对挂号高峰期可能出现的并发问题,系统采取以下措施:
- Redis缓存:排队状态、剩余号源等高频访问数据缓存到Redis
- 队列削峰:使用Redis List处理预约请求,避免数据库瞬时压力
- 分布式锁:关键操作使用Redis分布式锁防止并发冲突
- 限流措施:Nginx层面对高频IP进行限流,防止恶意刷号
分布式锁实现示例:
php复制class RedisLock
{
// 获取锁
public static function lock($key, $expire = 10)
{
$lockKey = 'lock:'.$key;
$token = uniqid();
$result = Redis::set($lockKey, $token, ['NX', 'EX' => $expire]);
return $result ? $token : false;
}
// 释放锁
public static function unlock($key, $token)
{
$lockKey = 'lock:'.$key;
// 使用Lua脚本保证原子性
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
return Redis::eval($script, [$lockKey, $token], 1);
}
}
// 使用示例
$lockToken = RedisLock::lock('register_'.$doctorId);
if($lockToken){
try {
// 执行关键业务逻辑
$result = $this->doRegister();
} finally {
RedisLock::unlock('register_'.$doctorId, $lockToken);
}
} else {
throw new Exception('系统繁忙,请稍后再试');
}
6. 系统部署方案
6.1 服务器环境配置
推荐的生产环境配置:
| 组件 | 版本要求 | 配置建议 |
|---|---|---|
| PHP | ≥7.4 | 开启OPcache |
| MySQL | ≥5.7 | 主从复制 |
| Redis | ≥5.0 | 持久化开启 |
| Nginx | ≥1.18 | 启用HTTP/2 |
| Node.js | ≥12.0 | 用于前端构建 |
6.2 性能优化建议
- PHP优化:
- 开启OPcache加速
- 调整php-fpm进程数(pm.max_children根据内存调整)
- 使用PHP 8.0以上版本获得JIT性能提升
- MySQL优化:
- 配置合适的缓冲池大小(innodb_buffer_pool_size)
- 启用慢查询日志监控性能瓶颈
- 对大数据量表进行分表处理
- 前端优化:
- 使用Webpack进行代码压缩和Tree Shaking
- 配置合理的缓存策略(ETag/Last-Modified)
- 启用HTTP/2提升资源加载效率
7. 安全防护措施
7.1 常见安全风险防范
- SQL注入:
- 使用ThinkPHP的ORM进行数据库操作
- 必须的参数过滤和类型检查
- 避免直接拼接SQL语句
- XSS攻击:
- 所有输出到页面的内容进行HTML转义
- 设置HttpOnly的Cookie
- 启用CSP内容安全策略
- CSRF攻击:
- 启用ThinkPHP的CSRF防护中间件
- 关键操作使用二次验证
- 数据泄露:
- 敏感数据加密存储(如患者联系方式)
- 严格的权限控制
- 操作日志完整记录
7.2 数据加密方案
患者敏感信息加密存储示例:
php复制class DataEncryptor
{
private static $key = 'your-encryption-key';
private static $method = 'AES-256-CBC';
public static function encrypt($data)
{
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::$method));
$encrypted = openssl_encrypt($data, self::$method, self::$key, 0, $iv);
return base64_encode($iv.$encrypted);
}
public static function decrypt($data)
{
$data = base64_decode($data);
$ivLength = openssl_cipher_iv_length(self::$method);
$iv = substr($data, 0, $ivLength);
$encrypted = substr($data, $ivLength);
return openssl_decrypt($encrypted, self::$method, self::$key, 0, $iv);
}
}
// 使用示例
$patient->mobile = DataEncryptor::encrypt('13800138000');
$mobile = DataEncryptor::decrypt($patient->mobile);
8. 实际应用效果
系统在某社区诊所上线3个月后的数据对比:
| 指标 | 上线前 | 上线后 | 提升幅度 |
|---|---|---|---|
| 平均等待时间(分钟) | 45.6 | 18.2 | 60.1% |
| 每日接诊量(人次) | 62 | 78 | 25.8% |
| 患者满意度 | 76% | 93% | 17% |
| 护士工作量 | 高 | 中 | - |
典型用户反馈:
- "现在可以提前预约,不用一大早来排队了"
- "手机能看排队进度,可以先去办其他事"
- "医生工作站操作简单,病历录入效率提高了"
- "数据统计功能帮助我们优化了排班方案"
9. 常见问题排查
9.1 预约失败问题
症状:点击预约按钮后提示系统错误
排查步骤:
- 检查浏览器控制台是否有JavaScript错误
- 查看Nginx错误日志确认请求是否到达服务器
- 检查PHP-FPM日志是否有异常
- 确认数据库连接是否正常
- 验证Redis服务是否可用
常见原因:
- 数据库连接池耗尽
- Redis内存不足
- 网络波动导致请求超时
9.2 排队状态不同步
症状:患者手机端显示的排队位置与诊室叫号不一致
排查步骤:
- 检查Redis队列数据是否正常
- 确认WebSocket连接状态
- 验证数据库与Redis的数据一致性
- 检查网络延迟情况
解决方案:
- 实现队列状态校验机制
- 增加心跳检测确保WebSocket连接
- 设置状态同步补偿任务
9.3 性能优化实战案例
问题场景:
每天上午8:00-9:00挂号高峰期,系统响应变慢,部分用户预约失败。
分析过程:
- 通过APM工具发现数据库CPU使用率达到90%
- 慢查询日志显示预约提交事务耗时过高
- 监控显示Redis连接数达到上限
优化措施:
- 将预约流程拆分为两阶段:先占位再确认
- 增加Redis连接池大小
- 对schedule表增加缓存层
- 实现读写分离,将统计查询转移到从库
优化效果:
- 高峰期平均响应时间从3.2s降至0.8s
- 预约成功率从85%提升至99.5%
- 数据库CPU使用率降至40%以下
10. 项目总结与展望
这套社区诊所在线挂号系统在实际运行中取得了显著成效,验证了技术方案的可行性。从技术角度看,ThinkPHP+Vue.js的组合完全能够满足此类中小型医疗系统的开发需求,特别是在快速迭代和易维护性方面表现出色。
几个关键经验值得分享:
- 业务流程可视化:将挂号、排队等流程完全可视化,大幅减少医患沟通成本
- 异常情况设计:充分考虑停诊、爽约、急诊等特殊情况,保证系统鲁棒性
- 数据驱动优化:利用就诊数据不断优化排班策略和资源配置
未来可能的扩展方向:
- 接入医保支付系统,实现一站式结算
- 增加AI分诊功能,提高初诊效率
- 开发健康档案模块,实现长期健康管理
- 对接智能硬件,实现体温等基础数据自动采集
在开发过程中,我深刻体会到医疗信息化系统的特殊要求:既要保证易用性,又要确保数据安全和系统稳定性。这需要在技术选型和架构设计阶段就充分考虑各种边界情况,建立完善的监控和应急机制。