Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.fsd.admin.service.AdminQueryFacadeService;
import com.fsd.admin.vo.AdminBatchTaskResultResponse;
import com.fsd.admin.vo.AdminDashboardSummaryResponse;
import com.fsd.common.exception.BusinessException;
import com.fsd.common.model.PageResponse;
import com.fsd.common.model.ApiResponse;
import com.fsd.dispatch.vo.DispatchExceptionListItemResponse;
Expand All @@ -26,6 +27,7 @@
import com.fsd.dispatch.service.DispatchAdminQueryService;
import com.fsd.dispatch.service.DispatchExceptionService;
import com.fsd.dispatch.dto.DispatchTaskManualAssignRequest;
import com.fsd.dispatch.entity.DispatchExceptionRecordEntity;
import com.fsd.dispatch.service.DispatchTaskService;
import com.fsd.dispatch.vo.DispatchTaskAssignResponse;
import com.fsd.dispatch.service.MobileOrderAuthService;
Expand All @@ -45,6 +47,7 @@
import com.fsd.dispatch.vo.ParkStationResponse;
import com.fsd.dispatch.vo.ParkVehicleSnapshotResponse;
import com.fsd.order.service.OrderAdminQueryService;
import com.fsd.order.service.OrderStateService;
import com.fsd.admin.service.OrderAdminDetailService;
import com.fsd.admin.service.TaskAdminDetailService;
import com.fsd.order.service.OrderQueryService;
Expand Down Expand Up @@ -77,6 +80,7 @@ public class AdminDispatchController {

private final OrderAdminQueryService orderAdminQueryService;
private final OrderQueryService orderQueryService;
private final OrderStateService orderStateService;

private final OrderAdminDetailService orderAdminDetailService;
private final TaskAdminDetailService taskAdminDetailService;
Expand All @@ -96,6 +100,7 @@ public class AdminDispatchController {

public AdminDispatchController(OrderAdminQueryService orderAdminQueryService,
OrderQueryService orderQueryService,
OrderStateService orderStateService,
OrderAdminDetailService orderAdminDetailService,
TaskAdminDetailService taskAdminDetailService,
DispatchAdminQueryService dispatchAdminQueryService,
Expand All @@ -113,6 +118,7 @@ public AdminDispatchController(OrderAdminQueryService orderAdminQueryService,
CoordinateTransformService coordinateTransformService) {
this.orderAdminQueryService = orderAdminQueryService;
this.orderQueryService = orderQueryService;
this.orderStateService = orderStateService;
this.orderAdminDetailService = orderAdminDetailService;
this.taskAdminDetailService = taskAdminDetailService;
this.dispatchAdminQueryService = dispatchAdminQueryService;
Expand Down Expand Up @@ -154,6 +160,20 @@ public ApiResponse<OrderDetailResponse> getOrderDetail(@PathVariable Long orderI
return ApiResponse.success(orderAdminDetailService.getEnrichedDetail(orderId));
}

@PostMapping("/orders/{orderId}/cancel")
@Operation(summary = "Cancel order")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Order cancelled"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "Not authenticated")
})
public ApiResponse<Void> cancelOrder(@PathVariable Long orderId,
@RequestBody(required = false) AdminTaskCancelRequest body,
HttpServletRequest request) {
AdminAuthSupport.requireAuth(request);
orderStateService.markCancelled(orderId);
return ApiResponse.success(null);
}

@GetMapping("/tasks")
@Operation(summary = "List all dispatch tasks")
public ApiResponse<List<DispatchTaskListItemResponse>> listTasks(HttpServletRequest request) {
Expand Down Expand Up @@ -301,6 +321,20 @@ public ApiResponse<AdminBatchTaskResultResponse> batchReassign(@Valid @RequestBo
return ApiResponse.success(batchTaskAdminService.batchReassign(request, operator.id(), operator.name()));
}

@PostMapping("/tasks/batch/unassign")
@Operation(summary = "Batch unassign tasks")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Batch result returned"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Validation error"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "Not authenticated")
})
public ApiResponse<AdminBatchTaskResultResponse> batchUnassign(@Valid @RequestBody AdminBatchTaskRequest request,
HttpServletRequest httpRequest) {
AdminAuthSupport.requireAuth(httpRequest);
OperatorIdentity operator = resolveOperator(httpRequest);
return ApiResponse.success(batchTaskAdminService.batchUnassign(request, operator.id(), operator.name()));
}

