第四階段從補齊表單驗證細節,走到生命週期掛載點,再到 Day 15:接上 Laravel 既有的 Policy,誰能看見這個操作按鈕 接上的 Policy 機制,權限判斷的地基算是打好了。
但那套機制裡的判斷邏輯,說穿了只是 is_admin 這一個欄位的二分結果,有管理身分就放行,沒有就全部擋下。這間批發商實際的團隊結構顯然不是這麼一回事。
倉管人員每天盯的是商品跟庫存數量對不對,訂單明細裡多寫一件或少算一件,直接影響隔天出貨。理應能編輯商品資料,也理應能新增或刪除訂單明細,因為明細一動,庫存跟著連動,這條線 Day 14:儲存前後可以掛什麼,庫存異動的自動計算 已經接好了。
業務人員在意的是客戶關係跟訂單能不能順利成立。理應能建立與編輯客戶、訂單,但商品的成本、庫存這類數字,不屬於業務的職責範圍,不該讓業務隨手調整,調錯一個成本欄位,影響的可能是整間公司的毛利計算。
財務人員關心的是訂單金額跟客戶往來紀錄是否正確,這份工作性質偏向覆核,理應能檢視訂單細節,卻未必需要新增或編輯訂單本身,那是業務端在跑的流程,財務只需要確認數字對得上。
三種角色,三種完全不同的關注點,如果沿用 Day 15 那個 is_admin 判斷,這三個人只會分成「有管理身分」跟「沒有」兩種待遇,倉管跟業務如果都被設成管理者,會同時看到一模一樣的操作按鈕,這顯然不貼近真實團隊的運作方式。今天的任務不是重新設計 Policy 機制,而是把這三個角色的職責差異,轉換成一張角色對應模組操作的具體矩陣,實作進 Day 15 已經接上的 Policy 類別裡。
矩陣有兩個維度。一個維度是角色,倉管、業務、財務;另一個維度是模組與操作的組合,商品的檢視、編輯,訂單的檢視、建立、編輯,訂單明細的新增、刪除。兩個維度交叉出來的每一格,才是真正要回答的問題,這個角色對這個模組的這個操作,能不能做。
矩陣可以想成一張座位表,角色是縱軸,模組操作是橫軸,每一格才是答案,不能一次把整行或整列填完就交卷,得一格一格想清楚。依前面對三個角色職責的描述,這張批發商的角色權限矩陣大致長這樣:
| 角色 | 商品檢視 | 商品編輯 | 訂單檢視 | 訂單建立 | 訂單編輯 | 訂單明細新增 | 訂單明細刪除 |
|---|---|---|---|---|---|---|---|
| 倉管 | 可以 | 可以 | 可以 | 不行 | 不行 | 可以 | 可以 |
| 業務 | 可以 | 不行 | 可以 | 可以 | 可以 | 不行 | 不行 |
| 財務 | 可以 | 不行 | 可以 | 不行 | 不行 | 不行 | 不行 |
這張表最容易看出「矩陣」這個說法的由來。倉管在商品跟訂單明細上權限很寬,換到訂單本身卻完全不能建立或編輯,同一個角色在不同模組上的權限程度並不一致。反過來看訂單檢視這一欄,三個角色全部都是可以,換到訂單編輯這一欄,只剩業務一個人過關,同一個模組操作,換了角色答案也不一樣。
畫這張表時最容易踩的陷阱,是偷懶把某個角色設計成整行全有或整行全無,反正這個人是管理者就全部放行,反正那個人只是外部窗口就全部擋下。這種懶惰的二分,跟 Day 15 那個 is_admin 判斷本質上是同一個問題,只是換了個名字。畫矩陣真正的目的,正是逼自己把每個角色對每個模組操作都認真想一次,想清楚了才落筆填格子,而不是先看系統有哪些功能,再隨手分配。
矩陣畫好,接下來要做的事情很單純,把這張表翻譯成程式碼,寫進 Day 15 已經存在的 Policy 類別裡。原本 OrderPolicy 裡的判斷邏輯是這樣的一個布林值:
public function update(User $user, Order $order): bool
{
return $user->is_admin;
}
現在要換掉的不是判斷的位置,而是判斷的依據。假設角色資訊存在使用者資料表的 role 欄位,值域是 warehouse、sales、finance 這三種,先從商品 Policy 開始動手,對照矩陣裡商品編輯那一欄,只有倉管可以:
namespace App\Policies;
use App\Models\Product;
use App\Models\User;
class ProductPolicy
{
public function update(User $user, Product $product): bool
{
return $user->role === 'warehouse';
}
}
update 方法不再是單一個布林值的簡單開關,而是依角色分別判斷。接著處理訂單明細 Policy,對照矩陣裡新增跟刪除這兩欄,同樣只有倉管放行,其他角色一律擋下:
namespace App\Policies;
use App\Models\OrderItem;
use App\Models\User;
class OrderItemPolicy
{
public function create(User $user): bool
{
return $user->role === 'warehouse';
}
public function delete(User $user, OrderItem $orderItem): bool
{
return $user->role === 'warehouse';
}
}
訂單 Policy 則要對照矩陣裡建立跟編輯這兩欄,改成只有業務放行:
namespace App\Policies;
use App\Models\Order;
use App\Models\User;
class OrderPolicy
{
public function create(User $user): bool
{
return $user->role === 'sales';
}
public function update(User $user, Order $order): bool
{
return $user->role === 'sales';
}
}
寫到這裡會發現一件事,$user->role === 'warehouse' 這一行判斷,在商品 Policy 跟訂單明細 Policy 裡各自出現了一次,內容一模一樣。角色判斷邏輯若在多個 Policy 類別裡重複出現,可以考慮抽出共用的判斷方式,例如在 User Model 上加一個 isWarehouse() 這樣的輔助方法,讓每個 Policy 呼叫 $user->isWarehouse() 而不是各自重寫一次字串比對。這是延續 Day 15 建立的判斷邏輯集中管理精神的自然延伸,集中管理的對象從「要不要放行」,進一步收斂到「怎麼判斷這個角色」。
矩陣寫進 Policy 之後,值不值得信任,不看程式碼,看畫面。準備三組帳號,角色欄位分別設成 warehouse、sales、finance,逐一登入後台檢查。
用倉管帳號登入,打開商品列表,編輯按鈕正常顯示,符合矩陣裡商品編輯那一格。切到某張訂單的訂單明細,新增跟刪除按鈕都在,符合預期。換成業務帳號,同樣打開商品列表,這次編輯按鈕不見了,只剩下檢視。回到訂單列表,建立跟編輯按鈕正常顯示,訂單明細那邊的新增刪除按鈕則跟著消失。最後用財務帳號登入,商品列表看不到編輯按鈕,訂單列表看得到資料卻找不到任何建立或編輯的入口,訂單明細更是連新增按鈕的影子都沒有。
這一刻的意義值得停下來說清楚。這是全系列第一次,同一套後台依登入者身分不同,呈現出真正不同的操作樣貌,不再是所有人打開畫面看到一模一樣的按鈕組合。
倉管看到的是庫存管理視角,業務看到的是接單視角,財務看到的是覆核視角,三種樣貌同時存在同一套 Panel 裡,靠的完全是 Policy 判斷結果的分流,Resource 本身沒有多寫任何一行條件判斷。這正是這個階段一開始設定的目標,設計出貼近真實團隊結構的角色權限,今天算是第一次把它具體兌現。
倉管、業務、財務這三個角色的權限差異,今天第一次被具體畫成矩陣並實作進 Policy 裡,不同角色登入後看到的操作介面確實不同了。
但這裡有個新的落差得誠實點出來。這張矩陣解決的是「這個角色能不能做這件事」,如果這間批發商同時服務多個經銷據點,兩個同樣是業務角色的人,一個在台北據點、一個在高雄據點,此刻打開訂單列表,看到的仍是完全相同的一份資料,矩陣完全沒有處理「這個角色只能看到自己據點的資料」這一層範圍限制。角色分清楚了,範圍還沒有。
多租戶隔離,讓每個經銷據點只看到自己的資料,是接下來要處理的內容。