feat: support shared FBA appointments and arrival calendar

This commit is contained in:
2026-10-09 11:01:39 +08:00
parent 4f9433c39a
commit e94271706c
34 changed files with 677 additions and 121 deletions
@@ -125,20 +125,31 @@ public class InboundPlanController {
public ApiResponse<FbaAppointmentResponse> batchFbaAppointment(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody BatchFbaAppointmentRequest request) {
String customerId = scope(user, request.customerId());
requireAppointmentPlatform(user);
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
return ApiResponse.success(appointmentService.batchCreate(customerId, operatorId, request));
return ApiResponse.success(appointmentService.batchCreate(operatorId, request));
}
@GetMapping("/fba-appointments/calendar")
@PreAuthorize("hasAuthority('" + OverseasPermissions.APPOINTMENT_LIST + "')")
@Operation(summary = "按日期范围查询 FBA 预约日历", description = "Required authority: " + OverseasPermissions.APPOINTMENT_LIST)
public ApiResponse<java.util.List<FbaAppointmentResponse>> fbaAppointmentCalendar(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate from,
@RequestParam @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE) java.time.LocalDate to) {
requireAppointmentPlatform(user);
return ApiResponse.success(appointmentService.calendar(from, to));
}
@GetMapping("/fba-appointments/{appointmentNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
@PreAuthorize("hasAnyAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "','" + OverseasPermissions.APPOINTMENT_LIST + "')")
/** 查询 FBA 预约及其选中的入仓明细。 */
@Operation(summary = "查询 FBA 预约详情", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
@Operation(summary = "查询 FBA 预约详情", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST + " or " + OverseasPermissions.APPOINTMENT_LIST)
public ApiResponse<FbaAppointmentResponse> fbaAppointmentDetail(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String appointmentNo,
@RequestParam(required = false) String customerId) {
return ApiResponse.success(appointmentService.detail(scope(user, customerId), appointmentNo));
@PathVariable String appointmentNo) {
requireAppointmentPlatform(user);
return ApiResponse.success(appointmentService.detail(appointmentNo));
}
@PutMapping("/fba-appointments/{appointmentNo}")
@@ -148,8 +159,25 @@ public class InboundPlanController {
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String appointmentNo,
@Valid @RequestBody UpdateFbaAppointmentRequest request) {
return ApiResponse.success(appointmentService.update(scope(user, request.customerId()),
appointmentNo, request));
requireAppointmentPlatform(user);
return ApiResponse.success(appointmentService.update(appointmentNo, request));
}
@DeleteMapping("/fba-appointments/{appointmentNo}")
@PreAuthorize("hasAuthority('platform:appointment:delete')")
@Operation(summary = "删除未关联排车的预约", description = "Required authority: platform:appointment:delete")
public ApiResponse<Void> deleteFbaAppointment(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String appointmentNo, @RequestParam int version) {
requireAppointmentPlatform(user);
appointmentService.delete(appointmentNo, version);
return ApiResponse.success(null);
}
private void requireAppointmentPlatform(AuthenticatedUser user) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "预约由平台统一管理");
}
}
private String scope(AuthenticatedUser user, String requestedCustomer) {
@@ -9,12 +9,12 @@ import java.time.LocalDateTime;
import java.util.List;
public record BatchFbaAppointmentRequest(
String customerId,
@NotEmpty @Size(max = 500) List<@NotNull Long> lineIds,
@NotBlank @Size(max = 64) String appointmentNo,
@NotNull LocalDateTime appointmentAt,
@NotNull LocalDateTime expectedLoadingAt,
@NotBlank @Size(max = 32) String appointmentStatus,
@Size(max = 1000) String appointmentRemark
@Size(max = 1000) String appointmentRemark,
LocalDateTime appointmentEndAt
) {
}
@@ -5,7 +5,6 @@ import java.util.List;
public record FbaAppointmentResponse(
Long id,
String customerId,
String appointmentNo,
LocalDateTime appointmentAt,
LocalDateTime expectedLoadingAt,
@@ -16,9 +15,11 @@ public record FbaAppointmentResponse(
String createdBy,
LocalDateTime createdAt,
LocalDateTime updatedAt,
List<Item> items
List<Item> items,
LocalDateTime appointmentEndAt,
Integer version
) {
public record Item(Long id, Long planId, Long lineId, Long orderId, String inboundCode,
String fbaWarehouseCode) {
String fbaWarehouseCode, String customerId) {
}
}
@@ -7,10 +7,11 @@ import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
public record UpdateFbaAppointmentRequest(
String customerId,
@NotNull LocalDateTime appointmentAt,
@NotNull LocalDateTime expectedLoadingAt,
@NotBlank @Size(max = 32) String appointmentStatus,
@Size(max = 1000) String appointmentRemark
@Size(max = 1000) String appointmentRemark,
LocalDateTime appointmentEndAt,
@NotNull @jakarta.validation.constraints.Min(0) Integer version
) {
}
@@ -12,11 +12,14 @@ import java.time.LocalDateTime;
public class InboundFbaAppointment {
@TableId(type = IdType.INPUT)
private Long id;
private String customerId;
private String appointmentNo;
private LocalDateTime appointmentAt;
@com.baomidou.mybatisplus.annotation.TableField(updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.ALWAYS)
private LocalDateTime appointmentEndAt;
private Integer version;
private LocalDateTime expectedLoadingAt;
private String appointmentStatus;
@com.baomidou.mybatisplus.annotation.TableField(updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.ALWAYS)
private String appointmentRemark;
private String fbaWarehouseCode;
private Integer selectedCount;
@@ -13,6 +13,7 @@ public class InboundFbaAppointmentItem {
@TableId(type = IdType.INPUT)
private Long id;
private Long appointmentId;
private String customerId;
private Long planId;
private Long lineId;
private Long orderId;
@@ -11,12 +11,26 @@ import java.util.List;
public interface InboundFbaAppointmentQueryMapper {
@Select("<script>" +
"SELECT l.id AS lineId,l.plan_id AS planId,l.order_id AS orderId,l.inbound_code AS inboundCode," +
"l.fba_warehouse_code AS fbaWarehouseCode " +
"l.fba_warehouse_code AS fbaWarehouseCode,p.customer_id AS customerId " +
"FROM ll_inbound_plan_line l JOIN ll_inbound_plan p ON p.id=l.plan_id " +
"WHERE p.customer_id=#{customerId} AND l.id IN " +
"WHERE l.id IN " +
"<foreach collection='lineIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"ORDER BY l.id FOR UPDATE" +
"</script>")
List<FbaAppointmentLineSnapshot> lockLines(@Param("customerId") String customerId,
@Param("lineIds") List<Long> lineIds);
List<FbaAppointmentLineSnapshot> lockLines(@Param("lineIds") List<Long> lineIds);
@Select("<script>SELECT COUNT(*) FROM ll_inbound_fba_appointment_item i " +
"JOIN ll_inbound_fba_appointment a ON a.id=i.appointment_id " +
"WHERE a.appointment_status NOT IN ('CANCELLED','FAILED') " +
"<if test='excludedId != null'>AND a.id&lt;&gt;#{excludedId} </if>AND i.line_id IN " +
"<foreach collection='lineIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
long countActiveAppointments(@Param("lineIds") List<Long> lineIds, @Param("excludedId") Long excludedId);
@Select("""
SELECT COUNT(*) FROM ll_inbound_fba_appointment_item i
JOIN ll_dispatch_item di ON di.order_id=i.order_id AND di.customer_id=i.customer_id
JOIN ll_dispatch_order d ON d.id=di.dispatch_order_id AND d.customer_id=di.customer_id
WHERE i.appointment_id=#{appointmentId} AND d.status<>'CANCELLED'
""")
long countActiveDispatches(@Param("appointmentId") Long appointmentId);
}
@@ -24,7 +24,7 @@ public interface InboundPlanMapper extends BaseMapper<InboundPlan> {
"<if test='q.fbaWarehouseCode != null and q.fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{q.fbaWarehouseCode}</if>" +
"<if test='q.appointmentStatus != null and q.appointmentStatus != \"\" and q.appointmentStatus != \"NOT_APPOINTED\"'> " +
"AND EXISTS (SELECT 1 FROM ll_inbound_fba_appointment_item ai JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id " +
"WHERE ai.plan_id=p.id AND a.customer_id=#{customerId} AND a.appointment_status=#{q.appointmentStatus})</if>" +
"WHERE ai.plan_id=p.id AND ai.customer_id=#{customerId} AND a.appointment_status=#{q.appointmentStatus})</if>" +
"<if test='q.appointmentStatus == \"NOT_APPOINTED\"'> AND NOT EXISTS (SELECT 1 FROM ll_inbound_fba_appointment_item ai " +
"WHERE ai.plan_id=p.id)</if>" +
"<if test='q.createdFrom != null'> AND p.created_at &gt;=#{q.createdFrom}</if>" +
@@ -18,14 +18,14 @@ public interface InboundPlanQueryMapper {
String FROM_AND_FILTER = " FROM ll_inbound_plan_line l" +
" JOIN ll_inbound_plan p ON p.id=l.plan_id" +
" JOIN ll_order o ON o.id=l.order_id" +
" JOIN ll_order o ON o.id=l.order_id AND o.customer_id=p.customer_id" +
"<if test='customerId != null and customerId != \"\"'> AND p.customer_id=#{customerId} AND o.customer_id=#{customerId}</if>" +
" LEFT JOIN ll_inbound_fba_appointment_item ai ON ai.line_id=l.id" +
" AND ai.id=(SELECT ai2.id FROM ll_inbound_fba_appointment_item ai2" +
" JOIN ll_inbound_fba_appointment a2 ON a2.id=ai2.appointment_id" +
" WHERE ai2.line_id=l.id <if test='customerId != null and customerId != \"\"'> AND a2.customer_id=#{customerId}</if>" +
" WHERE ai2.line_id=l.id AND ai2.customer_id=p.customer_id" +
" ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1)" +
" LEFT JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id <if test='customerId != null and customerId != \"\"'> AND a.customer_id=#{customerId}</if>" +
" LEFT JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id" +
" WHERE 1=1" +
"<if test='q.planNo != null and q.planNo != \"\"'> AND p.plan_no LIKE CONCAT('%',#{q.planNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND l.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
@@ -5,6 +5,7 @@ public record FbaAppointmentLineSnapshot(
Long planId,
Long orderId,
String inboundCode,
String fbaWarehouseCode
String fbaWarehouseCode,
String customerId
) {
}
@@ -20,6 +20,8 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -32,8 +34,29 @@ public class InboundFbaAppointmentService {
@Resource private InboundFbaAppointmentItemMapper itemMapper;
@Resource private InboundFbaAppointmentQueryMapper queryMapper;
@Transactional
public FbaAppointmentResponse batchCreate(String customerId, String operatorId,
/** A bounded calendar read uses two batch queries, never one detail query per appointment. */
@Transactional(readOnly = true)
public List<FbaAppointmentResponse> calendar(LocalDate from, LocalDate to) {
if (from == null || to == null || !to.isAfter(from) || ChronoUnit.DAYS.between(from, to) > 42) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "日历查询范围必须为 1 至 42 天");
}
List<InboundFbaAppointment> appointments = appointmentMapper.selectList(
new LambdaQueryWrapper<InboundFbaAppointment>()
.ge(InboundFbaAppointment::getAppointmentAt, from.atStartOfDay())
.lt(InboundFbaAppointment::getAppointmentAt, to.atStartOfDay())
.orderByAsc(InboundFbaAppointment::getAppointmentAt, InboundFbaAppointment::getId));
if (appointments.isEmpty()) return List.of();
var groupedItems = itemMapper.selectList(new LambdaQueryWrapper<InboundFbaAppointmentItem>()
.in(InboundFbaAppointmentItem::getAppointmentId,
appointments.stream().map(InboundFbaAppointment::getId).toList())
.orderByAsc(InboundFbaAppointmentItem::getLineId))
.stream().collect(java.util.stream.Collectors.groupingBy(InboundFbaAppointmentItem::getAppointmentId));
return appointments.stream().map(appointment -> response(appointment,
groupedItems.getOrDefault(appointment.getId(), List.of()))).toList();
}
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
public FbaAppointmentResponse batchCreate(String operatorId,
BatchFbaAppointmentRequest request) {
List<Long> lineIds = List.copyOf(request.lineIds());
if (new HashSet<>(lineIds).size() != lineIds.size()) {
@@ -42,31 +65,37 @@ public class InboundFbaAppointmentService {
String appointmentNo = requiredTrimmed(request.appointmentNo(), "Appointment number is required");
String status = normalizeStatus(request.appointmentStatus());
String remark = trimToNull(request.appointmentRemark());
validateWindow(request.appointmentAt(), request.appointmentEndAt());
InboundFbaAppointment existing = find(customerId, appointmentNo);
InboundFbaAppointment existing = find(appointmentNo);
if (existing != null) {
return verifyReplay(existing, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark);
return verifyReplay(existing, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark, request.appointmentEndAt());
}
List<FbaAppointmentLineSnapshot> lines = queryMapper.lockLines(customerId, lineIds);
List<FbaAppointmentLineSnapshot> lines = queryMapper.lockLines(lineIds);
if (lines.size() != lineIds.size()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"One or more selected inbound lines do not exist in this tenant");
"所选入仓明细不存在,请刷新后重试");
}
String fbaWarehouseCode = commonFbaWarehouse(lines);
// The row locks serialize concurrent submissions selecting the same inbound lines.
existing = find(customerId, appointmentNo);
existing = find(appointmentNo);
if (existing != null) {
return verifyReplay(existing, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark);
return verifyReplay(existing, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark, request.appointmentEndAt());
}
if (queryMapper.countActiveAppointments(lineIds, null) > 0) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "所选货物已有有效预约,请编辑原预约或先取消");
}
LocalDateTime now = LocalDateTime.now();
InboundFbaAppointment appointment = new InboundFbaAppointment();
appointment.setId(IdWorker.getId());
appointment.setCustomerId(customerId);
appointment.setAppointmentNo(appointmentNo);
appointment.setAppointmentAt(request.appointmentAt());
appointment.setAppointmentEndAt(request.appointmentEndAt());
appointment.setVersion(0);
appointment.setExpectedLoadingAt(request.expectedLoadingAt());
appointment.setAppointmentStatus(status);
appointment.setAppointmentRemark(remark);
@@ -78,9 +107,9 @@ public class InboundFbaAppointmentService {
try {
appointmentMapper.insert(appointment);
} catch (DuplicateKeyException exception) {
InboundFbaAppointment concurrent = find(customerId, appointmentNo);
InboundFbaAppointment concurrent = find(appointmentNo);
if (concurrent != null) {
return verifyReplay(concurrent, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark);
return verifyReplay(concurrent, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark, request.appointmentEndAt());
}
throw exception;
}
@@ -89,6 +118,7 @@ public class InboundFbaAppointmentService {
InboundFbaAppointmentItem item = new InboundFbaAppointmentItem();
item.setId(IdWorker.getId());
item.setAppointmentId(appointment.getId());
item.setCustomerId(line.customerId());
item.setPlanId(line.planId());
item.setLineId(line.lineId());
item.setOrderId(line.orderId());
@@ -101,39 +131,75 @@ public class InboundFbaAppointmentService {
}
@Transactional(readOnly = true)
public FbaAppointmentResponse detail(String customerId, String appointmentNo) {
InboundFbaAppointment appointment = find(customerId,
requiredTrimmed(appointmentNo, "Appointment number is required"));
public FbaAppointmentResponse detail(String appointmentNo) {
InboundFbaAppointment appointment = find(requiredTrimmed(appointmentNo, "Appointment number is required"));
if (appointment == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "FBA appointment not found");
}
return response(appointment, items(appointment.getId()));
}
@Transactional
public FbaAppointmentResponse update(String customerId, String appointmentNo,
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
public FbaAppointmentResponse update(String appointmentNo,
UpdateFbaAppointmentRequest request) {
InboundFbaAppointment appointment = find(customerId,
requiredTrimmed(appointmentNo, "Appointment number is required"));
InboundFbaAppointment appointment = find(requiredTrimmed(appointmentNo, "Appointment number is required"));
if (appointment == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "FBA appointment not found");
}
String status = normalizeStatus(request.appointmentStatus());
validateWindow(request.appointmentAt(), request.appointmentEndAt());
if (request.version() == null || !Objects.equals(appointment.getVersion(), request.version())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "预约已被其他人修改,请重新打开后再保存");
}
List<Long> lineIds = items(appointment.getId()).stream().map(InboundFbaAppointmentItem::getLineId).sorted().toList();
if (!lineIds.isEmpty()) {
queryMapper.lockLines(lineIds);
if (!Set.of("FAILED", "CANCELLED").contains(status)
&& queryMapper.countActiveAppointments(lineIds, appointment.getId()) > 0) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "所选货物已有其他有效预约,不能恢复当前预约");
}
}
appointment.setAppointmentAt(request.appointmentAt());
appointment.setExpectedLoadingAt(request.expectedLoadingAt());
appointment.setAppointmentStatus(normalizeStatus(request.appointmentStatus()));
appointment.setAppointmentStatus(status);
appointment.setAppointmentEndAt(request.appointmentEndAt());
appointment.setVersion(request.version() + 1);
appointment.setAppointmentRemark(trimToNull(request.appointmentRemark()));
appointment.setUpdatedAt(LocalDateTime.now());
appointmentMapper.updateById(appointment);
if (appointmentMapper.update(appointment, new LambdaQueryWrapper<InboundFbaAppointment>()
.eq(InboundFbaAppointment::getId, appointment.getId())
.eq(InboundFbaAppointment::getVersion, request.version())) != 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "预约已被其他人修改,请重新打开后再保存");
}
return response(appointment, items(appointment.getId()));
}
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
public void delete(String appointmentNo, int version) {
InboundFbaAppointment appointment = find(requiredTrimmed(appointmentNo, "预约编号不能为空"));
if (appointment == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "预约不存在");
List<Long> lineIds = items(appointment.getId()).stream().map(InboundFbaAppointmentItem::getLineId).sorted().toList();
if (!lineIds.isEmpty()) queryMapper.lockLines(lineIds);
if (queryMapper.countActiveDispatches(appointment.getId()) > 0) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "预约货物已关联排车,不允许删除,请先处理关联排车");
}
if (appointmentMapper.delete(new LambdaQueryWrapper<InboundFbaAppointment>()
.eq(InboundFbaAppointment::getId, appointment.getId())
.eq(InboundFbaAppointment::getVersion, version)) != 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "预约已被其他人修改,请刷新后重试");
}
itemMapper.delete(new LambdaQueryWrapper<InboundFbaAppointmentItem>()
.eq(InboundFbaAppointmentItem::getAppointmentId, appointment.getId()));
}
private FbaAppointmentResponse verifyReplay(InboundFbaAppointment existing, List<Long> requestedLineIds,
LocalDateTime appointmentAt, LocalDateTime expectedLoadingAt,
String status, String remark) {
String status, String remark, LocalDateTime appointmentEndAt) {
List<InboundFbaAppointmentItem> existingItems = items(existing.getId());
Set<Long> existingLineIds = existingItems.stream()
.map(InboundFbaAppointmentItem::getLineId).collect(java.util.stream.Collectors.toSet());
if (!Objects.equals(existing.getAppointmentAt(), appointmentAt)
|| !Objects.equals(existing.getAppointmentEndAt(), appointmentEndAt)
|| !Objects.equals(existing.getExpectedLoadingAt(), expectedLoadingAt)
|| !Objects.equals(existing.getAppointmentStatus(), status)
|| !Objects.equals(existing.getAppointmentRemark(), remark)
@@ -155,10 +221,9 @@ public class InboundFbaAppointmentService {
return warehouses.get(0);
}
private InboundFbaAppointment find(String customerId, String appointmentNo) {
private InboundFbaAppointment find(String appointmentNo) {
LambdaQueryWrapper<InboundFbaAppointment> wrapper = new LambdaQueryWrapper<InboundFbaAppointment>()
.eq(InboundFbaAppointment::getAppointmentNo, appointmentNo);
if (customerId != null) wrapper.eq(InboundFbaAppointment::getCustomerId, customerId);
return appointmentMapper.selectOne(wrapper);
}
@@ -170,13 +235,20 @@ public class InboundFbaAppointmentService {
private FbaAppointmentResponse response(InboundFbaAppointment appointment,
List<InboundFbaAppointmentItem> items) {
return new FbaAppointmentResponse(appointment.getId(), appointment.getCustomerId(),
return new FbaAppointmentResponse(appointment.getId(),
appointment.getAppointmentNo(), appointment.getAppointmentAt(), appointment.getExpectedLoadingAt(),
appointment.getAppointmentStatus(),
appointment.getAppointmentRemark(), appointment.getFbaWarehouseCode(), appointment.getSelectedCount(),
appointment.getCreatedBy(), appointment.getCreatedAt(), appointment.getUpdatedAt(),
items.stream().map(item -> new FbaAppointmentResponse.Item(item.getId(), item.getPlanId(),
item.getLineId(), item.getOrderId(), item.getInboundCode(), item.getFbaWarehouseCode())).toList());
item.getLineId(), item.getOrderId(), item.getInboundCode(), item.getFbaWarehouseCode(), item.getCustomerId())).toList(),
appointment.getAppointmentEndAt(), appointment.getVersion());
}
private static void validateWindow(LocalDateTime start, LocalDateTime end) {
if (start == null || (end != null && !end.isAfter(start))) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "预约结束时间必须晚于开始时间");
}
}
private static String normalizeStatus(String value) {
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.security;
/** Canonical authorities for overseas browser APIs. */
public final class OverseasPermissions {
public static final String APPOINTMENT_LIST = "platform:appointment:list";
public static final String LAST_MILE_ORDER_LIST = "platform:overseas-warehouse:last-mile-order:list";
public static final String LAST_MILE_ORDER_EDIT = "platform:overseas-warehouse:last-mile-order:edit";
public static final String LAST_MILE_ORDER_EXPORT = "platform:overseas-warehouse:last-mile-order:export";