Skip to content

Commit e22c7a9

Browse files
authored
Merge pull request #53 from DamnDamnDamnM3/feature/mileage-tracking
feat: 新增行車里程追蹤功能
2 parents bb3e9cf + bf72747 commit e22c7a9

17 files changed

Lines changed: 162 additions & 16 deletions

File tree

backend/src/main/java/com/vehicle/management/api/controller/BorrowingController.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.vehicle.management.api.controller;
22

33
import com.vehicle.management.api.dto.BorrowingResponse;
4+
import com.vehicle.management.api.dto.CompleteBorrowingRequest;
45
import com.vehicle.management.api.dto.ReviewBorrowingRequest;
56
import com.vehicle.management.api.dto.SubmitBorrowingRequest;
67
import com.vehicle.management.domain.model.User;
@@ -77,7 +78,8 @@ public BorrowingResponse startUse(@PathVariable UUID id) {
7778
}
7879

7980
@PostMapping("/{id}/complete")
80-
public BorrowingResponse complete(@PathVariable UUID id) {
81-
return BorrowingResponse.from(borrowingService.completeUse(id));
81+
public BorrowingResponse complete(@PathVariable UUID id,
82+
@Valid @RequestBody CompleteBorrowingRequest req) {
83+
return BorrowingResponse.from(borrowingService.completeUse(id, req.endMileage()));
8284
}
8385
}

backend/src/main/java/com/vehicle/management/api/dto/BorrowingResponse.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ public record BorrowingResponse(
1515
String state,
1616
String reviewNote,
1717
Instant createdAt,
18-
Instant updatedAt
18+
Instant updatedAt,
19+
Integer startMileage,
20+
Integer endMileage
1921
) {
2022
public static BorrowingResponse from(BorrowingRequest r) {
2123
return new BorrowingResponse(
2224
r.getId(), r.getUserId(), r.getVehicleId(),
2325
r.getPeriodStart(), r.getPeriodEnd(), r.getPurpose(),
2426
r.getStateName(), r.getReviewNote(),
25-
r.getCreatedAt(), r.getUpdatedAt());
27+
r.getCreatedAt(), r.getUpdatedAt(),
28+
r.getStartMileage(), r.getEndMileage());
2629
}
2730
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.vehicle.management.api.dto;
2+
3+
import jakarta.validation.constraints.Min;
4+
5+
/** 還車請求 DTO,攜帶結束里程數。 */
6+
public record CompleteBorrowingRequest(
7+
@Min(value = 0, message = "里程數不可為負數")
8+
int endMileage
9+
) {}

backend/src/main/java/com/vehicle/management/api/dto/VehicleResponse.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ public record VehicleResponse(
1111
String model,
1212
int year,
1313
String status,
14-
Instant createdAt
14+
Instant createdAt,
15+
int currentMileage
1516
) {
1617
public static VehicleResponse from(Vehicle v) {
1718
return new VehicleResponse(v.getId(), v.getPlate(), v.getModel(),
18-
v.getYear(), v.getStatus().name(), v.getCreatedAt());
19+
v.getYear(), v.getStatus().name(), v.getCreatedAt(), v.getCurrentMileage());
1920
}
2021
}

