背景
今天我們來聊一個關於「車輛預定系統」的話題,話題來源於社群一位小夥伴的提問(原文請戳這裡)。
這個場景是這樣的:
現在需要開發一套車輛租賃系統,已知每輛入駐系統的汽車都會維護基本資訊。其中有一個屬性為汽車的「可租賃時間段」屬性。什麼意思呢?比如一些租金相對便宜的車,全年都對外租賃。而一些租金相對較高的車,僅在節假日或者週末租賃。根據租賃時間段不同共分為以下幾種情況:
A. 全年都可租賃
B. 僅週六週日可租賃
C. 僅週六可租賃
D. 僅週日可租賃
E. 週六週日和節假日可租賃
當汽車在某個時間段被租賃以後,便不可被租賃,但是其他時間段是可被租賃的。現在的需求是,當使用者輸入日期區間以後,需要能夠顯示當前可以租賃的車輛資訊,並且可以根據汽車的基本資訊進行檢索。
原文字來是一篇提問貼,但是總感覺三言兩語也說不清楚。而且類似的場景還有很多,比如:會議室預定系統等等,其原理都是大致相同的。所以我們索性來個Reset
,乾脆當成一道考試題,來看看應該如何解決。
話不多說,讓我們一起走進今天的「錦囊之旅」吧~
原型分析
首先我們需要確認一下有哪些模型。這裡我們列舉用到的幾個模型以及一些基本的欄位屬性。
車輛模型 (Car)
「車輛模型」肯定是必不可少的。其中包含以下幾個主要的屬性:
欄位 | 描述 |
---|---|
car_id | 車輛唯一 ID |
lease_type | 1. 全年都可租賃 2. 僅週六週日可租賃 3. 僅週六可租賃 4. 僅週日可租賃 5. 週六週日和節假日可租賃 |
租賃人模型(Leaseholder)
「租賃人模型」即租借人員的基本資訊。其中包含以下幾個主要屬性:
欄位 | 描述 |
---|---|
leaseholder_id | 租賃人唯一 ID |
phone | 租賃人手機號 |
租賃記錄模型(Lease Record)
「租賃記錄模型」即租賃人和車輛的關係模型。其中包含以下幾個主要屬性:
欄位 | 描述 |
---|---|
record_id | 租賃記錄唯一 ID |
car_id | 車輛唯一 ID |
leaseholder_id | 租賃人唯一 ID |
lease_start_date | 租賃起始日期 |
lease_end_date | 租賃截止日期 |
假設現在有三輛車A,B 和 C 。A 全年可租賃,B 僅週六可租賃,C 週六週日和節假日可租賃。我們用下面一張日程圖來表示三輛車的租賃記錄:
現在我想查一下 10 月 4 號到 10 月 8 號之間有哪些可以租借的車輛。
從圖上我們不難發現,這個時間段內只有 A 車輛滿足條件。現在就讓我們看看如何在程式中實現吧。Let’s go~
方案設計
常規設計方案
其實,在這個場景中,資料模型並不複雜,資料儲存邏輯也不復雜。錄入車輛資訊,錄入租賃人資訊,當某輛車在某個時間段被租賃時,記錄下租賃關係。看上去好像也沒什麼複雜的呢?
Really ???
想象一下,現在如果想查詢某段時間內可以租賃的車輛應該怎麼查詢呢?
或許,我們需要透過以下幾個步驟進行操作:
- 查詢出該時間段內所有符合租賃條件的車輛資訊 A
- 查詢出該時間段內已經存在租賃記錄的資訊 B
- 則該時間段內可租賃的車輛為:A - B = { x| x∈A 且 x∉B }
讓我們分別來看看這三步如何實現。
步驟一
第一步是查詢出該時間段內所有符合租賃條件的車輛資訊。
首先,我們需要將給定的時間範圍進行拆分。在拆分之前,我們先來定義一些變數。
- t1:選定起始日期
- t2:選定截止日期
- lease_type: 租賃型別,型別定義同模型定義
這裡我們需要根據車輛租賃型別和選中的起止日期來進行篩選。為方便理解,我們透過表格的形式進行描述。
集合 | 條件 |
---|---|
A1 | lease_type = 1 |
A2 | 條件1:lease_type = 2 條件2:(t1 = t2 且 (t1 = 週六 或者 t1 = 週日))或者(t2 - t1 = 1 且 t1 = 週六) |
A3 | 條件1:lease_type = 3 條件2:t1 = t2 且 t1 = 週六 |
A4 | 條件1:lease_type = 4 條件2:t1 = t2 且 t1 = 週日 |
A5 | 條件1: lease_type = 5 條件2:t1 到 t2之間的每一天都是週六週日或者節假日 |
上面表格中,A1 ~ A5 表示可預定車輛的不同集合,如果用集合 A 表示所有可預定車輛的話, 則 A = A1 ∪ A2 ∪ A3 ∪ A4 ∪ A5 。
看到這些數學公式是不是感覺有點眼花撩亂。實際上,只有當你把問題抽象成最基本的數學模型的時候,你才不會陷入錯綜複雜的if else
迴圈中。
從表格中不難看出,每一組條件都是由lease_type
和起止時間兩個條件決定(A1 除外)。這裡lease_type
是表中的欄位屬性條件,而第二個條件可以透過程式碼進行判斷,程式碼示例如下:
車輛預定類 CarLease.php
class CarLease
{
/**
* @var string[] 節假日
*/
protected $holidays = [
'2022-12-31',
'2023-01-01',
...
'2023-10-01',
'2023-10-02',
'2023-10-03',
'2023-10-04',
'2023-10-05',
'2023-10-06',
];
/**
* 判斷是否週六
*
* @param string $date 日期
* @return bool
*/
public function checkSaturday(string $date): bool
{
return date('w', strtotime($date)) == 5;
}
/**
* 判斷是否週日
*
* @param string $date 日期
* @return bool
*/
public function checkSunday(string $date): bool
{
return date('w', strtotime($date)) == 6;
}
/**
* 判斷是否週末
*
* @param string $date 日期
* @return bool
*/
public function checkWeekend(string $date): bool
{
return in_array(date('w', strtotime($date)), [5, 6]);
}
/**
* 判斷是否節假日
*
* @param string $date 日期
* @return bool
*/
public function checkHoliday(string $date): bool
{
return in_array($date, $this->holidays);
}
/**
* 批次檢查是否週末或者假期
*
* @param string $startDate 起始日期
* @param string $endDate 截止日期
* @return bool
*/
public function batchCheckWeekendOrHoliday(string $startDate, string $endDate): bool
{
$i = $startDate;
while($i <= $endDate){
if(!$this->checkWeekend($i) && !$this->checkHoliday($i)){
return false;
}
$i = date('Y-m-d', strtotime("{$i} +1 day"));
}
return true;
}
}
汽車模型Car.php
結構如下
Car.php
class Car extends Model
{
const LEASE_TYPE_EVERY_DAY = 1; //全年都可租賃
const LEASE_TYPE_ONLY_WEEKEND = 2; //僅週六週日可租賃
const LEASE_TYPE_ONLY_SATURDAY = 3; //僅週六可租賃
const LEASE_TYPE_ONLY_SUNDAY = 4; //僅週日可租賃
const LEASE_TYPE_WEEKEND_OR_HOLIDAY = 5; //週六週日和節假日可租賃
}
以下是呼叫邏輯:
//初始化起止日期
$startDate = '2023-10-04';
$endDate = '2023-10-08';
//計算起止日期差值
$diffDays = date_diff(date_create($startDate), date_create($endDate))->days;
//例項化車輛預定類
$carLease = new CarLease();
$query = Car::query();
//A1
//條件1:lease_type = 1
$query = $query->where('lease_type', Car::LEASE_TYPE_EVERY_DAY);
//A2
//條件1:lease_type = 2
//條件2:(t1 = t2 且 (t1 = 週六 或者 t1 = 週日))或者(t2 - t1 = 1 且 t1 = 週六)
if($diffDays == 0 && ($carLease->checkWeekend($startDate)) || $diffDays == 1 && $carLease->checkSaturday($startDate)){
$query = $query->orWhere('lease_type', Car::LEASE_TYPE_ONLY_WEEKEND);
}
//A3
//條件1:lease_type = 3
//條件2:t1 = t2 且 t1 = 週六
if($diffDays == 0 && $carLease->checkSaturday($startDate)){
$query = $query->orWhere('lease_type', Car::LEASE_TYPE_ONLY_SATURDAY);
}
//A4
//條件1:lease_type = 4
//條件2:t1 = t2 且 t1 = 週日
if($diffDays == 0 && $carLease->checkSunday($startDate)){
$query = $query->orWhere('lease_type', Car::LEASE_TYPE_ONLY_SUNDAY);
}
//A5
//條件1: lease_type = 5
//條件2:t1 到 t2 之間的每一天都是週六週日或者節假日
if($carLease->batchCheckWeekendOrHoliday($startDate, $endDate)){
$query = $query->orWhere('lease_type', Car::LEASE_TYPE_WEEKEND_OR_HOLIDAY);
}
//查詢所有滿足條件車輛
$cars = $query->get();
至此,我們就完成了根據日期條件篩選所有滿足條件的車輛的操作了。
接下來,我們需要查詢出所有已經被預定的車輛資訊。
步驟二
在這一步中,我們需要查詢出所有「在指定日期範圍內有預定記錄」的車輛,並進行去重。
我們先來看一個集合示意圖:
這裡我們用以下變數表示上圖各臨界點:
- t:時間軸
- t1:指定起始日期
- t2:指定截止日期
- t′:動態截止日期
- t″:動態起始日期
使用 A1 表示指定開始日期和動態截止日期之間的元素的集合,則用集合描述法表示為:{ x ∈ A1 | t1 ≤ x ≤ t′ }。
使用 A2 表示動態起始日期和指定截止日期之間的元素的集合,則用集合描述法表示為:{ x ∈ A2 | t″ ≤ x ≤ t2 }。
所以,這裡我們所求的「在指定日期範圍內有預定記錄」的車輛,就是 A1 和 A2 的交集,即 A1 ∩ A2。用動態的角度來看的話,就是當 t′ 向右運動,而 t″ 向左運動,形成的相交區域。
程式碼實現如下:
$leasedCars = LeaseRecord::where('lease_start_date', '<=', $endDate)
->where('lease_end_date', '>=', $startDate)
->get();
這樣,我們就統計出了所有「在指定日期範圍內有預定記錄」的車輛資訊。
步驟三
因為我們的訴求是按照指定的日期範圍預定車輛,所以需要保證範圍內的每一天車輛都是可預定的。因此,這裡需要取步驟一和步驟三兩個結果的差集。
程式碼邏輯如下:
$cars = $query->whereNotExists(function ($query) use ($startDate, $endDate) {
$query->select(DB::raw(1))
->from('lease_records')
->where('lease_start_date', '<=', $endDate)
->where('lease_end_date', '>=', $startDate)
->whereColumn('lease_records.car_id', 'cars.car_id');
})->get();
這裡我們使用了
not exists
語法排除了存在預定記錄的車輛。這樣,我們仍然可以在外層結構上使用翻頁的邏輯。
總結
在本篇文章中,我們解決了一個「車輛預定」的問題。
這是一個很有意思的話題,雖然實現起來難度並不大,但是蘊涵了許多數學的思想在裡面,特別是集合處理的思想。
當然,本篇文章僅限於從基本功能進行實現,並未考慮資料量帶來的演算法複雜度問題。在《工作小錦囊系列——如何實現一個車輛預定功能(下)》 一文中,我們將來討論如何使用 Redis
的 Bitmap
來解決此類的問題。
感謝大家的持續關注~
本作品採用《CC 協議》,轉載必須註明作者和本文連結