美大夫预约系统

一、原始版

1、医生精准预约

后台设置医生在不同机构的每天出诊时间,出诊类型分 循环出诊 和临时出诊;

  • 设置循环出诊,生成未来90天的循环出诊信息;

​ 比如后台设置循环出诊 每周一上午10:00-12:00,会提前生成未来90天每周一10:00-12:00的数据。

  • 设置临时出诊,只是生成固定日期的出诊信息

    比如 设置 2021年7月1号-2021年7月3号 10:00-12:00 出诊,只是会生成7.1号-7.3号的出诊数据

2、实现思想

  • 后台生成医生在不同机构的出诊时间模板

  • 依赖时间模板定时生成未来90天预约数据

  • 前端根据生成的预约数据展示给用户可约数据

3、数据约束

  • 医生
  • 日期+时间
  • 机构
  • 预约类型,临时出诊,循环出诊
  • 时间宽度单位 每30分钟 为一个时间单位

4、模板生成预约数据技术实现

工厂模式 + 枚举算法

5、设计图

后台工厂模式生成预约数据设计图

6、ER图

image.png

7、后台设置

8、前端预约

image-20210602120249657

image-20210602120238712

9、代码端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
<?php

namespace App\Helper;

use App\Models\Diagnose;
use App\Models\DiagnoseLog;
use App\Models\DiagnoseSchedule;
use App\Models\ScheduleModel;
use App\Models\Space;
use App\Models\StopDiagnose;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;