backend/src/main/java/com/vehicle/management/domain/model/BorrowingRequest.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public class BorrowingRequest {
3737
private String reviewNote;
3838
private final Instant createdAt;
3939
private Instant updatedAt;
40+
/** 出車時記錄的起始里程(可為 null)。 */
41+
private Integer startMileage;
42+
/** 還車時記錄的結束里程(可為 null)。 */
43+
private Integer endMileage;
4044

4145
/**
4246
* 建構新的借車申請,初始狀態為 {@link PendingState}(待審核)。
@@ -102,14 +106,16 @@ public void startUse() {
102106
}
103107

104108
/**
105-
* 完成用車並還車。
109+
* 完成用車並還車,記錄結束里程
106110
* 委派給目前的 {@link BorrowingState#complete} 執行狀態轉換。
107111
*
112+
* @param endMileage 還車時的里程數
108113
* @throws com.vehicle.management.domain.state.InvalidStateTransitionException
109114
* 若目前狀態不是 IN_USE
110115
*/
111-
public void complete() {
116+
public void complete(int endMileage) {
112117
state.complete(this);
118+
this.endMileage = endMileage;
113119
this.updatedAt = Instant.now();
114120
}
115121

@@ -160,4 +166,21 @@ public void transitionState(BorrowingState newState, String note) {
160166

161167
/** @return 最後更新時間 */
162168
public Instant getUpdatedAt() { return updatedAt; }
169+
170+
/** @return 出車時的起始里程(可能為 {@code null}) */
171+
public Integer getStartMileage() { return startMileage; }
172+
173+
/** @return 還車時的結束里程(可能為 {@code null}) */
174+
public Integer getEndMileage() { return endMileage; }
175+
176+
/**
177+
* 由 Adapter 呼叫,從資料庫還原里程資料。
178+
*
179+
* @param startMileage 起始里程
180+
* @param endMileage 結束里程
181+
*/
182+
public void restoreMileage(Integer startMileage, Integer endMileage) {
183+
this.startMileage = startMileage;
184+
this.endMileage = endMileage;
185+
}
163186
}

backend/src/main/java/com/vehicle/management/domain/model/Vehicle.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public class Vehicle {
3232
/** 可變狀態欄位,僅透過狀態守衛方法修改。 */
3333
private VehicleStatus status;
3434
private final Instant createdAt;
35+
/** 車輛目前累積里程(公里),預設為 0。 */
36+
private int currentMileage;
3537

3638
/**
3739
* 建構車輛物件。
@@ -45,12 +47,29 @@ public class Vehicle {
4547
*/
4648
public Vehicle(UUID id, String plate, String model, int year,
4749
VehicleStatus status, Instant createdAt) {
50+
this(id, plate, model, year, status, createdAt, 0);
51+
}
52+
53+
/**
54+
* 建構車輛物件(含里程)。
55+
*
56+
* @param id 車輛唯一識別碼
57+
* @param plate 車牌號碼
58+
* @param model 車款名稱
59+
* @param year 出廠年份
60+
* @param status 初始狀態
61+
* @param createdAt 建立時間
62+
* @param currentMileage 目前累積里程
63+
*/
64+
public Vehicle(UUID id, String plate, String model, int year,
65+
VehicleStatus status, Instant createdAt, int currentMileage) {
4866
this.id = id;
4967
this.plate = plate;
5068
this.model = model;
5169
this.year = year;
5270
this.status = status;
5371
this.createdAt = createdAt;
72+
this.currentMileage = currentMileage;
5473
}
5574

5675
/**
@@ -109,4 +128,21 @@ public void markMaintenance() {
109128

110129
/** @return 車輛資料建立時間 */
111130
public Instant getCreatedAt() { return createdAt; }
131+
132+
/** @return 車輛目前累積里程(公里) */
133+
public int getCurrentMileage() { return currentMileage; }
134+
135+
/**
136+
* 更新車輛累積里程。
137+
*
138+
* @param km 新的里程數(必須 >= 目前里程)
139+
* @throws IllegalArgumentException 若 km 小於目前里程(里程不可倒退)
140+
*/
141+
public void updateMileage(int km) {
142+
if (km < currentMileage) {
143+
throw new IllegalArgumentException(
144+
"New mileage " + km + " is less than current mileage " + currentMileage);
145+
}
146+
this.currentMileage = km;
147+
}
112148
}

backend/src/main/java/com/vehicle/management/infrastructure/persistence/BorrowingRepositoryAdapter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,15 @@ private BorrowingRequest toDomain(BorrowingRequestEntity e) {
5656
e.getId(), e.getUserId(), e.getVehicleId(),
5757
e.getPeriodStart(), e.getPeriodEnd(), e.getPurpose(), e.getCreatedAt());
5858
// Restore state by replaying transitions
59+
int endMil = e.getEndMileage() != null ? e.getEndMileage() : 0;
5960
switch (e.getState()) {
6061
case "APPROVED" -> r.approve(e.getReviewNote());
6162
case "REJECTED" -> r.reject(e.getReviewNote());
6263
case "IN_USE" -> { r.approve(null); r.startUse(); }
63-
case "RETURNED" -> { r.approve(null); r.startUse(); r.complete(); }
64+
case "RETURNED" -> { r.approve(null); r.startUse(); r.complete(endMil); }
6465
// PENDING: no replay needed
6566
}
67+
r.restoreMileage(e.getStartMileage(), e.getEndMileage());
6668
return r;
6769
}
6870

@@ -78,6 +80,8 @@ private BorrowingRequestEntity toEntity(BorrowingRequest r) {
7880
e.setReviewNote(r.getReviewNote());
7981
e.setCreatedAt(r.getCreatedAt());
8082
e.setUpdatedAt(r.getUpdatedAt());
83+
e.setStartMileage(r.getStartMileage());
84+
e.setEndMileage(r.getEndMileage());
8185
return e;
8286
}
8387
}

