feat: support shared FBA appointments and arrival calendar
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
|
||||
## 模块入口
|
||||
|
||||
预约日历到港只读:`portschedule/controller/PlatformPortScheduleController.java` 的 `/arrival-calendar`,沿 `PortScheduleService`、`PortScheduleMapper` 查询有效柜次及实际到港节点;合同 `src/main/resources/openapi/arrival-calendar-v1.yaml`,权限 `platform:appointment:list`。不在日历维护第二套船节点。
|
||||
|
||||
表内 `controller/`、`service/`、`mapper/`、`loading/` 等业务包路径相对 `src/main/java/com/baiyue/firstleg/`;`src/`、`docs/`、`scripts/` 和 `../` 开头路径相对本服务目录。裸文件名沿同一条目最近业务包定位,必要时在该包用 `rg --files` 查找,不从全仓猜路径。
|
||||
|
||||
事件人工重试:`controller/EventOperationsController.java` 提供内部 `/internal/firstleg/event-operations/{entryId}` 查询及 `/retry` 写入,只允许装柜完成与船节点事件;独立 `EVENT_OPERATIONS_TOKEN` 保护。原事件不可修改,DEAD→PENDING 与 `event_delivery_retry_audit` 同事务提交,脚本见根 `scripts/event-outbox-operations.mjs`。
|
||||
|
||||
+12
@@ -37,6 +37,18 @@ import java.util.List;
|
||||
public class PlatformPortScheduleController {
|
||||
@Resource private PortScheduleService service;
|
||||
|
||||
@GetMapping("/arrival-calendar")
|
||||
@PreAuthorize("hasAuthority('platform:appointment:list')")
|
||||
public ApiResponse<List<com.baiyue.firstleg.portschedule.dto.ArrivalCalendarEntry>> arrivalCalendar(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@org.springframework.web.bind.annotation.RequestParam
|
||||
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate from,
|
||||
@org.springframework.web.bind.annotation.RequestParam
|
||||
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate to) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.arrivalCalendar(from, to));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule:list')")
|
||||
public ApiResponse<PageResult<ScheduleView>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.baiyue.firstleg.portschedule.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record ArrivalCalendarEntry(Long containerBookingId, String bookingNo, String containerNo,
|
||||
String vesselName, String voyageNo, String destinationPortName, String destinationWarehouseCode,
|
||||
LocalDateTime arrivalAt, String arrivalType) {}
|
||||
+20
@@ -11,6 +11,26 @@ import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface PortScheduleMapper extends BaseMapper<PortScheduleEntity> {
|
||||
@Select("""
|
||||
SELECT c.id containerBookingId,c.booking_no bookingNo,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_carrier_schedule_node_fact n ON n.container_booking_id=c.id
|
||||
AND n.standard_node='ARRIVED_DESTINATION_PORT' AND n.actual_at IS NOT NULL
|
||||
AND n.id=(SELECT n2.id FROM fl_carrier_schedule_node_fact n2
|
||||
WHERE n2.container_booking_id=c.id AND n2.standard_node='ARRIVED_DESTINATION_PORT'
|
||||
AND n2.actual_at IS NOT NULL ORDER BY n2.observed_at DESC,n2.id DESC LIMIT 1)
|
||||
WHERE c.record_status='ACTIVE'
|
||||
AND COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at)>=#{from}
|
||||
AND COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at)<#{to}
|
||||
ORDER BY arrivalAt,c.id
|
||||
""")
|
||||
java.util.List<com.baiyue.firstleg.portschedule.dto.ArrivalCalendarEntry> arrivalCalendar(
|
||||
@Param("from") java.time.LocalDateTime from, @Param("to") java.time.LocalDateTime to);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT * FROM fl_port_schedule
|
||||
|
||||
+4
@@ -35,6 +35,10 @@ public class PortScheduleRepository {
|
||||
return PageResult.from(schedules.selectPage(new Page<>(query.page(), query.size()), query));
|
||||
}
|
||||
public PortScheduleEntity find(long id) { return schedules.selectById(id); }
|
||||
public List<com.baiyue.firstleg.portschedule.dto.ArrivalCalendarEntry> arrivalCalendar(
|
||||
java.time.LocalDateTime from, java.time.LocalDateTime to) {
|
||||
return schedules.arrivalCalendar(from, to);
|
||||
}
|
||||
public PortScheduleEntity latestForDispatch(long customerId, long oceanBookingId, long containerBookingId) {
|
||||
return schedules.latestForDispatch(customerId, oceanBookingId, containerBookingId);
|
||||
}
|
||||
|
||||
+9
@@ -48,6 +48,15 @@ public class PortScheduleService {
|
||||
return detail(required(id));
|
||||
}
|
||||
|
||||
public java.util.List<com.baiyue.firstleg.portschedule.dto.ArrivalCalendarEntry> arrivalCalendar(
|
||||
java.time.LocalDate from, java.time.LocalDate to) {
|
||||
if (from == null || to == null || !to.isAfter(from)
|
||||
|| java.time.temporal.ChronoUnit.DAYS.between(from, to) > 42) {
|
||||
throw badRequest("日历查询范围必须为1至42天");
|
||||
}
|
||||
return repository.arrivalCalendar(from.atStartOfDay(), to.atStartOfDay());
|
||||
}
|
||||
|
||||
public java.util.List<PortScheduleHistoryEntity> history(long id) {
|
||||
required(id);
|
||||
return repository.histories(id);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 预约日历到港信息
|
||||
version: 1.0.0
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/api/platform/v1/port-schedules/arrival-calendar:
|
||||
get:
|
||||
summary: 查询日期范围内货柜到港信息
|
||||
description: '仅平台,权限platform:appointment:list。按有效柜次返回一条记录;实际到港优先取最新已观测实际船节点,其次柜次实际到港,再取预计到港。未提供日期的柜次不伪造日期。区间左闭右开,最多42天。'
|
||||
parameters:
|
||||
- {name: from, in: query, required: true, schema: {type: string, format: date}}
|
||||
- {name: to, in: query, required: true, schema: {type: string, format: date}}
|
||||
responses:
|
||||
'200':
|
||||
description: 业务响应,成功code为字符串200。
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
containerBookingId: {type: string}
|
||||
bookingNo: {type: string}
|
||||
containerNo: {type: string, nullable: true}
|
||||
vesselName: {type: string, nullable: true}
|
||||
voyageNo: {type: string, nullable: true}
|
||||
destinationPortName: {type: string, nullable: true}
|
||||
destinationWarehouseCode: {type: string, nullable: true}
|
||||
arrivalAt: {type: string}
|
||||
arrivalType: {type: string, enum: [ACTUAL, ESTIMATED]}
|
||||
'403': {description: 无权限或非平台身份}
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.firstleg.portschedule.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.portschedule.repository.PortScheduleRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ArrivalCalendarTest {
|
||||
@Test
|
||||
void rejectsInvalidRangesAndPassesTheWholeHalfOpenRangeToOneQuery() {
|
||||
var repository = mock(PortScheduleRepository.class);
|
||||
var service = new PortScheduleService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
var from = LocalDate.of(2026, 9, 28);
|
||||
assertThrows(BizException.class, () -> service.arrivalCalendar(from, from));
|
||||
assertThrows(BizException.class, () -> service.arrivalCalendar(from, from.plusDays(43)));
|
||||
verifyNoInteractions(repository);
|
||||
when(repository.arrivalCalendar(from.atStartOfDay(), from.plusDays(42).atStartOfDay())).thenReturn(List.of());
|
||||
assertTrue(service.arrivalCalendar(from, from.plusDays(42)).isEmpty());
|
||||
verify(repository).arrivalCalendar(from.atStartOfDay(), from.plusDays(42).atStartOfDay());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user