/**
* @desc 精准预约生成日期的工具类
*
* 1:添加预约模版
*
* 2:批量生成循环的3个月的预约数据
*
* 3:定时任务每天生成第90天的数据
*
* 4:修改临时出诊数据
*
* 5:设置停诊数据
*
* 6:新增出诊数据
*
* 7:添加日志
*
* 8:删除预约模版
*
* 生成每天8:00到21:30的预约
**/
class AppointmentTool
{
public $startTime = 8;
public $endTime = 22;

public $operateId='';
public $operateType='';
public $logType = '';
public $dayCount = 90;
public function __construct($operateId,$operateType,$logType)
{
$this->operateId = $operateId;
$this->operateType = $operateType;
$this->logType = $logType;
}
public function writeLog( $content,$logType ='')
{
$param['operater_id'] = $this->operateId;
$param['operater_type'] = $this->operateType;
$param['operate_content'] = $content;
$param['log_type'] = empty($logType) ? $this->logType : $logType;
DiagnoseLog::create($param);
}

/**
* 1:添加预约模版
* (1)判断是否已经有预约的模版
* (2)添加模版
*(3)生成90天的数据
***/
public function setSchduleModel($spaceId, $workUnitId, $workUnName, $daySchdules)
{
$params['space_id'] = $spaceId;
$params['work_unit_id'] = $workUnitId;
$params['work_unit_name'] = $workUnName;
foreach ($daySchdules as $daySchdule)
{
$params['day_schdule'] = '';
$flag = explode(':',$daySchdule['time']);
if((int)$flag[0]<12 && (int)$flag[0]>=8)
{
$params['day_schdule'] = 'morrning';
}
elseif((int)$flag[0]>=12 && (int)$flag[0]<18)
{
$params['day_schdule'] = 'afternoon';
}
elseif((int)$flag[0]>=18 && (int)$flag[0]<22)
{
$params['day_schdule'] = 'evening';
}
else
{
continue;
}
$params['week'] = $daySchdule['week'];
$params['time'] = $daySchdule['time'];
$params['is_stop_diagnose'] = 0;
$params['is_delete'] = 0;
$this->createSchduleModel($params);
}
return true;
}

//创建预约模版
public function createSchduleModel($param,$logType='')
{
$where['space_id'] = $param['space_id'];
$where['work_unit_id'] = $param['work_unit_id'];
$where['week'] = $param['week'];
$where['time'] = $param['time'];
$where['is_delete'] = $param['is_delete'];
$res = ScheduleModel::firstOrCreate($where,$param);
$this->writeLog('生成模版数据:'.json_encode($param),$logType);
}

/**
* 定时任务生成生成第90天的数据
**/
public function creatSchduleData()
{
$nowObj = Carbon::now();
// $nintySchdule = $nowObj->addDay($this->dayCount);
//$week = $nintySchdule->dayOfWeek;
$week =$nowObj->dayOfWeek;
$spaceIds = ScheduleModel::where('week',$week)->select('space_id')->distinct()->get();
if($spaceIds->isEmpty())
{
Log::error(date('Y-m-d H:i:s').'模版第90天数据不存在,spaceId',$spaceIds->toArray());
return false;
}
$spaceArrs = $spaceIds->toArray();
foreach ($spaceArrs as $spaceArr)
{
$spaceObj = Space::find($spaceArr['space_id']);

$modelDatas = ScheduleModel::where('week',$week)
->where('space_id',$spaceArr['space_id'])
->where('is_stop_diagnose',ScheduleModel::STOPDIAGNOSENO)
->where('is_delete',ScheduleModel::DELETENO)
->get();
if($modelDatas->isEmpty())
{
continue;
}
foreach ($modelDatas as $modelData)
{
$param['space_id'] = $modelData->space_id;
$param['work_unit_id'] = $modelData->work_unit_id;
$param['appoint_date'] = '';
$param['user_id']=$spaceObj->user_id;
$param['work_unit_name']=$modelData->work_unit_name;
$param['day_schdule'] = $modelData->day_schdule;
$param['time'] = $modelData->time;
$param['week'] = $modelData->week;
$param['is_stop_diagnose'] =$modelData->is_stop_diagnose;
$param['is_delete'] =$modelData->is_delete;
$param['number'] =1;
$param['schdule_type'] = DiagnoseSchedule::CYCLE;
$param['model_id']=$modelData->id;
$param['date_time'] = '';
$res = $this->createOperateData($param);
if($res)
{
Log::info('定时生成每周数据',$param);
}
else
{
Log::error('定时生成数据失败',$param);
}
}
}
}

/**
* @desc 4:修改或者添加临时出诊数据
**/
public function setInterimSchdule($spaceId, $userId, $workUnitId, $workUnName, $startTime, $endTime ,$schduleType,$modelId=0)
{
$timeRes = $this->changeTimeToSchdule($startTime, $endTime);
if ($timeRes['errorCode'] != 0) {
return $timeRes;
}
$timeDatas = $timeRes['data'];
foreach ($timeDatas as $timeData) {
$flag = [
'space_id' => $spaceId,
'work_unit_id' => $workUnitId,
'appoint_date' => $timeData['date'],
'user_id'=>$userId,
'work_unit_name'=>$workUnName
];
foreach ($timeData['daySchdule'] as $daySchdules)
{
$flag['day_schdule'] = $daySchdules[3];
$flag['time'] = $daySchdules[1];
$flag['week'] = $timeData['week'];
$flag['appoint_date'] =$timeData['date'];
$flag['is_stop_diagnose'] =0;
$flag['is_delete'] =0;
$flag['number'] =1;
$flag['schdule_type']=$schduleType;
$flag['model_id']=$modelId;
$flag['date_time'] = $timeData['date'].' '.$daySchdules[1];
$params = $flag;
$params['date_time'] = $timeData['date'].' '.$daySchdules[2];
$this->addScheduleData($flag);
if( isset($daySchdules[2]) && !empty($daySchdules[2]))
{
$params['time'] = $daySchdules[2];
$this->addScheduleData($params);
}
}
}
}

/**
* @desc 添加数据生成预约数据
***/
private function addScheduleData($param,$logType='')
{
$where['space_id'] = $param['space_id'];
$where['work_unit_id'] = $param['work_unit_id'];
$where['appoint_date'] = $param['appoint_date'];
$where['time'] = $param['time'];
$where['is_delete'] = $param['is_delete'];
$diagneseSchedulObjs = DiagnoseSchedule::where('space_id',$where['space_id'])
->where('work_unit_id',$where['work_unit_id'])
->where('appoint_date',$where['appoint_date'])
->where('time',$where['time'])
->where('is_delete',$where['is_delete'])
->get();
if($diagneseSchedulObjs->isEmpty())
{
$stopDiagnoseObj = StopDiagnose::where(
['space_id'=>$param['space_id'],'time'=>$param['time'],'appoint_date'=>$param['appoint_date']]
)->get();
if($stopDiagnoseObj->isNotEmpty())
{
$this->writeLog('存在停诊数据生成预约数据失败:spaceId'.$param['space_id'].'时间'.$param['time'].'日期'.$param['appoint_date']);
}
else
{
return DiagnoseSchedule::create($param);
}

}
else
{
$diagneseSchedulObj = $diagneseSchedulObjs->first();
$diagneseSchedulObj->schdule_type = $param['schdule_type'];
$diagneseSchedulObj->save();
}
$this->writeLog('生成预约数据:'.json_encode($param),$logType);
return true;
}

/***
* @desc 根据选择时间段来生成
* 1,解析时间段生成上午下午的模版
* 2,解析时间段生成每天的模版
* 3,解析时间段,生成每周的模版
*
**/
public function changeTimeToSchdule($startTime, $endTime)
{
//开始时间大于当前时间
$startTimeObj = Carbon::parse($startTime);
$endTimeObj = Carbon::parse($endTime);
$int = (new Carbon)->diffInHours($endTimeObj, false);
$nowObj = Carbon::now();
if ($int < 0) {
$message = '结束时间:' . $endTime . '小于当前时间:' . $nowObj;
Log::info($message);
return array('errorCode' => 5000, 'errorMessage' => $message, 'data' => '');
}
//结束时间小于开始时间
if (false == $endTimeObj->gt($startTimeObj)) {
$message = '结束时间' . $endTime . '小于开始时间' . $startTime;
Log::info($message);
return array('errorCode' => 5001, 'errorMessage' => $message, 'data' => '');
}

//开始时间和结束时间是同一天
if ($endTimeObj->toDateString() == $startTimeObj->toDateString()) {
return array('errorCode' => 0, 'errorMessage' => 'success', 'data' => array($this->caculateSameDay($startTimeObj, $endTimeObj)));
} else {
return array('errorCode' => 0, 'errorMessage' => 'success', 'data' => $this->caculateDiffDay($startTimeObj, $endTimeObj));
}
}

//计算不在同一天的数据
private function caculateDiffDay($startTimeObj, $endTimeObj)
{
$dayInt = $startTimeObj->diffInDays($endTimeObj) + 1;
$returnData = array();
for ($i = 1; $i <= $dayInt; ++$i) {
$startTimeParam = $startTimeObj;
$endTimeparam = $endTimeObj;
if ($i == 1) {
$flagStart = $startTimeParam->toDateString();
$endTimeparam = Carbon::parse($flagStart . ' 22:00:00');
} else if ($i == $dayInt) {
$flagEnd = $endTimeObj->toDateString();
$startTimeParam = Carbon::parse($flagEnd . ' 08:00:00');
} else {
$flagObj = $startTimeParam->addDay(1);
$flagMid = $flagObj->toDateString();
$startTimeParam = Carbon::parse($flagMid . ' 08:00:00');
$endTimeparam = Carbon::parse($flagMid . ' 22:00:00');
}
$returnData[] = $this->caculateSameDay($startTimeParam, $endTimeparam);
}
return $returnData;
}

//计算同一天的数据
private function caculateSameDay($startTimeObj, $endTimeObj)
{
$startHour = $startTimeObj->hour;
$startMin = $startTimeObj->minute;
$endHour = $endTimeObj->hour;
$endMin = $endTimeObj->minute;
//开始时间大于8点,设置为8点
if ($startHour <= $this->startTime) {
$startHour = $this->startTime;
}
//结束时间小于22点设置 为22点
if ($endHour > $this->endTime) {
$endHour = $this->endTime;
}
$week = $endTimeObj->dayOfWeek;

if ($startMin > 30 && $startMin < 60) {
$startHour++;
}

$data = array();
$data2 = array();
$i=1;
for ($startHour; $startHour <= $endHour; ++$startHour) {
if ($startHour < 12) {
if($startHour<10)
{
$startHour = 0 . $startHour;
}
$data[$i][1] = $startHour . ':' . '00';
$data[$i][2] = $startHour . ':' . '30';
$data[$i][3] = 'morrning';
} elseif ($startHour >= 12 && $startHour < 18) {
$data[$i][1] = $startHour . ':' . '00';
$data[$i][2] = $startHour . ':' . '30';
$data[$i][3] = 'afternoon';
} else if ($startHour >= 18 && $startHour < 22) {
$data[$i][1] = $startHour . ':' . '00';
$data[$i][2] = $startHour . ':' . '30';
$data[$i][3] = 'evening';
}
$i++;
}
if(!empty($data))
{
if($endMin<30)
{
unset($data[$i-1][2]);
}
}
return array('week' => $week, 'date' => $endTimeObj->toDateString(), 'daySchdule' => $data);
}

/**
* @desc 根据判断时间段是否已经被预约
***/
public function checkIsAppointByDate($spaceId,$workUnitId,$date,$time=DiagnoseSchedule::DAYSCHDULE)
{
$isAppoint = DiagnoseSchedule::checkIsAppointByDate($spaceId,$workUnitId,$date,$time);
if($isAppoint->isEmpty())
{
return true;
}
else
{
return false;
}
}

/**
* @desc 根据日期判断是否已经被预约
***/
public function checkIsAppointByWeek($spaceId,$workUnitId,$week,$time)
{
$isAppoint = DiagnoseSchedule::checkIsAppointByWeek($spaceId,$workUnitId,$week,$time);
if($isAppoint->isEmpty())
{
return true;
}
else
{
return false;
}
}

/**
* @desc 生成90天预约数据
**/
public function createNinetySchdules($spaceId)
{
$spaceObj = Space::find($spaceId);
if(empty($spaceObj))
{
return false;
}
$modelDatas = ScheduleModel::where('space_id',$spaceId)
->where('is_stop_diagnose',ScheduleModel::STOPDIAGNOSENO)
->where('is_delete',ScheduleModel::DELETENO)
->get();
if($modelDatas->isEmpty())
{
return false;
}
foreach ($modelDatas as $modelData)
{
$param['space_id'] = $modelData->space_id;
$param['work_unit_id'] = $modelData->work_unit_id;
$param['appoint_date'] = '';
$param['user_id']=$spaceObj->user_id;
$param['work_unit_name']=$modelData->work_unit_name;
$param['day_schdule'] = $modelData->day_schdule;
$param['time'] = $modelData->time;
$param['week'] = $modelData->week;
$param['is_stop_diagnose'] =$modelData->is_stop_diagnose;
$param['is_delete'] =$modelData->is_delete;
$param['number'] =1;
$param['schdule_type'] = DiagnoseSchedule::CYCLE;
$param['model_id']=$modelData->id;
$param['date_time'] = '';
$res = $this->createOperateData($param);
}
return true;
}

/**
* @desc 生成schdule的数据
**/
private function createOperateData($param)
{
$dates = $this->conversionDateByWeek($param['week']);
foreach ($dates as $date)
{
$param['appoint_date'] = $date;
$param['date_time'] = $param['appoint_date'].' '.$param['time'];
$this->addScheduleData($param);
}
return true;
}

/**
* @desc 根据周几来判断生成的时间
**/
public function conversionDateByWeek($week)
{
$nowObj = Carbon::now();
$weekDay = $nowObj->dayOfWeek;
$dayInt = $week-$weekDay;
if( $dayInt!=0)
{
$weekDayCount = 12;
}
else
{
$weekDayCount = 13;
}
$dates = array();
for ($i=1;$i<=$weekDayCount;$i++)
{
$flagDayObj = Carbon::now()->addDay($dayInt);
if($dayInt>0 && $i==1 )
{
$dates[] = $flagDayObj->toDateString();
}
$flagFutureDay = $flagDayObj->addDay($i*7);
$dateString = $flagFutureDay->toDateString();
$dates[] = $dateString;
}
return $dates;
}

/**
* @desc 异步事件处理批量出停诊
**/
public function stopOrStartDiagnose($spaceId,$startTime,$endTime,$diagnoseType)
{
$datas = $this->changeTimeToSchdule($startTime, $endTime);
if($datas['errorCode']!=0)
{
return $datas;
}
foreach ($datas['data'] as $time)
{
$modeWhere['space_id'] = $spaceId;
$schduleWhere['space_id'] = $spaceId;
$date = $time['date'];
$timeLists = $time['daySchdule'];
foreach ($timeLists as $timeList)
{
$diagnoseSchduleObjs = DiagnoseSchedule::where('space_id',$spaceId)
->where('appoint_date',$date)
->whereIn('time',[$timeList[1],$timeList[2]])
->get();
foreach ($diagnoseSchduleObjs as $diagnoseSchduleObj)
{
if(empty($diagnoseSchduleObj))continue;
$diagnoseSchduleObj->is_stop_diagnose = $diagnoseType;
$diagnoseSchduleObj->save();
$this->writeLog('停诊数据:ID'.$diagnoseSchduleObj->id);
if($diagnoseSchduleObj->model_id != 0)
{
$modelObj = ScheduleModel::find($diagnoseSchduleObj->model_id);
$modelObj->is_stop_diagnose = $diagnoseType;
$modelObj->save();
$this->writeLog('停诊模版数据:模版ID'.$modelObj->id);
}
}
}
}
return array('errorCode' => 0, 'errorMessage' => 'success', 'data' =>[]);
}


/**
* @desc 批量停诊按照时间格式停诊
**/
public function stopOrStartDiagnoseBySchdule($spaceId,$startTime,$endTime,$timeSchdule,$diagnoseType)
{
$startDate = Carbon::parse($startTime);
$endDate = Carbon::parse($endTime);//->toDateString();
$int = $startDate->diffInDays($endDate,true);
$operateSchdules = $this->getTimeSchdules($timeSchdule);
for($i=0;$i<=$int;$i++)
{
$flag = Carbon::parse($startTime);
$flagDate = $flag->addDay($i);
$date = $flagDate->toDateString();
//批量出停诊
$this->addStopDiagnoseDatas($spaceId,$date,$operateSchdules,$diagnoseType);
$res = DiagnoseSchedule::batchStop($spaceId,$date,$operateSchdules,$diagnoseType);
if(false === $res)
{
Log::error('停诊数据失败,spaceId:'.$spaceId.'startTime'.$startTime.'endTime'.$endTime,$operateSchdules);
return false;
}
}
return true;
}

private function addStopDiagnoseDatas($spaceId,$date,$operateSchdules,$diagnoseType)
{
if(empty($spaceId) || empty($date) || empty($operateSchdules))
{
return false;
}
foreach ($operateSchdules as $operateSchdule)
{
$param['space_id'] = $spaceId;
$param['time'] = $operateSchdule;
$param['appoint_date'] = $date;
if($diagnoseType == 1)
{
StopDiagnose::firstOrCreate($param,$param);
}
else
{
StopDiagnose::where($param)->delete();
}

}
return true;
}

private function getTimeSchdules($timeSchdules)
{
if(empty($timeSchdules))
{
return array_merge(ScheduleModel::MORRNINTSCHDULE,ScheduleModel::AFTRENOONSCHDULE,ScheduleModel::EVENINGSCHDULE);
}
$returnData = array();
foreach ($timeSchdules as $timeSchdule)
{
$returnData = array_merge($returnData,ScheduleModel::DAYSCHEULD[$timeSchdule]);
}
return $returnData;
}

/**
* @desc 停诊时判断是否存已经预约的数据
* @return true 可以停诊 false 不可以停诊
***/
public function checkBatchStopDiagnose($spaceId,$startTime,$endTime,$timeSchdule)
{
$startDate = Carbon::parse($startTime);
$endDate = Carbon::parse($endTime);//->toDateString();
$int = $startDate->diffInDays($endDate,true);
$operateSchdules = $this->getTimeSchdules($timeSchdule);
for($i=0;$i<$int;$i++)
{
$flag = Carbon::parse($startTime);
$flagDate = $flag->addDay($i);
$date = $flagDate->toDateString();

//批量出停诊
$res = DiagnoseSchedule::checkBatchStop($spaceId,$date,$operateSchdules);

if(false == $res)
{
return false;
}
}
return true;
}

/**
* @desc 停诊时判断是否存已经预约的数据
* @return true 可以停诊 false 不可以停诊
***/
public function checkStopDiagnose($spaceId,$startTime,$endTime)
{
$checkCanStop = DiagnoseSchedule::checkOneIsAppoint($spaceId,$startTime,$endTime);
if($checkCanStop->isNotEmpty())
{
return false;
}
else
{
return true;
}
}

/***
* @desc 删除
**/
public function deleteDiagnose($diagnoseSchduleId)
{
$scheduleObj = DiagnoseSchedule::find($diagnoseSchduleId);
if(empty($scheduleObj))
{
return false;
}
if($scheduleObj->schdule_type == DiagnoseSchedule::INTERIM)
{
$scheduleObj->is_delete = DiagnoseSchedule::DELETEYES;
$scheduleObj->save();
return true;
}
else
{
$schduleObjs = DiagnoseSchedule::where('space_id',$scheduleObj->space_id)
->where('work_unit_id',$scheduleObj->work_unit_id)
->where('week',$scheduleObj->week)
->where('time',$scheduleObj->time)
->get();
if($schduleObjs->isEmpty())
{
return false;
}
$modelId = 0;
foreach ($schduleObjs as $schduleObj)
{
$schduleObj->is_delete = DiagnoseSchedule::DELETEYES;
$schduleObj->save();
$modelId = $schduleObj->model_id;
$this->writeLog('删除数据:ID'.$schduleObj->id);
}
$modelObj = ScheduleModel::find($modelId);
if(!empty($modelObj))
{
$modelObj->is_delete = ScheduleModel::DELETEYES;
$modelObj->save();
$this->writeLog('删除模版数据:模版ID'.$modelObj->id);
}
return true;
}
}

/**
* @desc 判断是否可以删除
* @param $diagnoseSchduleId
* @return true 可以删除 false 不可以删除
**/
public function checkIsDelete($diagnoseSchduleId)
{
$scheduleObj = DiagnoseSchedule::find($diagnoseSchduleId);
if(empty($scheduleObj))
{
return false;
}
if($scheduleObj->schdule_type == DiagnoseSchedule::INTERIM)
{
if($scheduleObj->number < 1 )
{
return false;
}
else
{
return true;
}
}
else
{
$schduleObjs = DiagnoseSchedule::where('space_id',$scheduleObj->space_id)
->where('work_unit_id',$scheduleObj->work_unit_id)
->where('week',$scheduleObj->week)
->where('time',$scheduleObj->time)
->where('number', '<', 1)
->get();
if($schduleObjs->isEmpty())
{
return true;
}
else
{
return false;
}
}
}


}