@GetMapping("/exceptions")
@Operation(summary = "List dispatch exceptions")
public ApiResponse<List<DispatchExceptionListItemResponse>> listExceptions(HttpServletRequest request) {
Expand Down Expand Up @@ -355,6 +389,21 @@ public ApiResponse<Void> resolveException(@PathVariable Long exceptionId,
@Valid @RequestBody AdminDispatchExceptionResolveRequest request,
HttpServletRequest httpRequest) {
AdminAuthSupport.requireAuth(httpRequest);
DispatchExceptionRecordEntity exception = dispatchExceptionService.getException(exceptionId);
if ("RESOLVED".equals(exception.getExceptionStatus())) {
throw new BusinessException("DISPATCH_EXCEPTION_ALREADY_RESOLVED", "Dispatch exception already resolved");
}
if ("REASSIGN".equals(request.getAction())) {
if (request.getVehicleId() == null) {
throw new BusinessException("DISPATCH_EXCEPTION_VEHICLE_REQUIRED", "Reassign action requires vehicleId");
}
DispatchTaskManualAssignRequest assignRequest = new DispatchTaskManualAssignRequest();
assignRequest.setVehicleId(request.getVehicleId());
assignRequest.setOperatorId(request.getResolverId());
assignRequest.setOperatorName(request.getResolverName());
assignRequest.setRemark(request.getRemark());
dispatchTaskService.reassignTask(exception.getTaskId(), assignRequest);
}
dispatchExceptionService.resolveException(exceptionId, request.toDispatchRequest());
return ApiResponse.success(null);
}
Expand Down Expand Up @@ -393,17 +442,19 @@ public ApiResponse<AdminDashboardSummaryResponse> getDashboardSummary(
}

@GetMapping("/parks")
@Operation(summary = "List parks")
@Operation(summary = "List parks", description = "Admin token or X-Mobile-Api-Key for mobile order page")
@SecurityRequirement(name = "")
public ApiResponse<List<ParkResponse>> listParks(HttpServletRequest request) {
AdminAuthSupport.requireAuth(request);
requireAdminOrMobileOrderKey(request);
return ApiResponse.success(parkPilotService.listParks());
}

@GetMapping("/park/layout")
@Operation(summary = "Get park layout", description = "Map layout with stations, roads, and vehicle positions")
@SecurityRequirement(name = "")
public ApiResponse<ParkLayoutResponse> getParkLayout(@RequestParam(required = false) Long parkId,
HttpServletRequest request) {
AdminAuthSupport.requireAuth(request);
requireAdminOrMobileOrderKey(request);
if (parkId == null) {
return ApiResponse.success(parkPilotService.getLayout());
}
Expand Down Expand Up @@ -466,10 +517,11 @@ public ApiResponse<List<ParkVehicleSnapshotResponse>> listParkVehicles(HttpServl
}

@GetMapping("/park/geofences")
@Operation(summary = "List park geofences")
@Operation(summary = "List park geofences", description = "Admin token or X-Mobile-Api-Key for mobile order page")
@SecurityRequirement(name = "")
public ApiResponse<List<ParkGeofenceResponse>> listParkGeofences(@RequestParam(required = false) Long parkId,
HttpServletRequest request) {
AdminAuthSupport.requireAuth(request);
requireAdminOrMobileOrderKey(request);
return ApiResponse.success(parkPilotService.listGeofences(parkId));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import com.fsd.admin.auth.AdminAuthSupport;
import com.fsd.admin.service.SystemHealthAdminService;
import com.fsd.admin.vo.AdminDetailedMetricsResponse;
import com.fsd.admin.vo.AdminHealthTimelineResponse;
import com.fsd.admin.vo.AdminSystemHealthResponse;
import com.fsd.common.model.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -35,4 +38,20 @@ public ApiResponse<AdminSystemHealthResponse> health(HttpServletRequest request)
AdminAuthSupport.requireAuth(request);
return ApiResponse.success(systemHealthAdminService.getHealth());
}

@GetMapping("/health/metrics")
@Operation(summary = "System detailed metrics")
public ApiResponse<AdminDetailedMetricsResponse> metrics(HttpServletRequest request) {
AdminAuthSupport.requireAuth(request);
return ApiResponse.success(systemHealthAdminService.getDetailedMetrics());
}

@GetMapping("/health/timeline")
@Operation(summary = "System health timeline")
public ApiResponse<AdminHealthTimelineResponse> timeline(HttpServletRequest request) {
AdminAuthSupport.requireAuth(request);
return ApiResponse.success(AdminHealthTimelineResponse.builder()
.items(List.of())
.build());
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.fsd.admin.controller;

import com.fsd.admin.service.SystemHealthAdminService;
import com.fsd.admin.vo.AdminSystemHealthResponse;
import com.fsd.common.model.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDateTime;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -13,12 +17,33 @@
@Tag(name = "Health", description = "Public liveness probe")
public class HealthController {

@GetMapping
private final SystemHealthAdminService systemHealthAdminService;

public HealthController(SystemHealthAdminService systemHealthAdminService) {
this.systemHealthAdminService = systemHealthAdminService;
}

@GetMapping({"", "/live"})
public ApiResponse<Map<String, Object>> health() {
return ApiResponse.success(Map.of(
"service", "fsd-core-server",
"status", "UP",
"probe", "liveness",
"timestamp", LocalDateTime.now().toString()
));
}

@GetMapping("/ready")
public ResponseEntity<ApiResponse<Map<String, Object>>> readiness() {
AdminSystemHealthResponse health = systemHealthAdminService.getHealth();
Map<String, Object> payload = Map.of(
"service", "fsd-core-server",
"status", health.getOverallStatus(),
"probe", "readiness",
"checkedAt", health.getCheckedAt().toString(),
"components", health.getComponents()
);
HttpStatus httpStatus = "DOWN".equals(health.getOverallStatus()) ? HttpStatus.SERVICE_UNAVAILABLE : HttpStatus.OK;
return ResponseEntity.status(httpStatus).body(ApiResponse.success(payload));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ public class AdminDispatchExceptionResolveRequest {

private String remark;

private Long vehicleId;

public DispatchExceptionResolveRequest toDispatchRequest() {
DispatchExceptionResolveRequest request = new DispatchExceptionResolveRequest();
request.setResolverId(resolverId);
request.setResolverName(resolverName);
request.setAction(action);
request.setRemark(remark);
request.setVehicleId(vehicleId);
return request;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fsd.admin.metrics;

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -50,4 +51,14 @@ public synchronized long getRecentAverageDurationMs() {
}
return sum / sampleSize;
}

public synchronized long getRecentPercentileDurationMs(double percentile) {
if (sampleSize == 0) {
return 0;
}
long[] samples = Arrays.copyOf(recentSamples, sampleSize);
Arrays.sort(samples);
int index = (int) Math.ceil(percentile * sampleSize) - 1;
return samples[Math.max(0, Math.min(index, sampleSize - 1))];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface AdminDispatchStreamService {
void broadcast(String eventName, Object payload);

boolean hasClients();

int getActiveConnectionCount();
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.fsd.admin.service;

import com.fsd.admin.vo.AdminDetailedMetricsResponse;
import com.fsd.admin.vo.AdminSystemHealthResponse;

public interface SystemHealthAdminService {

AdminSystemHealthResponse getHealth();

AdminDetailedMetricsResponse getDetailedMetrics();
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public boolean hasClients() {
return !emitters.isEmpty();
}

@Override
public int getActiveConnectionCount() {
return emitters.size();
}

private void removeEmitter(SseEmitter emitter) {
emitters.remove(emitter);
}
Expand Down
Loading
Loading