backend/src/main/java/com/vehicle/management/infrastructure/persistence/VehicleRepositoryAdapter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void delete(UUID id) {
5353

5454
private Vehicle toDomain(VehicleEntity e) {
5555
return new Vehicle(e.getId(), e.getPlate(), e.getModel(), e.getYear(),
56-
VehicleStatus.valueOf(e.getStatus()), e.getCreatedAt());
56+
VehicleStatus.valueOf(e.getStatus()), e.getCreatedAt(), e.getCurrentMileage());
5757
}
5858

5959
private VehicleEntity toEntity(Vehicle v) {
@@ -64,6 +64,7 @@ private VehicleEntity toEntity(Vehicle v) {
6464
e.setYear(v.getYear());
6565
e.setStatus(v.getStatus().name());
6666
e.setCreatedAt(v.getCreatedAt());
67+
e.setCurrentMileage(v.getCurrentMileage());
6768
return e;
6869
}
6970
}

backend/src/main/java/com/vehicle/management/infrastructure/persistence/entity/BorrowingRequestEntity.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ public class BorrowingRequestEntity {
3939
@Column(name = "updated_at", nullable = false)
4040
private Instant updatedAt = Instant.now();
4141

42+
@Column(name = "start_mileage")
43+
private Integer startMileage;
44+
45+
@Column(name = "end_mileage")
46+
private Integer endMileage;
47+
4248
public BorrowingRequestEntity() {}
4349

4450
public UUID getId() { return id; }
@@ -61,4 +67,8 @@ public BorrowingRequestEntity() {}
6167
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
6268
public Instant getUpdatedAt() { return updatedAt; }
6369
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
70+
public Integer getStartMileage() { return startMileage; }
71+
public void setStartMileage(Integer startMileage) { this.startMileage = startMileage; }
72+
public Integer getEndMileage() { return endMileage; }
73+
public void setEndMileage(Integer endMileage) { this.endMileage = endMileage; }
6474
}

backend/src/main/java/com/vehicle/management/infrastructure/persistence/entity/VehicleEntity.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public class VehicleEntity {
2727
@Column(name = "created_at", nullable = false, updatable = false)
2828
private Instant createdAt = Instant.now();
2929

30+
@Column(name = "current_mileage", nullable = false)
31+
private int currentMileage = 0;
32+
3033
public VehicleEntity() {}
3134

3235
public UUID getId() { return id; }
@@ -41,4 +44,6 @@ public VehicleEntity() {}
4144
public void setStatus(String status) { this.status = status; }
4245
public Instant getCreatedAt() { return createdAt; }
4346
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
47+
public int getCurrentMileage() { return currentMileage; }
48+
public void setCurrentMileage(int currentMileage) { this.currentMileage = currentMileage; }
4449
}

0 commit comments

Comments
 (0)