10、总结

这是市面上最简单的一个预约版本,简单如商品上下架。
使用的算法:枚举算法,

优点:医生库存可控,方便时间管理,

缺点:先生成数据集,修改删除都是批量处理,可塑性比较低

二、升级版

所谓的升级版就是加各种干扰因子。

后台生成可约时间,依赖项目时长,机构营业时间,机构班次,项目麻醉时间

用户预约时,除了选择医生、时间外,还要选择项目;通过选择的项目,来获取对应的时间号源

1、香蜜丽格预约香蜜

​ 根据机构的营业时间+机构下项目时长+项目是否麻醉等约束条件,生成未来固定一段时间的医生相关联的每个项目可约模板。每天按照模板生成 医生+项目时长+项目麻醉类型+可约时间段+日期+时间 的可约数据

2、设计实现

  • 后台生成医生的每个项目的出诊时间模板
  • 依赖时间模板定时生成未来给定天预约数据
  • 前端根据生成的预约数据展示给用户可约数据

3、技术原理

​ 生成器模式+试探算法

4、干扰因子

  • 机构
  • 项目治疗时长
  • 项目麻醉时长
  • 机构排班
  • 可约周期
  • 出诊类型
  • 同一时间点可约个数

5、设计图

image-20210602164750754

7、后台界面

