feat(appointments): add four-type calendar sources and align release contracts
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.terminaldrayage.service.ContainerCalendarSourceService;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/terminal-drayages")
|
||||
public class ContainerCalendarSourceController {
|
||||
private final ContainerCalendarSourceService service;
|
||||
/** 装配只读日历业务服务。 */
|
||||
public ContainerCalendarSourceController(ContainerCalendarSourceService service) { this.service = service; }
|
||||
/** 预约列表可读基础柜信息,还柜事实继续核验原业务读取权限。 */
|
||||
@GetMapping("/calendar-sources")
|
||||
@PreAuthorize("hasAuthority('platform:appointment:list')")
|
||||
public ApiResponse<ContainerCalendarSourceService.Result> calendar(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.calendar(from, to, user.permissions()));
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 四类日历使用的柜级来源,日期均保留原业务当地值。 */
|
||||
public record ContainerCalendarSource(Long containerBookingId, String cabinetNo, String containerNo,
|
||||
String vesselName, String voyageNo, String destinationPortName, String destinationWarehouseCode,
|
||||
LocalDateTime arrivalAt, String arrivalType) { }
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.mapper;
|
||||
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.ContainerCalendarSource;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ContainerCalendarSourceMapper {
|
||||
/** 同时选出到港、缺日期和可能还柜的有效出仓柜,最终日期由业务服务复核。 */
|
||||
@Select("""
|
||||
SELECT c.id containerBookingId,c.cabinet_no cabinetNo,c.container_no containerNo,
|
||||
c.vessel_name vesselName,c.voyage_no voyageNo,c.destination_port_name destinationPortName,
|
||||
c.destination_warehouse_code destinationWarehouseCode,
|
||||
COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at) arrivalAt,
|
||||
CASE WHEN COALESCE(n.actual_at,c.actual_port_arrival_at) IS NOT NULL THEN 'ACTUAL' ELSE 'ESTIMATED' END arrivalType
|
||||
FROM fl_container_booking c
|
||||
LEFT JOIN fl_terminal_drayage t ON t.container_booking_id=c.id
|
||||
LEFT JOIN fl_carrier_schedule_node_fact n ON n.id=(
|
||||
SELECT f.id FROM fl_carrier_schedule_node_fact f WHERE f.container_booking_id=c.id
|
||||
AND f.standard_node='ARRIVED_DESTINATION_PORT' AND f.actual_at IS NOT NULL
|
||||
ORDER BY f.observed_at DESC,f.id DESC LIMIT 1)
|
||||
WHERE c.record_status='ACTIVE'
|
||||
AND EXISTS(SELECT 1 FROM fl_outbound_dispatch d WHERE d.container_booking_id=c.id
|
||||
AND d.confirmed_at IS NOT NULL AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING')
|
||||
AND (
|
||||
COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at) IS NULL
|
||||
OR (COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at)>=#{arrivalFrom}
|
||||
AND COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at)<#{to})
|
||||
OR (t.actual_empty_return_at>=#{from} AND t.actual_empty_return_at<#{to})
|
||||
OR (c.unloading_completed_at>=#{returnFrom} AND c.unloading_completed_at<#{to})
|
||||
OR EXISTS(SELECT 1 FROM fl_carrier_schedule_node_fact r WHERE r.container_booking_id=c.id
|
||||
AND r.standard_node='EMPTY_RETURNED' AND r.actual_at>=#{from} AND r.actual_at<#{to})
|
||||
OR EXISTS(SELECT 1 FROM fl_direct_delivery_unloading u WHERE u.container_booking_id=c.id
|
||||
AND u.actual_at>=#{returnFrom} AND u.actual_at<#{to})
|
||||
)
|
||||
ORDER BY c.id LIMIT 2001
|
||||
""")
|
||||
List<ContainerCalendarSource> sources(@Param("from") LocalDateTime from,
|
||||
@Param("arrivalFrom") LocalDateTime arrivalFrom, @Param("returnFrom") LocalDateTime returnFrom,
|
||||
@Param("to") LocalDateTime to);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.repository;
|
||||
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.ContainerCalendarSource;
|
||||
import com.baiyue.firstleg.terminaldrayage.mapper.ContainerCalendarSourceMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class ContainerCalendarSourceRepository {
|
||||
private final ContainerCalendarSourceMapper mapper;
|
||||
/** 使用本服务 Mapper 读取日历所需柜级来源。 */
|
||||
public ContainerCalendarSourceRepository(ContainerCalendarSourceMapper mapper) { this.mapper = mapper; }
|
||||
/** 到港前移两天覆盖默认到仓,直送还柜候选额外留一天覆盖夏令时。 */
|
||||
public List<ContainerCalendarSource> sources(LocalDate from, LocalDate to) {
|
||||
return mapper.sources(from.atStartOfDay(), from.minusDays(2).atStartOfDay(),
|
||||
from.minusDays(4).atStartOfDay(), to.atStartOfDay());
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.service;
|
||||
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.ContainerCalendarSource;
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.View;
|
||||
import com.baiyue.firstleg.terminaldrayage.repository.ContainerCalendarSourceRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@Service
|
||||
public class ContainerCalendarSourceService {
|
||||
public record Result(List<ContainerCalendarSource> containers, List<View> emptyReturns,
|
||||
boolean emptyReturnsReadable) { }
|
||||
private final ContainerCalendarSourceRepository repository;
|
||||
private final TerminalDrayageService drayages;
|
||||
/** 复用真实还柜业务的截止时间和完成事实,不在日历另算还柜状态。 */
|
||||
public ContainerCalendarSourceService(ContainerCalendarSourceRepository repository, TerminalDrayageService drayages) {
|
||||
this.repository = repository; this.drayages = drayages;
|
||||
}
|
||||
/** 按日期范围读取到港、默认到仓来源及可见的还柜记录。 */
|
||||
public Result calendar(LocalDate from, LocalDate to, Set<String> permissions) {
|
||||
if (from == null || to == null || !to.isAfter(from) || ChronoUnit.DAYS.between(from, to) > 42)
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "日历查询范围必须为1至42天");
|
||||
var sources = repository.sources(from, to);
|
||||
if (sources.size() > 2000)
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "日历来源超过2000柜,请缩小范围或补齐未定日期");
|
||||
boolean readable = permissions != null && permissions.contains("platform:firstleg:terminal-drayage:list");
|
||||
var returns = new ArrayList<View>();
|
||||
if (readable) {
|
||||
var ids = sources.stream().map(ContainerCalendarSource::containerBookingId).toList();
|
||||
for (int start = 0; start < ids.size(); start += 200) {
|
||||
for (var row : drayages.authorize(drayages.views(ids.subList(start, Math.min(start + 200, ids.size()))), permissions)) {
|
||||
var at = row.actualEmptyReturnAt() != null ? row.actualEmptyReturnAt() : row.emptyReturnWarning().deadlineAt();
|
||||
if (at != null && !at.toLocalDate().isBefore(from) && at.toLocalDate().isBefore(to)) returns.add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 未授权还柜读取时,也不能通过仅因还柜日期命中的旧柜候选推断受保护事实。
|
||||
var visibleSources = readable ? sources : sources.stream().filter(row -> row.arrivalAt() == null
|
||||
|| !row.arrivalAt().toLocalDate().isBefore(from.minusDays(2)) && row.arrivalAt().toLocalDate().isBefore(to)).toList();
|
||||
return new Result(List.copyOf(visibleSources), List.copyOf(returns), readable);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user