114-2 逢甲大學 軟體框架設計 期末專題
Corporate Vehicle Management System
| 學號 | 姓名 | 負責範疇 |
|---|---|---|
| D1210799 | 王建葦 | 系統架構設計、OOD 原則應用、後端 API 實作 |
| D1249623 | 陳稚翔 | 前端 Vue 3 實作、資料庫設計、整合測試 |
本系統為企業內部車輛借用管理平台,提供完整的車輛申請、審核、出車、還車工作流程,以及車輛保養紀錄管理功能。系統以物件導向設計原則(SOLID、迪米特法則)與多種 GoF 設計模式為核心架構依據。
| 功能 | 說明 |
|---|---|
| 使用者認證 | JWT 無狀態認證,BCrypt 密碼加密 |
| 車輛管理 | 車輛 CRUD、狀態追蹤(可用 / 使用中 / 維修中) |
| 借車申請 | 時段衝突檢查、申請 → 審核 → 出車 → 還車完整流程 |
| 審核工作流 | 管理員核准 / 拒絕、備注填寫、全部記錄查詢 |
| 保養管理 | 保養紀錄新增 / 查詢、到期日提醒 |
| 違規管理 | 還車超時自動登錄、管理員 / 主管手動登錄(車損、違停、交通違規等) |
| 通知收件夾 | 站內訊息中心 + 未讀狀態燈,借車事件自動通知相關人(Observer Pattern) |
| 稽核日誌 | 借車狀態操作自動記錄並持久化,管理員可查詢(Command Pattern) |
| 帳號安全 | 密碼強度政策、連續登入失敗帳號鎖定 |
| 資料治理 | 車輛軟刪除與還原,保留資料可追溯 |
| 申請維護 | 管理員 / 主管撤銷已核准申請、更改申請內容 |
| 層級 | 技術 | 版本 |
|---|---|---|
| 後端框架 | Spring Boot | 3.3.4 |
| 安全認證 | Spring Security + JJWT | 0.12.6 |
| 資料庫 | PostgreSQL + Spring Data JPA | — |
| DB 遷移 | Flyway | — |
| API 文件 | springdoc-openapi(Swagger UI) | 2.6.0 |
| 健康監控 | Spring Boot Actuator | — |
| 前端框架 | Vue 3 + TypeScript | — |
| 狀態管理 | Pinia | 2.x |
| HTTP 客戶端 | Axios | 1.x |
| 前端建構 | Vite | 5.x |
┌─────────────────────────────────────────────────┐
│ Presentation Layer (Vue 3 SPA + REST API) │
│ LoginView / EmployeeBorrowView / Admin Views │
│ AuthController / VehicleController / ... │
├─────────────────────────────────────────────────┤
│ Service Layer │
│ BorrowingService / VehicleService / │
│ MaintenanceService / UserService │
├─────────────────────────────────────────────────┤
│ Domain Layer │
│ BorrowingRequest (State Pattern) │
│ Vehicle / User / MaintenanceRecord │
│ Role / Permission (組合模式) │
├─────────────────────────────────────────────────┤
│ Repository Interface Layer (DIP) │
│ IVehicleRepository / IBorrowingRepository / … │
├─────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ JPA Adapters / JWT Security / Flyway Migration │
└─────────────────────────────────────────────────┘
├── backend/
│ └── src/
│ ├── main/java/com/vehicle/management/
│ │ ├── api/
│ │ │ ├── controller/ # REST Controllers + GlobalExceptionHandler
│ │ │ └── dto/ # Request / Response records
│ │ ├── domain/
│ │ │ ├── model/ # BorrowingRequest, Vehicle, User, Notification, AuditLog…
│ │ │ ├── state/ # State Pattern: BorrowingState, PendingState, …
│ │ │ ├── role/ # Role, AdminRole, EmployeeRole, ManagerRole, RoleFactory
│ │ │ ├── observer/ # BorrowingEventPublisher, Email/InboxNotificationObserver
│ │ │ ├── strategy/ # ConflictCheckStrategy, StrictOverlapStrategy, BufferedOverlapDecorator
│ │ │ ├── chain/ # Chain of Responsibility: BorrowingValidator 責任鏈
│ │ │ └── command/ # Command Pattern: ApproveCommand, RejectCommand, …
│ │ ├── service/ # BorrowingService, NotificationService, AuditService, LoginAttemptService…
│ │ ├── repository/ # IVehicleRepository, IBorrowingRepository, INotificationRepository…
│ │ │ └── inmemory/ # InMemory 實作(單元測試用)
│ │ └── infrastructure/
│ │ ├── persistence/ # JPA Entities + Repository Adapters
│ │ ├── security/ # JwtUtil, JwtAuthFilter, SecurityConfig
│ │ └── config/ # OpenApiConfig(Swagger UI)
│ └── test/java/com/vehicle/management/
│ ├── unit/ # BorrowingServiceTest, VehicleServiceTest, …
│ └── integration/ # BorrowingControllerTest (WebMvcTest)
└── frontend/
└── src/
├── api/ # auth / vehicles / borrowings / notifications / audit / system…
├── stores/ # auth.ts (Pinia)
├── router/ # index.ts
└── views/ # LoginView / EmployeeBorrowView / InboxView / AdminAuditView / …
本專案實作了 10 個 GoF 設計模式,涵蓋 Behavioral、Structural、Creational 三大分類,其中後 4 個(Builder、Chain of Responsibility、Decorator、Command)用於重構既有設計、消除擴充痛點。
| # | 模式 | 分類 | 主要類別 |
|---|---|---|---|
| 1 | State | Behavioral | BorrowingRequest, BorrowingState, PendingState… |
| 2 | Observer | Behavioral | BorrowingEventPublisher, EmailNotificationObserver, InboxNotificationObserver |
| 3 | Strategy | Behavioral | ConflictCheckStrategy, StrictOverlapStrategy |
| 4 | Template Method | Behavioral | AbstractProtectedService, VehicleService, MaintenanceService |
| 5 | Factory Method | Creational | RoleFactory, AdminRole, EmployeeRole, ManagerRole |
| 6 | Adapter | Structural | *RepositoryAdapter, Jpa*Repo |
| 7 | Builder | Creational | BorrowingRequest.Builder |
| 8 | Chain of Responsibility | Behavioral | BorrowingValidator, PermissionValidator, TimeConflictValidator |
| 9 | Decorator | Structural | BufferedOverlapDecorator |
| 10 | Command | Behavioral | BorrowingCommand, BorrowingCommandBus, ApproveCommand |
問題:借車申請有 5 種狀態,每種狀態只允許特定的轉換操作,若用 if-else 判斷狀態字串,Service 層會充滿分支邏輯。
解法:BorrowingRequest(Context)持有 BorrowingState 介面,所有狀態轉換邏輯封裝在各 ConcreteState 類別。非法轉換拋出 InvalidStateTransitionException(HTTP 422)。
classDiagram
class BorrowingRequest {
-BorrowingState state
+approve(reviewNote)
+reject(reviewNote)
+startUse()
+complete(endMileage)
+transitionState(newState, note)
}
class BorrowingState {
<<interface>>
+approve(BorrowingRequest, note)
+reject(BorrowingRequest, note)
+startUse(BorrowingRequest)
+complete(BorrowingRequest)
+getStateName() String
}
class PendingState {
+approve() → ApprovedState
+reject() → RejectedState
}
class ApprovedState {
+startUse() → InUseState
}
class InUseState {
+complete() → ReturnedState
}
class ReturnedState {
+所有操作拋出例外
}
class RejectedState {
+所有操作拋出例外
}
BorrowingRequest o--> BorrowingState : state
BorrowingState <|.. PendingState
BorrowingState <|.. ApprovedState
BorrowingState <|.. InUseState
BorrowingState <|.. ReturnedState
BorrowingState <|.. RejectedState
狀態流轉圖:
stateDiagram-v2
[*] --> PENDING : submitRequest()
PENDING --> APPROVED : approve()
PENDING --> REJECTED : reject()
APPROVED --> IN_USE : startUse()
IN_USE --> RETURNED : complete()
RETURNED --> [*]
REJECTED --> [*]
問題:借車申請狀態變更時需通知相關人員,但通知邏輯(Email、簡訊、推播)不應寫死在 Service 內。
解法:BorrowingService 繼承 BorrowingEventPublisher(Subject),Spring 自動注入所有實作 BorrowingEventObserver 的 Bean,Service 只呼叫 notifyApproved() 等方法,不感知具體通知實作(DIP)。
classDiagram
class BorrowingEventPublisher {
<<abstract>>
-List~BorrowingEventObserver~ observers
+addObserver(obs)
+removeObserver(obs)
#notifyApproved(req)
#notifyRejected(req)
#notifyStarted(req)
#notifyCompleted(req)
}
class BorrowingEventObserver {
<<interface>>
+onApproved(BorrowingRequest)
+onRejected(BorrowingRequest)
+onStarted(BorrowingRequest)
+onCompleted(BorrowingRequest)
}
class BorrowingService {
+approveRequest() → notifyApproved()
+rejectRequest() → notifyRejected()
+startUse() → notifyStarted()
+completeUse() → notifyCompleted()
}
class EmailNotificationObserver {
+onApproved() : 印出 Email 通知
+onRejected() : 印出 Email 通知
+onStarted() : 印出 Email 通知
+onCompleted() : 印出 Email 通知
}
BorrowingEventPublisher <|-- BorrowingService
BorrowingEventObserver <|.. EmailNotificationObserver
BorrowingEventPublisher o--> BorrowingEventObserver : observers
新增通知管道(SMS、Line 推播)只需實作
BorrowingEventObserver並標記@Component,Spring 自動注入、無需修改任何現有程式碼(OCP)。
問題:衝突判斷的演算法多樣(嚴格重疊、寬鬆同日、加緩衝時間),若寫死在 Service 中,更換邏輯需修改 Service 本身。
解法:ConflictCheckStrategy 介面定義演算法骨架,StrictOverlapStrategy 為預設實作,由 Spring DI 注入 BorrowingService。替換策略只需更換 @Bean 設定(OCP)。
classDiagram
class ConflictCheckStrategy {
<<interface>>
+hasConflict(existingRequests, periodStart, periodEnd) boolean
}
class StrictOverlapStrategy {
+hasConflict() : newStart < existEnd AND newEnd > existStart
-isActiveState(state) boolean
}
class TimeConflictValidator {
-ConflictCheckStrategy conflictStrategy
+validate(ctx) : if conflictStrategy.hasConflict()...
}
ConflictCheckStrategy <|.. StrictOverlapStrategy
TimeConflictValidator --> ConflictCheckStrategy : 注入(Spring DI)
Decorator Pattern(#65)可在不修改
StrictOverlapStrategy的情況下疊加「緩衝時間」規則。
問題:VehicleService、MaintenanceService、UserService 每個 public 方法都以相同的 if (!actor.can(permission)) throw new PermissionDeniedException(...) 開頭,違反 DRY;未來需加入稽核日誌時,需修改每個 Service。
解法:AbstractProtectedService 定義「先驗證、後執行」的演算法骨架(Template Method),子類別以 lambda 提供業務邏輯。
classDiagram
class AbstractProtectedService {
<<abstract>>
+supply(actor, perm, Supplier action) T 「樣板方法 template」
+requirePermission(actor, perm, Runnable action) 「樣板方法 template」
#checkPermission(actor, perm) 「固定步驟 primitive」
}
class VehicleService {
+createVehicle(actor, ...) : supply(MANAGE_VEHICLE, lambda)
+deleteVehicle(actor, ...) : requirePermission(MANAGE_VEHICLE, lambda)
}
class MaintenanceService {
+addRecord(actor, ...) : supply(MANAGE_VEHICLE, lambda)
}
class UserService {
+createUserByAdmin(actor, ...) : supply(MANAGE_USER, lambda)
+changeUserRole(actor, ...) : supply(MANAGE_USER, lambda)
}
AbstractProtectedService <|-- VehicleService
AbstractProtectedService <|-- MaintenanceService
AbstractProtectedService <|-- UserService
note for AbstractProtectedService "樣板方法 supply()/requirePermission() 為固定演算法骨架,內部依序呼叫兩個小步驟:① checkPermission()(固定步驟,私有 primitive)→ ② action.get()/run()(變動步驟,子類別以 lambda 提供)"
樣板方法呼叫小步驟的流程(「大的」骨架方法 call「小的」step):
sequenceDiagram
participant Sub as 子類別 createVehicle
participant TM as supply 樣板方法
participant Fix as checkPermission 固定步驟
participant Var as action.get 變動步驟
Sub->>TM: supply(actor, MANAGE_VEHICLE, lambda)
TM->>Fix: ① 呼叫固定步驟 checkPermission(actor, perm)
alt 權限不足
Fix-->>TM: throw PermissionDeniedException
end
TM->>Var: ② 呼叫變動步驟 action.get()
Var-->>TM: Vehicle(業務邏輯結果)
TM-->>Sub: Vehicle
問題:UserService 需依字串(如 "ADMIN")建立對應的 Role 物件,若直接 new AdminRole() 會產生對具體類別的直接依賴。
解法:RoleFactory.create(roleName) 集中管理 Role 物件建立,呼叫端只依賴 Role 介面(DIP)。新增角色只需加入新類別與一行 case,不需修改呼叫端(OCP)。
模式變體說明(依老師回饋補充):本專案的
RoleFactory是參數化(靜態)工廠——以roleName參數在單一create()的switch內選擇要new的ConcreteProduct,屬 GoF Factory Method 的簡化變體(業界亦稱 Simple Factory / Static Factory),而非「由Creator子類別各自覆寫factoryMethod()」的教科書形式。下圖以 Creator / Product / ConcreteProduct / Client 四個角色標示其結構。
classDiagram
class RoleFactory {
<<Creator・靜態工廠>>
+create(roleName) Role 「參數化 factory method」
}
class Role {
<<Product 介面>>
+getName() String
+getPermissions() Set~Permission~
}
class AdminRole {
<<ConcreteProduct>>
+getName() : "ADMIN"
+getPermissions() : APPROVE_BORROWING, MANAGE_VEHICLE, MANAGE_USER, MANAGE_VIOLATION, SUBMIT_REQUEST
}
class EmployeeRole {
<<ConcreteProduct>>
+getName() : "EMPLOYEE"
+getPermissions() : SUBMIT_REQUEST
}
class ManagerRole {
<<ConcreteProduct>>
+getName() : "MANAGER"
+getPermissions() : APPROVE_BORROWING, MANAGE_VIOLATION, SUBMIT_REQUEST
}
class UserService {
<<Client>>
+建立/改角色時呼叫 RoleFactory.create(roleName)
}
class Permission {
<<enum>>
APPROVE_BORROWING
MANAGE_VEHICLE
MANAGE_USER
MANAGE_VIOLATION
SUBMIT_REQUEST
}
Role <|.. AdminRole : ConcreteProduct
Role <|.. EmployeeRole : ConcreteProduct
Role <|.. ManagerRole : ConcreteProduct
UserService ..> RoleFactory : ① create(roleName)
RoleFactory ..> Role : ② 回傳 Product 抽象
RoleFactory ..> AdminRole : 依 roleName 選擇 new
RoleFactory ..> EmployeeRole : new
RoleFactory ..> ManagerRole : new
AdminRole ..> Permission : 持有
EmployeeRole ..> Permission : 持有
ManagerRole ..> Permission : 持有
note for RoleFactory "參數化工廠:單一 create() 內以 switch(roleName) 選擇 ConcreteProduct,回傳 Product 抽象(Role)。呼叫端 UserService 只依賴 Role,不依賴具體角色類別。"
問題:Domain 層定義了 IVehicleRepository 等領域介面,但資料持久化使用 Spring Data JPA,其介面(JpaRepository)與 Domain 介面不相容。
解法:每個 *RepositoryAdapter 類別實作 Domain 介面(Target),內部持有 JPA Repository(Adaptee),透過 toDomain() / toEntity() 轉換進行橋接(Object Adapter)。Service 層不感知 JPA 的存在(DIP)。
classDiagram
class VehicleService {
<<Client>>
+只依賴 IVehicleRepository(Target)
}
class IVehicleRepository {
<<Target 介面・Domain 期望>>
+findById(id) Optional~Vehicle~
+findAll() List~Vehicle~
+findAvailable(start, end) List~Vehicle~
+save(vehicle) Vehicle
+delete(id)
}
class VehicleRepositoryAdapter {
<<Adapter>>
-JpaVehicleRepo jpa
+findById(id) : jpa.findById().map(toDomain)
+save(v) : jpa.save(toEntity(v))
-toDomain(entity) Vehicle
-toEntity(vehicle) VehicleEntity
}
class JpaVehicleRepo {
<<Adaptee・Spring Data JPA>>
+findById(UUID)
+findAll()
+save(VehicleEntity)
}
class VehicleEntity {
<<JPA Entity>>
UUID id
String plate
String model
...
}
VehicleService ..> IVehicleRepository : 依賴 Target(DIP)
IVehicleRepository <|.. VehicleRepositoryAdapter : 實作 Target
VehicleRepositoryAdapter --> JpaVehicleRepo : 委派 Adaptee
JpaVehicleRepo --> VehicleEntity : 管理 Entity
note for VehicleRepositoryAdapter "Adapter 角色:對外實作 Target(IVehicleRepository),對內持有並委派 Adaptee(JpaVehicleRepo),透過 toDomain()/toEntity() 在 Domain 物件與 JPA Entity 間轉換。Client(VehicleService)全程只看見 Target。"
5 個 Adapter 結構相同:
BorrowingRepositoryAdapter、UserRepositoryAdapter、MaintenanceRepositoryAdapter、ViolationRepositoryAdapter。
| 原則 | 應用位置 |
|---|---|
| SRP | Service 層分為 BorrowingService / VehicleService / MaintenanceService / UserService,各自只有單一修改理由 |
| OCP | Factory Method 新增角色免改舊程式碼;Observer 新增通知管道免改 Service;Strategy 替換演算法免改 Context |
| DIP | 所有 Service 依賴 Repository 介面(非 JPA 實作);Strategy 與 Observer 依賴抽象介面注入 |
| LoD | Service 呼叫 request.approve() 委派給 State,不直接操作狀態字串;MaintenanceService 呼叫 record.isDue() 而非讀取欄位 |
| DRY | AbstractProtectedService 統一權限守衛邏輯,消除各 Service 的重複 if-throw |
| 組合優於繼承 | User 持有 Set<Role>,Role 持有 Set<Permission>,角色以組合方式擴充 |
7. Builder Pattern — 借車申請物件建構(#63)
問題:BorrowingRepositoryAdapter.toDomain() 從資料庫還原物件時,需呼叫 r.approve(null); r.startUse(); 等副作用方法重播 State transition,會觸發 Observer 通知且邏輯脆弱。
解法:BorrowingRequest.Builder 提供 restoreState(String) 方法直接設定目標 State 物件,完全繞過 State transition 副作用,使 Adapter 的還原邏輯清晰且安全。
classDiagram
class BorrowingRequest {
<<Product 產品>>
-UUID id
-UUID userId
-UUID vehicleId
-BorrowingState state
-String reviewNote
-Instant updatedAt
-Integer startMileage
-Integer endMileage
+builder(id, userId, vehicleId, start, end, purpose, createdAt) Builder
-BorrowingRequest(Builder b)
}
class Builder {
<<Builder 建造者・static nested>>
-BorrowingState state
-String reviewNote
-Instant updatedAt
-Integer startMileage
-Integer endMileage
+reviewNote(note) Builder
+updatedAt(t) Builder
+startMileage(km) Builder
+endMileage(km) Builder
+restoreState(stateName) Builder
+build() BorrowingRequest
}
class BorrowingRepositoryAdapter {
<<Director/Client 指揮者>>
+toDomain(entity) : builder(...).restoreState(s).reviewNote(n)...build()
}
class BorrowingState {
<<interface・產品零件>>
+getStateName() String
}
BorrowingRequest *-- Builder : 內部類別(static nested)
BorrowingRepositoryAdapter ..> Builder : ① 逐步設定欄位(fluent 鏈)
Builder ..> BorrowingRequest : ② build() 產生 Product
Builder ..> BorrowingState : restoreState() 直接指定狀態(無轉換副作用)
note for Builder "每個 setter 回傳 this 形成流暢鏈;restoreState(stateName) 以 switch 直接建立對應的 BorrowingState(PendingState/ApprovedState/InUseState/ReturnedState/RejectedState),繞過 approve()/startUse() 等會觸發 Observer 的副作用方法。"
toDomain()舊寫法:r.approve(null); r.startUse();(重播副作用) 新寫法:.restoreState("IN_USE").reviewNote(...).build()(直接設定,無副作用)
8. Chain of Responsibility Pattern — 借車申請多步驟驗證(#64)
問題:BorrowingService.submitRequest() 依序進行「權限檢查 → 車輛存在 → 時段衝突」三步驗證,邏輯全寫在一個方法中,新增驗證規則需修改 Service 本身(違反 OCP)。
解法:三個驗證步驟各自實作 BorrowingValidator(@Component @Order),Spring 依 @Order 排序後注入 List<BorrowingValidator>,submitRequest() 只需 validators.forEach(v -> v.validate(ctx))。
classDiagram
class BorrowingValidator {
<<interface>>
+validate(ctx BorrowingValidationContext)
}
class BorrowingValidationContext {
<<record>>
+actor User
+vehicleId UUID
+periodStart Instant
+periodEnd Instant
}
class PermissionValidator {
+validate(ctx) : 檢查 SUBMIT_REQUEST 權限
}
class VehicleExistenceValidator {
+validate(ctx) : 確認車輛存在
}
class TimeConflictValidator {
-IBorrowingRepository borrowingRepo
-ConflictCheckStrategy conflictStrategy
+validate(ctx) : 以策略檢查時段衝突
}
class BorrowingService {
-List~BorrowingValidator~ validators
+submitRequest() : validators.forEach(v→v.validate(ctx))
}
BorrowingValidator <|.. PermissionValidator
BorrowingValidator <|.. VehicleExistenceValidator
BorrowingValidator <|.. TimeConflictValidator
BorrowingService --> BorrowingValidator : Spring 注入責任鏈
TimeConflictValidator --> ConflictCheckStrategy : 委派演算法
新增驗證規則(例如「同一員工每月申請不超過 5 次」)只需新增一個
@Component @Order的BorrowingValidator實作,無需修改BorrowingService(OCP)。
9. Decorator Pattern — 可疊加的衝突緩衝策略(#65)
問題:若需在現有嚴格重疊策略上加入「借車前後保留 30 分鐘緩衝時間」的規則,直接修改 StrictOverlapStrategy 會使其邏輯複雜,且難以在不同情境下選擇是否啟用緩衝。
解法:BufferedOverlapDecorator 包裝任意 ConflictCheckStrategy,在委派前後延伸時段,不修改被包裝的策略(OCP)。透過 Spring @Bean @Primary 可選擇性啟用。
classDiagram
class ConflictCheckStrategy {
<<Component 介面>>
+hasConflict(existing, start, end) boolean
}
class StrictOverlapStrategy {
<<ConcreteComponent>>
+hasConflict() : newStart < existEnd AND newEnd > existStart
-isActiveState(state) boolean
}
class BufferedOverlapDecorator {
<<ConcreteDecorator>>
-ConflictCheckStrategy inner
-Duration buffer
+BufferedOverlapDecorator(inner, buffer)
+hasConflict(existing, start, end) : inner.hasConflict(start-buffer, end+buffer)
}
class TimeConflictValidator {
<<Client>>
-ConflictCheckStrategy conflictStrategy
+validate(ctx) : conflictStrategy.hasConflict(...)
}
ConflictCheckStrategy <|.. StrictOverlapStrategy : ConcreteComponent
ConflictCheckStrategy <|.. BufferedOverlapDecorator : IS-A(實作同介面)
BufferedOverlapDecorator o--> ConflictCheckStrategy : HAS-A inner(包裝並委派)
TimeConflictValidator --> ConflictCheckStrategy : 注入(Spring @Primary 決定是否裝飾)
note for BufferedOverlapDecorator "Decorator 同時 IS-A 與 HAS-A Component:實作 ConflictCheckStrategy(可被當成一般策略注入),又持有一個 inner ConflictCheckStrategy。hasConflict() 先把時段前後各延伸 buffer,再委派 inner。因 inner 型別是介面,可層層堆疊多個 Decorator(如再包一層節假日規則)而不改任何既有類別(OCP)。"
啟用緩衝:在 Spring 設定類別宣告
@Bean @Primary ConflictCheckStrategy buffered(StrictOverlapStrategy base) { return new BufferedOverlapDecorator(base, Duration.ofMinutes(30)); }
10. Command Pattern — 借車狀態操作稽核(#66)
問題:BorrowingController 直接呼叫 borrowingService.approveRequest(...) 等方法,各操作的稽核日誌、非同步處理等橫切關注點分散在各 endpoint,無法統一管理。
解法:每個狀態變更操作封裝為 BorrowingCommand(ApproveCommand、RejectCommand、StartUseCommand、CompleteCommand),由 BorrowingCommandBus(Invoker)統一執行並記錄稽核日誌。
classDiagram
class BorrowingCommand {
<<interface>>
+execute() BorrowingRequest
+describe() String
}
class ApproveCommand {
-User actor
-UUID requestId
-String note
+execute() : service.approveRequest(actor, id, note)
+describe() : "ApproveCommand[...]"
}
class RejectCommand {
+execute() : service.rejectRequest(...)
}
class StartUseCommand {
+execute() : service.startUse(...)
}
class CompleteCommand {
-int endMileage
+execute() : service.completeUse(...)
}
class BorrowingCommandBus {
+dispatch(cmd) : log audit → cmd.execute()
}
class BorrowingController {
-BorrowingCommandBus commandBus
+approve() : commandBus.dispatch(new ApproveCommand(...))
+reject() : commandBus.dispatch(new RejectCommand(...))
+startUse() : commandBus.dispatch(new StartUseCommand(...))
+complete() : commandBus.dispatch(new CompleteCommand(...))
}
BorrowingCommand <|.. ApproveCommand
BorrowingCommand <|.. RejectCommand
BorrowingCommand <|.. StartUseCommand
BorrowingCommand <|.. CompleteCommand
BorrowingCommandBus --> BorrowingCommand : dispatch() → execute()
BorrowingController --> BorrowingCommandBus : 使用
BorrowingCommandBus.dispatch()目前記錄[AUDIT]稽核日誌;未來可在此加入命令佇列、重試、非同步執行,無需修改任何 Command 實作(OCP)。
同上方 State Pattern 章節的
stateDiagram-v2。
完整流程依借車申請的生命週期狀態 PENDING → APPROVED → IN_USE → RETURNED 拆分為四個階段,
各自聚焦一個 REST 端點與其涉及的設計模式,避免單一大圖資訊過載。
sequenceDiagram
actor Emp as 員工
participant C as BorrowingController
participant S as BorrowingService
participant V as 驗證責任鏈
participant R as IBorrowingRepository
Emp->>C: POST /api/borrowings
C->>S: submitRequest(actor, vehicleId, start, end, purpose)
Note over S,V: Chain of Responsibility,依序驗證,任一失敗即中止
S->>V: validators.forEach(validate)
V-->>V: 1. PermissionValidator(SUBMIT_REQUEST 權限)
V-->>V: 2. VehicleExistenceValidator(車輛存在)
V-->>V: 3. TimeConflictValidator(時段衝突,委派 ConflictCheckStrategy)
V-->>S: 全數通過
S->>S: new BorrowingRequest(初始 PendingState)
S->>R: save(request)
S-->>C: BorrowingResponse(PENDING)
C-->>Emp: 201 Created
sequenceDiagram
actor Apv as 審核者(管理員 / 主管)
participant C as BorrowingController
participant Bus as BorrowingCommandBus
participant Cmd as ApproveCommand
participant S as BorrowingService
participant St as BorrowingState
participant Obs as EmailNotificationObserver
Apv->>C: POST /api/borrowings/{id}/approve
Note over C,Cmd: Command Pattern,封裝操作並集中稽核
C->>Bus: dispatch(new ApproveCommand(...))
Bus->>Bus: 記錄稽核日誌 [AUDIT]
Bus->>Cmd: execute()
Cmd->>S: approveRequest(actor, id, note)
opt actor 角色為 MANAGER
S->>S: checkManagerDepartmentScope(僅限審核同部門申請)
end
Note over S,St: State Pattern,委派狀態物件轉換
S->>St: request.approve(note)
St-->>S: PendingState → ApprovedState
S->>S: borrowingRepo.save(request)
Note over S,Obs: Observer Pattern,廣播事件
S->>Obs: notifyApproved → onApproved(寄送 Email)
S-->>C: BorrowingResponse(APPROVED)
C-->>Apv: 200 OK
Note over Apv,Obs: 拒絕流程對稱,RejectCommand → rejectRequest → RejectedState → notifyRejected
sequenceDiagram
actor Op as 管理端
participant C as BorrowingController
participant Bus as BorrowingCommandBus
participant S as BorrowingService
participant Veh as Vehicle / VehicleRepo
participant St as BorrowingState
participant Obs as EmailNotificationObserver
Op->>C: POST /api/borrowings/{id}/start
Note over C,Bus: Command Pattern,StartUseCommand
C->>Bus: dispatch(new StartUseCommand(...))
Bus->>S: execute() → startUse(id)
S->>Veh: markInUse() + save(車輛狀態 → IN_USE)
S->>St: request.startUse()
St-->>S: ApprovedState → InUseState
S->>S: borrowingRepo.save(request)
S->>Obs: notifyStarted → onStarted(寄送 Email)
S-->>C: BorrowingResponse(IN_USE)
C-->>Op: 200 OK
sequenceDiagram
actor Op as 管理端
participant C as BorrowingController
participant Bus as BorrowingCommandBus
participant S as BorrowingService
participant Veh as Vehicle / VehicleRepo
participant Obs as EmailNotificationObserver
participant Vio as ViolationService
Op->>C: POST /api/borrowings/{id}/complete
Note over C,Bus: Command Pattern,CompleteCommand
C->>Bus: dispatch(new CompleteCommand(..., endMileage))
Bus->>S: execute() → completeUse(id, endMileage)
S->>Veh: markAvailable() + updateMileage() + save
S->>S: request.complete() → ReturnedState(State Pattern)
S->>S: borrowingRepo.save(request)
S->>Obs: notifyCompleted → onCompleted(寄送 Email)
opt 實際還車晚於預計歸還時間
S->>Vio: checkAndCreateOverdue(request)
Vio-->>Vio: 自動建立 OVERDUE 違規記錄
end
S-->>C: BorrowingResponse(RETURNED)
C-->>Op: 200 OK
- Java 21+(需加入
PATH或設定JAVA_HOME) - Node.js 18+
- PostgreSQL 14+(僅正式模式需要,開發模式可跳過)
- Maven 3.9+(可選,本專案含 Maven Wrapper,無 Maven 亦可執行)
適合本機快速測試,啟動後自動建立測試帳號與車輛資料。
後端(Windows PowerShell)
cd backend
.\mvnw.cmd spring-boot:run "-Dspring-boot.run.profiles=dev"
# API 服務啟動於 http://localhost:8080後端(Mac / Linux)
cd backend
./mvnw spring-boot:run -Dspring-boot.run.profiles=devDev 模式啟動後會自動建立:
- 管理員帳號:
admin@demo.com/Admin1234 - 主管帳號:
manager@demo.com/Manager1234(部門:測試部門) - 員工帳號(員工甲):
emp@demo.com/Emp12345(部門:測試部門) - 3 輛測試車輛
主管與員工甲同屬「測試部門」,可用於展示「主管僅能審核同部門員工借車申請」的部門範圍限制功能。
後端
# Windows PowerShell — 建立資料庫
createdb vehicle_mgmt
# 設定環境變數
$env:DB_USERNAME = "postgres"
$env:DB_PASSWORD = "your_password"
$env:JWT_SECRET = "at-least-32-characters-long-secret"
# 啟動後端(Flyway 自動執行 DB 遷移)
cd backend
.\mvnw.cmd spring-boot:run
# API 服務啟動於 http://localhost:8080# Mac / Linux
createdb vehicle_mgmt
export DB_USERNAME=postgres
export DB_PASSWORD=your_password
export JWT_SECRET=at-least-32-characters-long-secret
cd backend
./mvnw spring-boot:run手動建立測試帳號
# Windows PowerShell
Invoke-RestMethod -Method Post -Uri http://localhost:8080/api/auth/register `
-ContentType "application/json" `
-Body '{"name":"管理員","email":"admin@company.com","password":"Admin1234","role":"ADMIN"}'
Invoke-RestMethod -Method Post -Uri http://localhost:8080/api/auth/register `
-ContentType "application/json" `
-Body '{"name":"員工甲","email":"emp@company.com","password":"Emp12345","role":"EMPLOYEE"}'# Mac / Linux(curl)
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"管理員","email":"admin@company.com","password":"Admin1234","role":"ADMIN"}'
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"員工甲","email":"emp@company.com","password":"Emp12345","role":"EMPLOYEE"}'cd frontend
npm install
npm run dev
# 前端啟動於 http://localhost:5173所有需要認證的端點請在 Header 加入:
Authorization: Bearer <JWT Token>
| 方法 | 路徑 | 說明 |
|---|---|---|
| POST | /api/auth/register |
註冊帳號(body: name, email, password, role) |
| POST | /api/auth/login |
登入,回傳 JWT token |
| 方法 | 路徑 | 權限 | 說明 |
|---|---|---|---|
| GET | /api/vehicles |
認證用戶 | 所有車輛列表 |
| GET | /api/vehicles/available?start=&end= |
認證用戶 | 查詢時段內可用車輛 |
| GET | /api/vehicles/{id} |
認證用戶 | 取得單一車輛 |
| POST | /api/vehicles |
Admin | 新增車輛 |
| PUT | /api/vehicles/{id} |
Admin | 更新車輛 |
| DELETE | /api/vehicles/{id} |
Admin | 刪除車輛 |
| 方法 | 路徑 | 權限 | 說明 |
|---|---|---|---|
| POST | /api/borrowings |
認證用戶 | 送出借車申請 |
| GET | /api/borrowings/my |
認證用戶 | 查看個人申請記錄 |
| GET | /api/borrowings/pending |
認證用戶 | 待審核申請列表 |
| GET | /api/borrowings |
Admin | 所有申請記錄 |
| POST | /api/borrowings/{id}/approve |
Admin | 核准申請 |
| POST | /api/borrowings/{id}/reject |
Admin | 拒絕申請 |
| POST | /api/borrowings/{id}/start |
Admin | 標記出車 |
| POST | /api/borrowings/{id}/complete |
Admin | 標記還車 |
| 方法 | 路徑 | 權限 | 說明 |
|---|---|---|---|
| POST | /api/maintenance |
Admin | 新增保養記錄 |
| GET | /api/maintenance/vehicle/{vehicleId} |
認證用戶 | 查詢車輛保養記錄 |
| DELETE | /api/maintenance/{id} |
Admin | 刪除保養記錄 |
| 方法 | 路徑 | 權限 | 說明 |
|---|---|---|---|
| GET | /api/users |
Admin | 查詢所有使用者 |
| GET | /api/users/{id} |
Admin | 查詢單一使用者 |
| POST | /api/users |
Admin | 建立使用者(body: name, email, password, role) |
| PUT | /api/users/{id} |
Admin | 更新名稱與 Email |
| PATCH | /api/users/{id}/role |
Admin | 變更使用者角色(body: role) |
| DELETE | /api/users/{id} |
Admin | 刪除使用者帳號 |
使用 InMemory Repository 實作,不依賴資料庫,執行速度為毫秒級:
# Windows PowerShell
cd backend
.\mvnw.cmd test "-Dtest=*ServiceTest"# Mac / Linux
cd backend
./mvnw test -Dtest="*ServiceTest"| 測試類別 | 測試案例 |
|---|---|
BorrowingServiceTest |
送審、核准、拒絕、員工無法審核、衝突檢查、完整工作流程 |
VehicleServiceTest |
建立、查詢、刪除、員工無法管理車輛 |
MaintenanceServiceTest |
新增、查詢(依車輛)、到期提醒、刪除、員工無法操作 |
UserServiceTest |
註冊、密碼雜湊、重複 email、角色權限驗證 |
使用 @WebMvcTest + @MockBean 測試 REST 控制器的完整 HTTP 處理鏈:
# Windows PowerShell
.\mvnw.cmd test "-Dtest=BorrowingControllerTest"# Mac / Linux
./mvnw test -Dtest="BorrowingControllerTest"測試內容涵蓋:HTTP 狀態碼、JSON 序列化、JWT 認證守衛、权限拒絕(403)、未認證請求(401)。
users ─────────────────────────────────────────────────────────
id, name, email, password_hash, created_at
│
├──→ user_roles ──→ roles ──→ role_permissions ──→ permissions
│
└──→ borrowing_requests ──→ vehicles
id, user_id, vehicle_id id, plate, model, year, status
period_start, period_end created_at
state, review_note
created_at, updated_at
vehicles ──→ maintenance_records
id, vehicle_id, date
items, cost
next_due_date, next_due_km
| Phase | 內容 | Issues |
|---|---|---|
| Phase 1 | 初始化後端(Spring Boot)、前端(Vue 3)、DB 遷移結構 | #1–3 ✅ |
| Phase 2 | Domain Layer — Vehicle、User、BorrowingRequest(State Pattern)、Role(Factory) | #4–7 ✅ |
| Phase 3 | Repository Layer — 介面定義、InMemory 實作、JPA Adapter | #8–10 ✅ |
| Phase 4 | Service Layer — BorrowingService(Observer + Strategy)、VehicleService 等 | #11–13 ✅ |
| Phase 5 | Presentation Layer — REST Controllers、DTOs、JWT Security | #14–17 ✅ |
| Phase 6 | Frontend — 5 個 Vue 3 頁面(登入、借車、審核、車輛管理、保養管理) | #18–22 ✅ |
| Phase 7 | 測試 — 4 個 Service 單元測試、1 個 Controller 整合測試 | #23–24 ✅ |
| Phase 8 | 使用者帳號管理 — CRUD API、前端管理頁面、角色切換 | #35 ✅ |
| Phase 9 | UML 文件(Mermaid)與 JavaDoc 全面補全 | #36 ✅ |
| Phase 10 | 新功能擴充 — 里程追蹤、月曆視圖、衝突 UI、違規記錄、儀表板、Excel 匯出、MANAGER 角色 | #52–58 ✅ |
| Phase 11 | CI/CD — GitHub Actions tag-based 自動部署至 VPS | #59 ✅ |
push tag v*
│
▼
GitHub Actions
├─ 建置後端 JAR(Maven, Java 21)
├─ SCP 上傳 JAR 到 VPS ~/deploy/
└─ SSH 進 VPS
├─ git pull 最新前端原始碼
├─ npm install 前端相依套件
├─ pm2 重啟後端(port 8080,dev profile,H2 DB)
└─ pm2 重啟前端(port 5173,Vite dev server)
│
▼
Cloudflare Tunnel
demo.jw-albert.dev → localhost:5173
/api/* → proxied to localhost:8080
CI/CD 只在推送 v* 格式的 tag 時觸發,不監聽 push to main。
這樣可以精準控制部署時機,節省 GitHub Actions 額度。
# 完成所有 PR 合併後,在 main 分支推送 tag 即可觸發部署
git checkout main
git pull origin main
git tag v1.0.0
git push origin v1.0.0Tag 命名建議遵循 Semantic Versioning:
v1.0.0— 正式版本v1.1.0— 新增功能v1.0.1— 修復 Bug
| Secret 名稱 | 說明 |
|---|---|
HOST_ADDRESS |
VPS IP 或域名 |
HOST_PORT |
SSH Port |
ACCOUNT_NAME |
SSH 使用者名稱 |
ACCOUNT_PASSWORD |
SSH 密碼 |
| 項目 | 說明 |
|---|---|
| Java 21 | 需安裝並可被 which java 找到(或位於 /usr/bin/java) |
| Node.js 18+ | 執行 Vite dev server |
| PM2 | 首次部署時會自動安裝(無需 sudo) |
| Git | 用於 clone / pull 前端原始碼 |
| Cloudflare Tunnel | 已設定 demo.jw-albert.dev → localhost:5173 |
注意:VPS 使用者不需要 sudo 權限。PM2 透過
NPM_CONFIG_PREFIX=$HOME/.npm-global安裝於使用者目錄下。
# SSH 進 VPS 後手動操作
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
# 更新程式碼
git -C ~/repo pull origin main
cd ~/repo/frontend && npm install
# 重啟服務
JAVA_BIN=$(which java)
pm2 delete vehicle-backend 2>/dev/null || true
pm2 start "$JAVA_BIN -jar ~/app/app.jar --spring.profiles.active=dev" --name vehicle-backend
pm2 delete vehicle-frontend 2>/dev/null || true
pm2 start "npm run dev" --name vehicle-frontend --cwd ~/repo/frontend
pm2 save && pm2 list