image-20210602165259216

8、前端预约页面

image-20210602165229119

9、总结

有点:一次性生成医生项目可约数据,方便查看

缺点:约束性条件多生成预约数据出错概率大,约束条件变更需要批量修改已经生成的可约数据,比较局限

三、铂金版

1、背景

共享医院,有手术,光电,注射,口腔 四种类型的手术室,每种类型手术室有多个,

相关项目手术室 有关键设备影响,比如M22 ,黄金微针 ,舒敏之星, 当前时间是否被占用,如果占用手术室不可约

相关项目手术室 人员占用情况影响 比如注射配台护士,光电配台护士 当前时间是否空闲,如果没有空闲,手术不可约

手术室预约要求至少30分钟,

会议室重叠算法

2、技术实现原理

(1)根据手术时长(比如1小时) 把一天在班时间[8:00-20:00]切割成小时间段比如[8:00-9:00]….[19:00-20:00]

(2)获取每个手术室每天可约的时间间隔(枚举算法)

(3)循环(1)每天可约时间和(2)中每个手术室可约时间做比较取出可约手术室

(4)可约器材和可约人员 循环(2)(3)步骤

(5)生成可约的时间和日期列表

3、算法

​ 工厂模型 + 贪心算法

4、设计图

5、代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

class EyRoomUsage extends Model
{
protected $table = "ey_room_usages";
protected $guarded = [];

const WEEK_MON = 1;//周一
const WEEK_TUES = 2;//周二
const WEEK_WED = 3;//周三
const WEEK_THUR = 4;//周四
const WEEK_FRI = 5;//周五
const WEEK_SAT = 6;//周六
const WEEK_SUN = 0;//周日

const DAYSCHDULE = [
'9:00', '9:30', '10:00', '10:30', '11:00', '11:30', '12:00', '12:30',
'13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30',
'17:00', '17:30', '18:00', '18:30', '19:00', '19:30', '20:00',
];
const SCHDULE_NAME_ARR = [
'morrning' => '上午',
'afternoon' => '下午',
'evening' => '晚上'
];
const WEEK_NAME_ARR = [
self::WEEK_MON => '星期一',
self::WEEK_TUES => '星期二',
self::WEEK_WED => '星期三',
self::WEEK_THUR => '星期四',
self::WEEK_FRI => '星期五',
self::WEEK_SAT => '星期六',
self::WEEK_SUN => '星期日',

];

const SOURCE_TYPE_ROOM = 'room';//类型为手术室
const SOURCE_TYPE_RESOURCE = 'resource';//类型为资源

const IS_DELETE_YES = 1;//删除
const IS_DELETE_NO = 0;//正常
//手术室间数
public static $anaesthesiaRoomNum = [
EyAppointmentSurgery::ANAESTHESIA_GENERAL => 6,//全麻
EyAppointmentSurgery::ANAESTHESIA_LOCATION => 8,//局麻
];


public static function createEyRoomUsage($data)
{
if (empty($data['appoint_date']) || empty($data['source_id'])) {
return false;
}
$obj = new self();
isset($data['source_type']) ? $obj->source_type = $data['source_type'] : '';
isset($data['source_id']) ? $obj->source_id = $data['source_id'] : '';
isset($data['duration']) ? $obj->duration = $data['duration'] : '';
isset($data['appoint_date']) ? $obj->appoint_date = $data['appoint_date'] : '';
isset($data['start_time']) ? $obj->start_time = $data['start_time'] : '';
isset($data['end_time']) ? $obj->end_time = $data['end_time'] : '';
isset($data['sort_room_num']) ? $obj->sort_room_num = $data['sort_room_num'] : '';
$obj->is_delete = self::IS_DELETE_NO;
$obj->save();
return $obj;

}

public static function cancelEyRoom($id)
{
$obj = self::find($id);
if (false == $obj instanceof self) {
return false;
}
$obj->is_delete = self::IS_DELETE_YES;
$obj->save();
return true;
}

//创建预约时检测当前时间是否可约
public static function checkCanCreateRoomUsage($timeList, $date, $sourceId, $sourceType = self::SOURCE_TYPE_ROOM)
{
if (empty($timeList) || empty($date) || empty($sourceId)) {
return ['errCode' => -1, 'errMsg' => '参数非法', 'roomId' => 0];
}
$dataListObj = self::where('source_type', $sourceType)
->where('source_id', $sourceId)
->where('appoint_date', '=', $date)
->where("is_delete", self::IS_DELETE_NO)
->get();
$res = $dataListObj->groupBy("sort_room_num");
$eyOperRoomPoolObj = EyOperationRoomPool::find($sourceId);
$souceNum = $eyOperRoomPoolObj->room_num;
$hasAppoitRoomPools = self::getPoolAppointData($res->toArray(), $souceNum);
if (empty($hasAppoitRoomPools)) {
$hasAppoitRoomPools = [1 => self::DAYSCHDULE];
}
[$checkappoint, $roomId] = self::timeFactory($hasAppoitRoomPools, $timeList);
if (false == $checkappoint) {
return ['errCode' => -1, 'errMsg' => '时间不可约', 'roomId' => 0];
} else {
return ['errCode' => 0, 'errMsg' => '', 'roomId' => $roomId];
}
}


/**
* @desc 获取预约列表
* $timeLong 时间*2
* $startTime 日期
* @param
***/
public static function getScheduleList($timeLong, $startTime, $endTime, $surgeryType, $poolId, $anaesthesiaType)
{
$timeLists = self::getScheduleModelTitle($startTime, $endTime);
$hasAppointDateList = self::getRoomUsageList($startTime, $endTime, $surgeryType, $poolId, self::SOURCE_TYPE_ROOM, $anaesthesiaType);
foreach ($timeLists as $key => $timeList) {
if (isset($hasAppointDateList[$key])) {
$timeLists[$key]['timeSchdule'] = self::timeFactoryList($hasAppointDateList[$key], $timeLong, $surgeryType);
} else {
$timeLists[$key]['timeSchdule'] = self::timeFactoryList([1 => self::DAYSCHDULE], $timeLong, $surgeryType);//如果手术室或者资源没有被约的默认取第一个
}
}
return $timeLists;
}

/**
* @desc 判断是否忙碌可约
* $currentRoomNum 手术室剩余可约间数
* $surgeryType 手术室类型
* 剩余可约手术室间数 >=(手术室总间数)*0.3 空闲可约
* 否则 忙碌可约
**/
public static function isBusyAppoint($currentRoomNum, $surgeryType)
{
if ($currentRoomNum <= 0) {
return false;
}
$obj = EyOperationRoomPool::where('room_type', $surgeryType)->first();
if (false == $obj instanceof EyOperationRoomPool) {
return false;
}
$threshold = round($obj->room_num * 0.3);
if ($currentRoomNum >$threshold) {
return true;
} else {
return false;
}
}

//生成每天可以预约的数据
public static function timeFactoryList($timeLists, $timeLog, $surgeryType = '')
{
$return = [];
if ($timeLog < 1) {
return [];
}
$listDatas = self::DAYSCHDULE;
$canAppointDate = [];
foreach ($listDatas as $key => $listData) {
$flag = [];
for ($i = $key; $i <= $key + $timeLog; $i++) {
if (isset($listDatas[$i])) {
$flag[] = isset($listDatas[$i]) ? $listDatas[$i] : '22:00';
}
}
if (!empty($flag) && 1 != count($flag)) {
[$checkappoint, $roomId, $busystatus] = self::timeFactory($timeLists, $flag, $surgeryType);
if (true == $checkappoint) {
$oktime = [];
$oktime['time'] = $flag;
$oktime['status'] = $busystatus;
$canAppointDate[] = $oktime;
}
}
}
$res = self::addTimeNode($canAppointDate);
return $res;;
}

//生成上下午时间节点
public static function addTimeNode($timeLists)
{
if (empty($timeLists)) {
return [
'morrning' => [],
'afternoon' => []
];
}
$morrn = [];
$afternoon = [];
foreach ($timeLists as $data) {
$timeList = $data['time'];
$startTime = current($timeList);
$nodeKey = array_search('13:00', self::DAYSCHDULE);;
$startTimeTimeKey = array_search($startTime, self::DAYSCHDULE);
if ($startTimeTimeKey < $nodeKey) {
$morrn[] = $data;
} else {
$afternoon[] = $data;
}
}
return [
'morrning' => $morrn,
'afternoon' => $afternoon
];
}


public static function timeFactory($timeLists, $toConfirmData, $surgeryType = '')
{
$count = count($toConfirmData);
sort($toConfirmData);
$checkedRoomId = 0;
$canCheckNum = 0;
foreach ($timeLists as $roomId => $timeList) {
if (count($timeList) != count($timeList, 1))//判断是二维数组
{
foreach ($timeList as $list) {
$res = array_diff($list, $toConfirmData);
$res2 = array_diff($toConfirmData, $list);

if (!empty($res) && $res != $list && count($list) > $count && empty($res2)) {
if (0 == $checkedRoomId) {
$checkedRoomId = $roomId;
}
$canCheckNum++;
//return [true, $roomId];
}
sort($list);
if ($list == $toConfirmData) {
if (0 == $checkedRoomId) {
$checkedRoomId = $roomId;
}
$canCheckNum++;
// return [true, $roomId];
}
}
} else {
$res = array_diff($timeList, $toConfirmData);
$res2 = array_diff($toConfirmData, $timeList);
if (!empty($res) && $res != $timeList && count($timeList) > $count && empty($res2)) {
return [true, $roomId, true];
}
sort($timeList);
if ($timeList == $toConfirmData) {
return [true, $roomId, true];
}
}
}

if (0 != $checkedRoomId && $canCheckNum > 0) {
$isBusy = self::isBusyAppoint($canCheckNum, $surgeryType);
return [true, $checkedRoomId, $isBusy];
} else {
return [false, 0, false];
}
}

//获取可以预约的列表
public static function getRoomUsageList($startDate, $endDate, $surgeryType, $sourceId, $sourceType = self::SOURCE_TYPE_ROOM, $anaesthesiaType = '')
{
if ($sourceType == self::SOURCE_TYPE_ROOM) {
$eyOperRoomPoolObj = EyOperationRoomPool::find($sourceId);
if (empty($anaesthesiaType) || EyAppointmentSurgery::SURGE_TYPE_SS != $surgeryType) {
$souceNum = $eyOperRoomPoolObj->room_num;
} else {
if ($anaesthesiaType == EyAppointmentSurgery::ANAESTHESIA_GENERAL) {
$souceNum = self::$anaesthesiaRoomNum[$anaesthesiaType];
} else {
$souceNum = $eyOperRoomPoolObj->room_num;
}
}

} else {
//TODO 获取资源数量
$souceNum = 0;
}
$dataLists = self::where('source_type', $sourceType)
->where('source_id', $sourceId)
->where('appoint_date', '>=', $startDate)
->where('appoint_date', '<=', $endDate)
->where("is_delete", self::IS_DELETE_NO)
->get();
$dataObjs = $dataLists->groupBy('appoint_date');
$appointRoomList = [];
if (!empty($dataObjs)) {
foreach ($dataObjs as $key => $dataObj) {
$res = $dataObj->groupBy("sort_room_num");
$appointRoomList[$key] = self::getPoolAppointData($res->toArray(), $souceNum);
}
}
return $appointRoomList;
}

//生成每个手术室每天可预约的时间点
public static function getPoolAppointData($datas, $souceNum)
{
if ($souceNum < 0) {
return [];
}
$return = [];
for ($i = 1; $i <= $souceNum; $i++) {
if (isset($datas[$i])) {
$return[$i] = self::getRoomAppoint($datas[$i]);
} else {
$return[$i][] = self::DAYSCHDULE;
}
}
return $return;

}

public static function getRoomAppoint($datas)
{
$timeListKeys = array_flip(self::DAYSCHDULE);
$timeListDatas = self::DAYSCHDULE;
$flagArr = [];
foreach ($datas as $data) {
$startTime = $data['start_time'];
$endTime = $data['end_time'];
if (empty($flagArr)) {
if (array_key_exists($startTime, $timeListKeys) && array_key_exists($endTime, $timeListKeys)) {
$flagArr = self::splitArrByKey($startTime, $endTime, $timeListDatas);
}
} else {
$flagArr = self::splitArrByKeyPlus($startTime, $endTime, $flagArr);
}
}
return $flagArr;
}

public static function splitArrByKeyPlus($startTime, $endTime, $ruleArrs)
{
$return = $ruleArrs;

foreach ($ruleArrs as $key => $ruleArr) {
if (in_array($startTime, $ruleArr) && in_array($endTime, $ruleArr)) {
$flagArr = self::splitArrByKey($startTime, $endTime, $ruleArr);
if (!empty($flagArr)) {
unset($return[$key]);
$return = array_merge($return, $flagArr);
}
}
}
return $return;
}

//拆分数组
public static function splitArrByKey($startTime, $endtime, $ruleArrs)
{
$startKey = array_search($startTime, $ruleArrs);
$endKey = array_search($endtime, $ruleArrs);
$startArr = array_slice($ruleArrs, 0, $startKey + 1);
$endArr = array_slice($ruleArrs, $endKey);
$return = [];
if (!empty($startArr) && 1 != count($startArr)) {
$return[] = $startArr;
}
if (!empty($endArr) && 1 != count($endArr)) {
$return[] = $endArr;
}
return $return;
}

public static function getScheduleModelTitle($startTime, $endTime)
{
$date = new Carbon($startTime);
$count = $date->diffInDays($endTime);
$i = 0;
$return = [
$date->toDateString() => [
'date' => $date->toDateString(),
'weekDay' => $date->dayOfWeek,
'week' => EyRoomUsage::WEEK_NAME_ARR[$date->dayOfWeek]
]
];
for ($i = 0; $i < $count; $i++) {
$flagDate = $date->addDay(1);
$return[$flagDate->toDateString()] = [
'date' => $flagDate->toDateString(),
'weekDay' => $flagDate->dayOfWeek,
'week' => EyRoomUsage::WEEK_NAME_ARR[$flagDate->dayOfWeek]
];
}
return $return;
}

//判断领建同步过来的预约是否消耗手术室预约
public static function checkLJAppointTime($startDateTime, $endDateTime)
{
$startCarbonObj = Carbon::parse($startDateTime);
$endCarbonObj = Carbon::parse($endDateTime);
$int = $startCarbonObj->diffInMinutes($endCarbonObj);
//可约时长最长是8小时
if ($int > 480) {
return [];
}
$list = [];
while ($startCarbonObj->lte($endCarbonObj)) {
$list[] = $startCarbonObj->hour . ':' . (0 == $startCarbonObj->minute ? '00' : $startCarbonObj->minute);
$startCarbonObj->addMinute(30);
}
return ['timeList' => $list, 'timeduration' => $int / 60, 'date' => $startCarbonObj->toDateString()];
}


}

6、前端页面图

image-20210603161949165

image-20210603162003366

7、总结

优点:只是依赖规则,依赖因子都是可插拔模式,扩展性比较好,可塑性高

确定:每次获取批量可约数据计算量大,枚举法比较暴力时间复杂度高(网上有会议室重叠算法和堆算法)