This commit is contained in:
2026-08-08 13:32:20 +08:00
parent 96788e10de
commit 3203adb5a7
73 changed files with 44331 additions and 26 deletions
@@ -0,0 +1,10 @@
package com.baiyue.firstleg.controller;
import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.core.api.*; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.firstleg.dto.TruckAppointmentDtos.*; import com.baiyue.firstleg.entity.TruckAppointmentEntity; import com.baiyue.firstleg.service.*; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime;
@Tag(name="拖车预约") @Validated @RestController @RequestMapping("/api/platform/v1/truck-appointments") public class PlatformTruckAppointmentController { @Resource TruckAppointmentService service;
@GetMapping @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse<PageResult<TruckAppointmentEntity>> page(@AuthenticationPrincipal AuthenticatedUser u,@Valid Query q){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.page(q));}
@GetMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:view')") public ApiResponse<TruckAppointmentEntity> detail(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.detail(id));}
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:create')") @BusinessAudit(event="TRUCK_APPOINTMENT_CREATE",resourceType="TRUCK_APPOINTMENT",captureBefore=false) public ApiResponse<TruckAppointmentEntity> create(@AuthenticationPrincipal AuthenticatedUser u,@Valid @RequestBody SaveRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.create(u.userId(),u.username(),r));}
@PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse<TruckAppointmentEntity> edit(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable long id,@Valid @RequestBody SaveRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.edit(id,u.userId(),u.username(),r));}
@PostMapping("/{id}/{action}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:operate')") @BusinessAudit(event="TRUCK_APPOINTMENT_ACTION",resourceType="TRUCK_APPOINTMENT") public ApiResponse<TruckAppointmentEntity> action(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable long id,@PathVariable String action,@RequestHeader(value="Idempotency-Key",required=false) String idem,@RequestHeader(value="X-Request-Hash",required=false) String hash,@Valid @RequestBody ActionRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.action(id,action.toUpperCase(),r,idem,hash,u.userId(),u.username()));}
@PostMapping("/{id}/reschedule") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:operate')") public ApiResponse<TruckAppointmentEntity> reschedule(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable long id,@RequestParam LocalDateTime plannedLoadingAt,@Valid @RequestBody ActionRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.reschedule(id,plannedLoadingAt,r,u.userId(),u.username()));}
}
@@ -0,0 +1,6 @@
package com.baiyue.firstleg.domain;
public enum TruckAppointmentStatus {
DRAFT, PENDING_DISPATCH, DISPATCHED, PICKUP_IN_PROGRESS, ARRIVED_WAREHOUSE,
LOADING, DEPARTED, COMPLETED, RESCHEDULED, EXCEPTION, CANCELLED
}
@@ -0,0 +1,7 @@
package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime;
public final class TruckAppointmentDtos { private TruckAppointmentDtos(){}
public record Query(@Size(max=64) String keyword, com.baiyue.firstleg.domain.TruckAppointmentStatus status, @Positive Long containerBookingId, @Min(1) long page, @Min(1) @Max(200) long size){ public Query{if(page==0)page=1;if(size==0)size=20;} }
public record SaveRequest(@Positive Long outboundOrderId,@Positive Long oceanBookingId,@Positive Long containerBookingId,@Positive Long loadingPlanId,@Positive Long warehouseId,@Positive Long customerId,@Size(max=128) String customerName,@Positive Long carrierId,@Size(max=128) String carrierName,@Size(max=64) String fleetContact,@Size(max=32) String fleetPhone,@Size(max=64) String driverName,@Size(max=32) String driverPhone,@Size(max=32) String driverIdCard,@Size(max=32) String vehicleNo,@Size(max=128) String storeName,@Size(max=64) String storeContact,@Size(max=32) String storePhone,@Size(max=255) String storeAddress,@Size(max=64) String storeCity,@Size(max=16) String storeZipcode,@PositiveOrZero Integer packageCount,@DecimalMin("0") BigDecimal grossWeightKg,@DecimalMin("0") BigDecimal volumeCbm,@PositiveOrZero Integer palletCount,@Size(max=255) String poNo,@NotNull LocalDateTime plannedLoadingAt,@PositiveOrZero Integer transitMinutes,@DecimalMin("0") BigDecimal payableAmount,@DecimalMin("0") BigDecimal receivableAmount,@Size(max=8) String currency,@Size(max=1000) String remark,@NotNull @PositiveOrZero Integer expectedVersion){}
public record ActionRequest(@NotNull @PositiveOrZero Integer expectedVersion,@Size(max=500) String reason){}
}
@@ -0,0 +1,5 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime;
@Getter @Setter @TableName("fl_truck_appointment_attachment") public class TruckAppointmentAttachmentEntity {
@TableId(type=IdType.ASSIGN_ID) private Long id; private Long appointmentId; private Long fileId; private String attachmentType; private String fileName; private String fileHash; private String visibility; private Long uploadedBy; private LocalDateTime uploadedAt;
}
@@ -0,0 +1,54 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import com.baiyue.firstleg.domain.TruckAppointmentStatus;
@Getter @Setter @TableName("fl_truck_appointment")
public class TruckAppointmentEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String appointmentNo;
private Long outboundOrderId;
private Long oceanBookingId;
private Long containerBookingId;
private Long loadingPlanId;
private Long warehouseId;
private Long customerId;
private String customerName;
private Long carrierId;
private String carrierName;
private String fleetContact;
private String fleetPhone;
private String driverName;
private String driverPhone;
private String driverIdCard;
private String vehicleNo;
private String storeName;
private String storeContact;
private String storePhone;
private String storeAddress;
private String storeCity;
private String storeZipcode;
private Integer packageCount;
private BigDecimal grossWeightKg;
private BigDecimal volumeCbm;
private Integer palletCount;
private String poNo;
private LocalDateTime plannedLoadingAt;
private LocalDateTime actualLoadingAt;
private Integer transitMinutes;
private BigDecimal payableAmount;
private BigDecimal receivableAmount;
private String currency;
private TruckAppointmentStatus status;
private Integer revision;
private Integer version;
private String remark;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,5 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime;
@Getter @Setter @TableName("fl_truck_appointment_history") public class TruckAppointmentHistoryEntity {
@TableId(type=IdType.ASSIGN_ID) private Long id; private Long appointmentId; private Integer revision; private String action; private String fromStatus; private String toStatus; private String reason; private Long operatorId; private String operatorName; private String requestId; private LocalDateTime createdAt;
}
@@ -0,0 +1,5 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime;
@Getter @Setter @TableName("fl_truck_appointment_operation") public class TruckAppointmentOperationEntity {
@TableId(type=IdType.ASSIGN_ID) private Long id; private Long appointmentId; private String idempotencyKey; private String requestHash; private String action; private String responseSnapshot; private LocalDateTime createdAt;
}
@@ -0,0 +1 @@
package com.baiyue.firstleg.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.TruckAppointmentAttachmentEntity; import org.apache.ibatis.annotations.Mapper; @Mapper public interface TruckAppointmentAttachmentMapper extends BaseMapper<TruckAppointmentAttachmentEntity>{}
@@ -0,0 +1 @@
package com.baiyue.firstleg.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.TruckAppointmentHistoryEntity; import org.apache.ibatis.annotations.Mapper; @Mapper public interface TruckAppointmentHistoryMapper extends BaseMapper<TruckAppointmentHistoryEntity>{}
@@ -0,0 +1,8 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.dto.TruckAppointmentDtos.Query; import com.baiyue.firstleg.entity.TruckAppointmentEntity; import org.apache.ibatis.annotations.*;
@Mapper public interface TruckAppointmentMapper extends BaseMapper<TruckAppointmentEntity>{
@Select("<script>SELECT * FROM fl_truck_appointment WHERE (#{q.keyword} IS NULL OR appointment_no LIKE CONCAT('%',#{q.keyword},'%') OR vehicle_no LIKE CONCAT('%',#{q.keyword},'%') OR carrier_name LIKE CONCAT('%',#{q.keyword},'%')) AND (#{q.status} IS NULL OR status=#{q.status}) AND (#{q.containerBookingId} IS NULL OR container_booking_id=#{q.containerBookingId}) ORDER BY updated_at DESC,id DESC</script>") Page<TruckAppointmentEntity> page(Page<TruckAppointmentEntity> p,@Param("q") Query q);
@Update("UPDATE fl_truck_appointment SET status=#{toStatus}, revision=revision+1, version=version+1, updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int transition(@Param("id") long id,@Param("fromStatus") String from,@Param("toStatus") String to,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET planned_loading_at=#{at}, revision=revision+1, version=version+1, status='RESCHEDULED', remark=#{reason}, updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND version=#{version}") int reschedule(@Param("id") long id,@Param("at") java.time.LocalDateTime at,@Param("reason") String reason,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET vehicle_no=#{e.vehicleNo},driver_name=#{e.driverName},driver_phone=#{e.driverPhone},payable_amount=#{e.payableAmount},receivable_amount=#{e.receivableAmount},planned_loading_at=#{e.plannedLoadingAt},remark=#{e.remark},version=version+1,revision=revision+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{e.id} AND version=#{version}") int updateEdit(@Param("e") TruckAppointmentEntity e,@Param("version") int version);
}
@@ -0,0 +1 @@
package com.baiyue.firstleg.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.TruckAppointmentOperationEntity; import org.apache.ibatis.annotations.Mapper; @Mapper public interface TruckAppointmentOperationMapper extends BaseMapper<TruckAppointmentOperationEntity>{}
@@ -0,0 +1,65 @@
package com.baiyue.firstleg.oceanbooking.controller;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import com.baiyue.firstleg.oceanbooking.service.OceanBookingService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Validated
@RestController
@Tag(name = "海运订舱")
@RequestMapping("/api/platform/v1/ocean-bookings")
public class PlatformOceanBookingController {
@Resource private OceanBookingService service;
@GetMapping @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
public ApiResponse<PageResult<OceanBookingEntity>> page(@AuthenticationPrincipal AuthenticatedUser user, @Valid Query query) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.page(query)); }
@GetMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:view')")
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(oceanBookingId)); }
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:create')")
@BusinessAudit(event="OCEAN_BOOKING_CREATE", resourceType="OCEAN_BOOKING", description="创建海运订舱单", captureBefore=false)
public ApiResponse<Detail> create(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CreateRequest request) { FirstlegScopeGuard.platform(user, request.data().customerId()); return ApiResponse.success(service.create(user.userId(), user.username(), key, request)); }
@PutMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:edit')")
@BusinessAudit(event="OCEAN_BOOKING_UPDATE", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="编辑海运订舱草稿")
public ApiResponse<Detail> updateDraft(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody UpdateDraftRequest request) { FirstlegScopeGuard.platform(user, request.data().customerId()); return ApiResponse.success(service.updateDraft(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/submit") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:submit')")
@BusinessAudit(event="OCEAN_BOOKING_SUBMIT", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="提交海运订舱")
public ApiResponse<Detail> submit(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.submit(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/confirm") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:confirm')")
@BusinessAudit(event="OCEAN_BOOKING_CONFIRM", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认海运订舱")
public ApiResponse<Detail> confirm(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.confirm(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/release") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:release')")
@BusinessAudit(event="OCEAN_BOOKING_RELEASE", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认放舱")
public ApiResponse<Detail> release(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.release(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/fail") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:confirm')")
@BusinessAudit(event="OCEAN_BOOKING_FAIL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="标记海运订舱失败")
public ApiResponse<Detail> fail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.fail(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/cancel") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:cancel')")
@BusinessAudit(event="OCEAN_BOOKING_CANCEL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="取消海运订舱")
public ApiResponse<Detail> cancel(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.cancel(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/change-request") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_REQUEST", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="申请变更船期")
public ApiResponse<Detail> requestChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ChangeRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.requestChange(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/change-confirm") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_CONFIRM", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认变更船期")
public ApiResponse<Detail> confirmChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.confirmChange(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/change-fail") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_FAIL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="标记变更船期失败")
public ApiResponse<Detail> failChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.failChange(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/allocations") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:allocate')")
@BusinessAudit(event="OCEAN_BOOKING_ALLOCATE_CONTAINER", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="分配订舱柜次")
public ApiResponse<Detail> allocate(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody AllocationRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.allocate(oceanBookingId, user.userId(), user.username(), key, request)); }
}
@@ -0,0 +1,11 @@
package com.baiyue.firstleg.oceanbooking.domain;
public enum OceanBookingStatus {
DRAFT,
SUBMITTED,
CONFIRMED,
RELEASED,
FAILED,
CHANGE_PENDING,
CANCELLED
}
@@ -0,0 +1,81 @@
package com.baiyue.firstleg.oceanbooking.dto;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingPartyEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingStatusHistoryEntity;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
public final class OceanBookingDtos {
private OceanBookingDtos() {}
public record Query(@Positive Long customerId, @Size(max = 24) String status,
@Size(max = 128) String keyword, @Min(1) long page,
@Min(1) @Max(200) long size) {
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
}
public record PartyRequest(@NotBlank @Size(max = 24) String partyType,
@NotBlank @Size(max = 255) String partyName,
@Size(max = 1000) String address,
@Size(max = 128) String contactName,
@Size(max = 64) String contactPhone) {}
public record ContainerRequirementRequest(@NotBlank @Size(max = 32) String containerType,
@NotBlank @Size(max = 32) String containerKind,
@NotNull @Positive Integer quantity) {}
public record DraftData(@NotNull @Positive Long customerId,
@NotBlank @Size(max = 128) String customerName,
@Size(max = 128) String billOfLadingNo,
@Positive Long shippingCompanyId,
@Size(max = 128) String shippingCompanyName,
@Positive Long bookingAgentId,
@Size(max = 128) String bookingAgentName,
@Size(max = 128) String vesselName,
@Size(max = 64) String voyageNo,
@Size(max = 32) String departurePortCode,
@Size(max = 128) String departurePortName,
@Size(max = 32) String destinationPortCode,
@Size(max = 128) String destinationPortName,
@Size(max = 128) String placeOfDelivery,
@Size(max = 32) String serviceTerm,
@Size(max = 32) String freightTerm,
LocalDateTime estimatedSailingAt,
LocalDateTime estimatedArrivalAt,
@Size(max = 1000) String remark,
@Valid @NotEmpty @Size(max = 3) List<PartyRequest> parties,
@Valid @NotEmpty @Size(max = 20) List<ContainerRequirementRequest> containerRequirements) {}
public record CreateRequest(@NotNull @Min(0) Integer expectedVersion,
@Valid @NotNull DraftData data) {}
public record UpdateDraftRequest(@NotNull @Min(0) Integer expectedVersion,
@Valid @NotNull DraftData data) {}
public record ActionRequest(@NotNull @Min(0) Integer expectedVersion,
@Size(max = 1000) String reason) {}
public record ChangeRequest(@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 128) String vesselName,
@NotBlank @Size(max = 64) String voyageNo,
LocalDateTime estimatedSailingAt,
LocalDateTime estimatedArrivalAt,
@NotBlank @Size(max = 1000) String reason) {}
public record AllocationRequest(@NotNull @Positive Long requirementId,
@NotNull @Positive Long containerBookingId,
@NotNull @Min(0) Integer expectedVersion) {}
public record Detail(OceanBookingEntity oceanBooking,
List<OceanBookingPartyEntity> parties,
List<OceanBookingContainerRequirementEntity> containerRequirements,
List<OceanBookingContainerAllocationEntity> containerAllocations,
List<OceanBookingStatusHistoryEntity> histories) {}
}
@@ -0,0 +1,24 @@
package com.baiyue.firstleg.oceanbooking.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@TableName("fl_ocean_booking_container_allocation")
public class OceanBookingContainerAllocationEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long oceanBookingId;
private Long requirementId;
private Long containerBookingId;
private Long allocatedBy;
private String allocatedByName;
@TableField(fill = FieldFill.INSERT) private LocalDateTime allocatedAt;
}
@@ -0,0 +1,19 @@
package com.baiyue.firstleg.oceanbooking.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName("fl_ocean_booking_container_requirement")
public class OceanBookingContainerRequirementEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long oceanBookingId;
private String containerType;
private String containerKind;
private Integer quantity;
private Integer sortNo;
}
@@ -0,0 +1,54 @@
package com.baiyue.firstleg.oceanbooking.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@TableName("fl_ocean_booking")
public class OceanBookingEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String bookingNo;
private String status;
private Long customerId;
private String customerName;
private String billOfLadingNo;
private Long shippingCompanyId;
private String shippingCompanyName;
private Long bookingAgentId;
private String bookingAgentName;
private String vesselName;
private String voyageNo;
private String departurePortCode;
private String departurePortName;
private String destinationPortCode;
private String destinationPortName;
private String placeOfDelivery;
private String serviceTerm;
private String freightTerm;
private LocalDateTime estimatedSailingAt;
private LocalDateTime estimatedArrivalAt;
private String pendingVesselName;
private String pendingVoyageNo;
private LocalDateTime pendingEstimatedSailingAt;
private LocalDateTime pendingEstimatedArrivalAt;
private String changeReason;
private String failureReason;
private String cancellationReason;
private String remark;
private Long createdBy;
private String createdByName;
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
private Long updatedBy;
private String updatedByName;
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
@Version @TableField(fill = FieldFill.INSERT) private Integer version;
}
@@ -0,0 +1,27 @@
package com.baiyue.firstleg.oceanbooking.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@TableName("fl_ocean_booking_operation")
public class OceanBookingOperationEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long oceanBookingId;
private Long operatorId;
private String operation;
private String idempotencyKey;
private String requestHash;
private String status;
private String resultJson;
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
private LocalDateTime completedAt;
}
@@ -0,0 +1,20 @@
package com.baiyue.firstleg.oceanbooking.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName("fl_ocean_booking_party")
public class OceanBookingPartyEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long oceanBookingId;
private String partyType;
private String partyName;
private String address;
private String contactName;
private String contactPhone;
}
@@ -0,0 +1,28 @@
package com.baiyue.firstleg.oceanbooking.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@TableName("fl_ocean_booking_status_history")
public class OceanBookingStatusHistoryEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long oceanBookingId;
private String fromStatus;
private String toStatus;
private String action;
private String reason;
private String beforeSnapshot;
private String afterSnapshot;
private Long operatorId;
private String operatorName;
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
}
@@ -0,0 +1,5 @@
package com.baiyue.firstleg.oceanbooking.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OceanBookingContainerAllocationMapper extends BaseMapper<OceanBookingContainerAllocationEntity> {}
@@ -0,0 +1,5 @@
package com.baiyue.firstleg.oceanbooking.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OceanBookingContainerRequirementMapper extends BaseMapper<OceanBookingContainerRequirementEntity> {}
@@ -0,0 +1,92 @@
package com.baiyue.firstleg.oceanbooking.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Query;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
@Mapper
public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
@Select("""
<script>
SELECT * FROM fl_ocean_booking
WHERE (#{query.customerId} IS NULL OR customer_id=#{query.customerId})
AND (#{query.status} IS NULL OR status=#{query.status})
AND (#{query.keyword} IS NULL OR booking_no LIKE CONCAT('%',#{query.keyword},'%')
OR customer_name LIKE CONCAT('%',#{query.keyword},'%')
OR bill_of_lading_no LIKE CONCAT('%',#{query.keyword},'%')
OR vessel_name LIKE CONCAT('%',#{query.keyword},'%')
OR voyage_no LIKE CONCAT('%',#{query.keyword},'%'))
ORDER BY updated_at DESC,id DESC
</script>
""")
Page<OceanBookingEntity> selectPage(Page<OceanBookingEntity> page, @Param("query") Query query);
@Update("""
UPDATE fl_ocean_booking SET customer_id=#{value.customerId},customer_name=#{value.customerName},
bill_of_lading_no=#{value.billOfLadingNo},shipping_company_id=#{value.shippingCompanyId},
shipping_company_name=#{value.shippingCompanyName},booking_agent_id=#{value.bookingAgentId},
booking_agent_name=#{value.bookingAgentName},vessel_name=#{value.vesselName},voyage_no=#{value.voyageNo},
departure_port_code=#{value.departurePortCode},departure_port_name=#{value.departurePortName},
destination_port_code=#{value.destinationPortCode},destination_port_name=#{value.destinationPortName},
place_of_delivery=#{value.placeOfDelivery},service_term=#{value.serviceTerm},freight_term=#{value.freightTerm},
estimated_sailing_at=#{value.estimatedSailingAt},estimated_arrival_at=#{value.estimatedArrivalAt},
remark=#{value.remark},updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{value.id} AND status='DRAFT' AND version=#{expectedVersion}
""")
int updateDraft(@Param("value") OceanBookingEntity value, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status=#{toStatus},failure_reason=#{failureReason},
cancellation_reason=#{cancellationReason},updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status=#{fromStatus} AND version=#{expectedVersion}
""")
int transition(@Param("oceanBookingId") long oceanBookingId, @Param("fromStatus") String fromStatus,
@Param("toStatus") String toStatus, @Param("failureReason") String failureReason,
@Param("cancellationReason") String cancellationReason,
@Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status='CHANGE_PENDING',pending_vessel_name=#{vesselName},
pending_voyage_no=#{voyageNo},pending_estimated_sailing_at=#{estimatedSailingAt},
pending_estimated_arrival_at=#{estimatedArrivalAt},change_reason=#{reason},
updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status='CONFIRMED' AND version=#{expectedVersion}
""")
int requestChange(@Param("oceanBookingId") long oceanBookingId, @Param("vesselName") String vesselName,
@Param("voyageNo") String voyageNo, @Param("estimatedSailingAt") LocalDateTime estimatedSailingAt,
@Param("estimatedArrivalAt") LocalDateTime estimatedArrivalAt, @Param("reason") String reason,
@Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status='CONFIRMED',vessel_name=pending_vessel_name,voyage_no=pending_voyage_no,
estimated_sailing_at=pending_estimated_sailing_at,estimated_arrival_at=pending_estimated_arrival_at,
pending_vessel_name=NULL,pending_voyage_no=NULL,pending_estimated_sailing_at=NULL,
pending_estimated_arrival_at=NULL,updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status='CHANGE_PENDING' AND version=#{expectedVersion}
""")
int confirmChange(@Param("oceanBookingId") long oceanBookingId, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET version=version+1,updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{oceanBookingId} AND version=#{expectedVersion}
AND status IN ('SUBMITTED','CONFIRMED','CHANGE_PENDING')
""")
int touchForAllocation(@Param("oceanBookingId") long oceanBookingId,
@Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
}
@@ -0,0 +1,25 @@
package com.baiyue.firstleg.oceanbooking.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingOperationEntity;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface OceanBookingOperationMapper extends BaseMapper<OceanBookingOperationEntity> {
@Insert("INSERT IGNORE INTO fl_ocean_booking_operation "
+ "(id,ocean_booking_id,operator_id,operation,idempotency_key,request_hash,status,created_at) "
+ "VALUES(#{id},#{oceanBookingId},#{operatorId},#{operation},#{key},#{hash},'PROCESSING',CURRENT_TIMESTAMP(3))")
int claim(@Param("id") long id, @Param("oceanBookingId") Long oceanBookingId,
@Param("operatorId") long operatorId, @Param("operation") String operation,
@Param("key") String key, @Param("hash") String hash);
@Update("UPDATE fl_ocean_booking_operation SET ocean_booking_id=#{oceanBookingId},status='SUCCESS',"
+ "result_json=#{resultJson},completed_at=CURRENT_TIMESTAMP(3) WHERE operator_id=#{operatorId} "
+ "AND operation=#{operation} AND idempotency_key=#{key} AND status='PROCESSING'")
int complete(@Param("oceanBookingId") long oceanBookingId, @Param("operatorId") long operatorId,
@Param("operation") String operation, @Param("key") String key,
@Param("resultJson") String resultJson);
}
@@ -0,0 +1,5 @@
package com.baiyue.firstleg.oceanbooking.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingPartyEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OceanBookingPartyMapper extends BaseMapper<OceanBookingPartyEntity> {}
@@ -0,0 +1,5 @@
package com.baiyue.firstleg.oceanbooking.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingStatusHistoryEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OceanBookingStatusHistoryMapper extends BaseMapper<OceanBookingStatusHistoryEntity> {}
@@ -0,0 +1,63 @@
package com.baiyue.firstleg.oceanbooking.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Query;
import com.baiyue.firstleg.oceanbooking.entity.*;
import com.baiyue.firstleg.oceanbooking.mapper.*;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class OceanBookingRepository {
@Resource private OceanBookingMapper bookings;
@Resource private OceanBookingPartyMapper parties;
@Resource private OceanBookingContainerRequirementMapper requirements;
@Resource private OceanBookingContainerAllocationMapper allocations;
@Resource private OceanBookingStatusHistoryMapper histories;
@Resource private OceanBookingOperationMapper operations;
@Resource private ContainerBookingMapper containerBookings;
public PageResult<OceanBookingEntity> page(Query query) {
return PageResult.from(bookings.selectPage(new Page<>(query.page(), query.size()), query));
}
public OceanBookingEntity find(long id) { return bookings.selectById(id); }
public void insert(OceanBookingEntity value) { bookings.insert(value); }
public int updateDraft(OceanBookingEntity value, int version, long operatorId, String operatorName) {
return bookings.updateDraft(value, version, operatorId, operatorName);
}
public int transition(long id, String from, String to, String failureReason, String cancellationReason,
int version, long operatorId, String operatorName) {
return bookings.transition(id, from, to, failureReason, cancellationReason, version, operatorId, operatorName);
}
public int requestChange(long id, OceanBookingEntity pending, String reason, int version,
long operatorId, String operatorName) {
return bookings.requestChange(id, pending.getPendingVesselName(), pending.getPendingVoyageNo(),
pending.getPendingEstimatedSailingAt(), pending.getPendingEstimatedArrivalAt(), reason,
version, operatorId, operatorName);
}
public int confirmChange(long id, int version, long operatorId, String operatorName) {
return bookings.confirmChange(id, version, operatorId, operatorName);
}
public int touchForAllocation(long id, int version, long operatorId, String operatorName) {
return bookings.touchForAllocation(id, version, operatorId, operatorName);
}
public List<OceanBookingPartyEntity> parties(long id) { return parties.selectList(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id).orderByAsc(OceanBookingPartyEntity::getId)); }
public List<OceanBookingContainerRequirementEntity> requirements(long id) { return requirements.selectList(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerRequirementEntity::getSortNo)); }
public List<OceanBookingContainerAllocationEntity> allocations(long id) { return allocations.selectList(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>().eq(OceanBookingContainerAllocationEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerAllocationEntity::getAllocatedAt)); }
public List<OceanBookingStatusHistoryEntity> histories(long id) { return histories.selectList(new LambdaQueryWrapper<OceanBookingStatusHistoryEntity>().eq(OceanBookingStatusHistoryEntity::getOceanBookingId, id).orderByAsc(OceanBookingStatusHistoryEntity::getCreatedAt)); }
public void replaceParties(long id, List<OceanBookingPartyEntity> values) { parties.delete(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id)); values.forEach(parties::insert); }
public void replaceRequirements(long id, List<OceanBookingContainerRequirementEntity> values) { requirements.delete(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id)); values.forEach(requirements::insert); }
public OceanBookingContainerRequirementEntity requirement(long id, long requirementId) { return requirements.selectOne(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).eq(OceanBookingContainerRequirementEntity::getId, requirementId)); }
public ContainerBookingEntity containerBooking(long id) { return containerBookings.selectById(id); }
public void insert(OceanBookingContainerAllocationEntity value) { allocations.insert(value); }
public void insert(OceanBookingStatusHistoryEntity value) { histories.insert(value); }
public boolean claimOperation(Long id, long operatorId, String action, String key, String hash) { return operations.claim(com.baomidou.mybatisplus.core.toolkit.IdWorker.getId(), id, operatorId, action, key, hash) == 1; }
public OceanBookingOperationEntity operation(long operatorId, String action, String key) { return operations.selectOne(new LambdaQueryWrapper<OceanBookingOperationEntity>().eq(OceanBookingOperationEntity::getOperatorId, operatorId).eq(OceanBookingOperationEntity::getOperation, action).eq(OceanBookingOperationEntity::getIdempotencyKey, key)); }
public int completeOperation(long id, long operatorId, String action, String key, String json) { return operations.complete(id, operatorId, action, key, json); }
}
@@ -0,0 +1,272 @@
package com.baiyue.firstleg.oceanbooking.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
import com.baiyue.firstleg.oceanbooking.entity.*;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Set;
@Service
public class OceanBookingService {
private static final Set<String> PARTY_TYPES = Set.of("SHIPPER", "CONSIGNEE", "NOTIFY_PARTY");
@Resource private OceanBookingRepository repository;
@Resource private ObjectMapper objectMapper;
public PageResult<OceanBookingEntity> page(Query query) {
String status = trim(query.status());
if (status != null) parseStatus(status);
return repository.page(new Query(query.customerId(), status, trim(query.keyword()), query.page(), query.size()));
}
public Detail detail(long oceanBookingId) { return detail(required(oceanBookingId)); }
@Transactional
public Detail create(long operatorId, String operatorName, String key, CreateRequest request) {
if (request.expectedVersion() != 0) throw concurrent();
validateDraft(request.data());
String action = "CREATE";
String hash = hash(action, null, request);
Detail replay = claimOrReplay(null, operatorId, action, key, hash);
if (replay != null) return replay;
OceanBookingEntity value = new OceanBookingEntity();
value.setId(IdWorker.getId());
value.setBookingNo("OB" + value.getId());
value.setStatus(OceanBookingStatus.DRAFT.name());
copy(value, request.data());
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
repository.insert(value);
repository.replaceParties(value.getId(), parties(value.getId(), request.data().parties()));
repository.replaceRequirements(value.getId(), requirements(value.getId(), request.data().containerRequirements()));
addHistory(value.getId(), null, OceanBookingStatus.DRAFT.name(), action, null, null, value, operatorId, operatorName);
return complete(value.getId(), operatorId, action, key, detail(value.getId()));
}
@Transactional
public Detail updateDraft(long oceanBookingId, long operatorId, String operatorName, String key,
UpdateDraftRequest request) {
validateDraft(request.data());
String action = "UPDATE_DRAFT";
Detail replay = claimOrReplay(oceanBookingId, operatorId, action, key, hash(action, oceanBookingId, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(oceanBookingId, request.expectedVersion());
requireStatus(before, OceanBookingStatus.DRAFT);
OceanBookingEntity value = new OceanBookingEntity(); value.setId(oceanBookingId); copy(value, request.data());
if (repository.updateDraft(value, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
repository.replaceParties(oceanBookingId, parties(oceanBookingId, request.data().parties()));
repository.replaceRequirements(oceanBookingId, requirements(oceanBookingId, request.data().containerRequirements()));
return complete(oceanBookingId, operatorId, action, key, detail(oceanBookingId));
}
@Transactional public Detail submit(long id, long operatorId, String name, String key, ActionRequest request) {
return transition(id, operatorId, name, key, "SUBMIT", OceanBookingStatus.DRAFT,
OceanBookingStatus.SUBMITTED, request, false);
}
@Transactional public Detail confirm(long id, long operatorId, String name, String key, ActionRequest request) {
return transition(id, operatorId, name, key, "CONFIRM", OceanBookingStatus.SUBMITTED,
OceanBookingStatus.CONFIRMED, request, false);
}
@Transactional public Detail release(long id, long operatorId, String name, String key, ActionRequest request) {
return transition(id, operatorId, name, key, "RELEASE", OceanBookingStatus.CONFIRMED,
OceanBookingStatus.RELEASED, request, false);
}
@Transactional
public Detail fail(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("失败原因不能为空");
String action = "FAIL";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.SUBMITTED);
if (repository.transition(id, OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.FAILED.name(), trim(request.reason()), null,
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, OceanBookingStatus.SUBMITTED.name(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail failChange(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("改船失败原因不能为空");
return transition(id, operatorId, name, key, "FAIL_CHANGE", OceanBookingStatus.CHANGE_PENDING,
OceanBookingStatus.FAILED, request, true);
}
@Transactional
public Detail cancel(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("取消原因不能为空");
String action = "CANCEL";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
OceanBookingStatus from = parseStatus(before.getStatus());
if (from == OceanBookingStatus.RELEASED || from == OceanBookingStatus.CANCELLED) {
throw conflict("订舱已越过不可逆节点或已经取消");
}
if (repository.transition(id, from.name(), OceanBookingStatus.CANCELLED.name(), null,
trim(request.reason()), request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, from.name(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail requestChange(long id, long operatorId, String name, String key, ChangeRequest request) {
validateRange(request.estimatedSailingAt(), request.estimatedArrivalAt());
String action = "REQUEST_CHANGE";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.CONFIRMED);
OceanBookingEntity pending = new OceanBookingEntity();
pending.setPendingVesselName(request.vesselName().trim());
pending.setPendingVoyageNo(request.voyageNo().trim());
pending.setPendingEstimatedSailingAt(request.estimatedSailingAt());
pending.setPendingEstimatedArrivalAt(request.estimatedArrivalAt());
if (repository.requestChange(id, pending, request.reason().trim(), request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail confirmChange(long id, long operatorId, String name, String key, ActionRequest request) {
String action = "CONFIRM_CHANGE";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.CHANGE_PENDING);
if (repository.confirmChange(id, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail allocate(long id, long operatorId, String name, String key, AllocationRequest request) {
String action = "ALLOCATE_CONTAINER";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
requiredVersion(id, request.expectedVersion());
OceanBookingContainerRequirementEntity requirement = repository.requirement(id, request.requirementId());
if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "箱型需求不存在");
ContainerBookingEntity container = repository.containerBooking(request.containerBookingId());
if (container == null) throw new BizException(ResultCode.NOT_FOUND, "柜次不存在");
if (!requirement.getContainerType().equals(container.getContainerType())) throw conflict("柜次箱型与订舱需求不一致");
long used = repository.allocations(id).stream().filter(v -> request.requirementId().equals(v.getRequirementId())).count();
if (used >= requirement.getQuantity()) throw conflict("该箱型需求已分配完毕");
if (repository.touchForAllocation(id, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
allocation.setId(IdWorker.getId()); allocation.setOceanBookingId(id);
allocation.setRequirementId(request.requirementId()); allocation.setContainerBookingId(request.containerBookingId());
allocation.setAllocatedBy(operatorId); allocation.setAllocatedByName(name);
try { repository.insert(allocation); } catch (DuplicateKeyException ex) { throw conflict("柜次已被其他订舱占用"); }
return complete(id, operatorId, action, key, detail(id));
}
private Detail transition(long id, long operatorId, String name, String key, String action,
OceanBookingStatus from, OceanBookingStatus to, ActionRequest request,
boolean failure) {
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, from);
if (from == OceanBookingStatus.DRAFT && to == OceanBookingStatus.SUBMITTED) validateSubmission(id);
if (repository.transition(id, from.name(), to.name(), failure ? trim(request.reason()) : null, null,
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, from.name(), to.name(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
private Detail claimOrReplay(Long id, long operatorId, String action, String key, String hash) {
validateKey(key);
if (repository.claimOperation(id, operatorId, action, key, hash)) return null;
OceanBookingOperationEntity existing = repository.operation(operatorId, action, key);
if (existing == null || !hash.equals(existing.getRequestHash())) throw conflict("幂等键已用于其他请求");
if (!"SUCCESS".equals(existing.getStatus()) || !StringUtils.hasText(existing.getResultJson())) throw conflict("相同请求正在处理中,请稍后重试");
try { return objectMapper.readValue(existing.getResultJson(), Detail.class); }
catch (JsonProcessingException ex) { throw new IllegalStateException("无法读取订舱幂等结果", ex); }
}
private Detail complete(long id, long operatorId, String action, String key, Detail detail) {
try {
if (repository.completeOperation(id, operatorId, action, key, objectMapper.writeValueAsString(detail)) != 1) {
throw conflict("订舱幂等请求完成状态冲突");
}
return detail;
} catch (JsonProcessingException ex) { throw new IllegalStateException("无法保存订舱幂等结果", ex); }
}
private void addHistory(long id, String from, String to, String action, String reason,
Object before, Object after, long operatorId, String operatorName) {
OceanBookingStatusHistoryEntity history = new OceanBookingStatusHistoryEntity();
history.setId(IdWorker.getId()); history.setOceanBookingId(id); history.setFromStatus(from);
history.setToStatus(to); history.setAction(action); history.setReason(trim(reason));
history.setBeforeSnapshot(json(before)); history.setAfterSnapshot(json(after));
history.setOperatorId(operatorId); history.setOperatorName(operatorName); repository.insert(history);
}
private Detail detail(OceanBookingEntity value) { return new Detail(value, repository.parties(value.getId()), repository.requirements(value.getId()), repository.allocations(value.getId()), repository.histories(value.getId())); }
private OceanBookingEntity required(long id) { OceanBookingEntity value = repository.find(id); if (value == null) throw new BizException(ResultCode.NOT_FOUND, "订舱单不存在"); return value; }
private OceanBookingEntity requiredVersion(long id, int version) { OceanBookingEntity value = required(id); if (!Integer.valueOf(version).equals(value.getVersion())) throw concurrent(); return value; }
private static void requireStatus(OceanBookingEntity value, OceanBookingStatus status) { if (!status.name().equals(value.getStatus())) throw conflict("订舱状态不允许当前操作"); }
private void validateDraft(DraftData data) {
validateRange(data.estimatedSailingAt(), data.estimatedArrivalAt());
Set<String> types = new HashSet<>();
for (PartyRequest party : data.parties()) {
String type = party.partyType().trim().toUpperCase();
if (!PARTY_TYPES.contains(type) || !types.add(type)) throw badRequest("订舱方必须各包含一条SHIPPER、CONSIGNEE和NOTIFY_PARTY");
}
if (!types.equals(PARTY_TYPES)) throw badRequest("订舱方必须各包含一条SHIPPER、CONSIGNEE和NOTIFY_PARTY");
}
private void validateSubmission(long id) {
Set<String> types = repository.parties(id).stream().map(OceanBookingPartyEntity::getPartyType)
.collect(java.util.stream.Collectors.toSet());
if (!types.equals(PARTY_TYPES) || repository.requirements(id).isEmpty()) {
throw conflict("订舱方或箱型需求不完整,不能提交");
}
}
private static void validateRange(LocalDateTime from, LocalDateTime to) { if (from != null && to != null && to.isBefore(from)) throw badRequest("预计到港时间不能早于预计开船时间"); }
private static void copy(OceanBookingEntity v, DraftData d) {
v.setCustomerId(d.customerId()); v.setCustomerName(d.customerName().trim()); v.setBillOfLadingNo(trim(d.billOfLadingNo()));
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(trim(d.shippingCompanyName()));
v.setBookingAgentId(d.bookingAgentId()); v.setBookingAgentName(trim(d.bookingAgentName()));
v.setVesselName(trim(d.vesselName())); v.setVoyageNo(trim(d.voyageNo()));
v.setDeparturePortCode(trim(d.departurePortCode())); v.setDeparturePortName(trim(d.departurePortName()));
v.setDestinationPortCode(trim(d.destinationPortCode())); v.setDestinationPortName(trim(d.destinationPortName()));
v.setPlaceOfDelivery(trim(d.placeOfDelivery())); v.setServiceTerm(trim(d.serviceTerm())); v.setFreightTerm(trim(d.freightTerm()));
v.setEstimatedSailingAt(d.estimatedSailingAt()); v.setEstimatedArrivalAt(d.estimatedArrivalAt()); v.setRemark(trim(d.remark()));
}
private static List<OceanBookingPartyEntity> parties(long id, List<PartyRequest> requests) { return requests.stream().map(r -> { OceanBookingPartyEntity v = new OceanBookingPartyEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setPartyType(r.partyType().trim().toUpperCase()); v.setPartyName(r.partyName().trim()); v.setAddress(trim(r.address())); v.setContactName(trim(r.contactName())); v.setContactPhone(trim(r.contactPhone())); return v; }).toList(); }
private static List<OceanBookingContainerRequirementEntity> requirements(long id, List<ContainerRequirementRequest> requests) { int[] sort = {0}; return requests.stream().map(r -> { OceanBookingContainerRequirementEntity v = new OceanBookingContainerRequirementEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setContainerType(r.containerType().trim().toUpperCase()); v.setContainerKind(r.containerKind().trim().toUpperCase()); v.setQuantity(r.quantity()); v.setSortNo(sort[0]++); return v; }).toList(); }
private String hash(String action, Long id, Object request) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest((action + '|' + id + '|' + objectMapper.writeValueAsString(request)).getBytes(StandardCharsets.UTF_8))); } catch (Exception ex) { throw new IllegalStateException("无法计算订舱请求摘要", ex); } }
private String json(Object value) { if (value == null) return null; try { return objectMapper.writeValueAsString(value); } catch (JsonProcessingException ex) { throw new IllegalStateException("无法保存订舱历史快照", ex); } }
private static OceanBookingStatus parseStatus(String status) { try { return OceanBookingStatus.valueOf(status); } catch (RuntimeException ex) { throw badRequest("订舱状态无效"); } }
private static void validateKey(String key) { if (!StringUtils.hasText(key) || key.length() > 128) throw badRequest("Idempotency-Key不能为空且不能超过128个字符"); }
private static String trim(String value) { if (value == null) return null; String result = value.trim(); return result.isEmpty() ? null : result; }
private static BizException badRequest(String message) { return new BizException(ResultCode.BAD_REQUEST, message); }
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
private static BizException concurrent() { return conflict("数据已被其他人修改,请刷新后重试"); }
}
@@ -0,0 +1,6 @@
package com.baiyue.firstleg.repository;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.common.core.api.PageResult; import com.baiyue.firstleg.dto.TruckAppointmentDtos.Query; import com.baiyue.firstleg.entity.*; import com.baiyue.firstleg.mapper.*; import jakarta.annotation.Resource; import org.springframework.stereotype.Repository;
@Repository public class TruckAppointmentRepository { @Resource TruckAppointmentMapper mapper; @Resource TruckAppointmentHistoryMapper history; @Resource TruckAppointmentAttachmentMapper attachments; @Resource TruckAppointmentOperationMapper operations;
public PageResult<TruckAppointmentEntity> page(Query q){return PageResult.from(mapper.page(new Page<>(q.page(),q.size()),q));} public TruckAppointmentEntity find(long id){return mapper.selectById(id);} public void insert(TruckAppointmentEntity e){mapper.insert(e);} public int transition(long id,String from,String to,int v){return mapper.transition(id,from,to,v);} public int reschedule(long id,java.time.LocalDateTime at,String reason,int v){return mapper.reschedule(id,at,reason,v);} public void history(TruckAppointmentHistoryEntity e){history.insert(e);} public void operation(TruckAppointmentOperationEntity e){operations.insert(e);} public void attachment(TruckAppointmentAttachmentEntity e){attachments.insert(e);}
public int updateEdit(TruckAppointmentEntity e,int v){return mapper.updateEdit(e,v);}
}
@@ -0,0 +1,14 @@
package com.baiyue.firstleg.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baiyue.common.core.api.PageResult; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.domain.TruckAppointmentStatus; import com.baiyue.firstleg.dto.TruckAppointmentDtos.*; import com.baiyue.firstleg.entity.*; import com.baiyue.firstleg.repository.TruckAppointmentRepository; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime;
@Service public class TruckAppointmentService { @Resource TruckAppointmentRepository repo;
public PageResult<TruckAppointmentEntity> page(Query q){return repo.page(q);} public TruckAppointmentEntity detail(long id){return required(id);}
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0"); TruckAppointmentEntity e=new TruckAppointmentEntity(); e.setId(IdWorker.getId()); e.setAppointmentNo("TA"+e.getId()); e.setStatus(TruckAppointmentStatus.DRAFT); e.setRevision(0); e.setVersion(0); e.setCreatedAt(LocalDateTime.now()); e.setUpdatedAt(e.getCreatedAt()); copy(e,r); repo.insert(e); record(e,null,e.getStatus().name(),"CREATE",null,uid,name); return e;}
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑"); copy(e,r); if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试"); record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name); return required(id);}
@Transactional public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String hash,long uid,String name){TruckAppointmentEntity e=required(id); checkVersion(e,r.expectedVersion()); TruckAppointmentStatus from=e.getStatus(),to=next(from,action); if("RESCHEDULE".equals(action)){throw conflict("改约请使用专用接口");} if(repo.transition(id,from.name(),to.name(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试"); e.setStatus(to);e.setVersion(e.getVersion()+1);e.setRevision(e.getRevision()+1); record(e,from.name(),to.name(),action,r.reason(),uid,name); operation(e,action,idem,hash); return e;}
@Transactional public TruckAppointmentEntity reschedule(long id,LocalDateTime at,ActionRequest r,long uid,String name){TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion()); if(e.getStatus()==TruckAppointmentStatus.COMPLETED||e.getStatus()==TruckAppointmentStatus.CANCELLED)throw conflict("当前状态不可改约"); if(repo.reschedule(id,at,r.reason(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试");e.setStatus(TruckAppointmentStatus.RESCHEDULED);e.setPlannedLoadingAt(at);record(e,e.getStatus().name(),"RESCHEDULED","RESCHEDULE",r.reason(),uid,name);return e;}
private TruckAppointmentStatus next(TruckAppointmentStatus s,String a){return switch(a){case"SUBMIT"->s==TruckAppointmentStatus.DRAFT?TruckAppointmentStatus.PENDING_DISPATCH:badStatus();case"DISPATCH"->s==TruckAppointmentStatus.PENDING_DISPATCH||s==TruckAppointmentStatus.RESCHEDULED?TruckAppointmentStatus.DISPATCHED:badStatus();case"PICKUP"->s==TruckAppointmentStatus.DISPATCHED?TruckAppointmentStatus.PICKUP_IN_PROGRESS:badStatus();case"ARRIVE"->s==TruckAppointmentStatus.PICKUP_IN_PROGRESS?TruckAppointmentStatus.ARRIVED_WAREHOUSE:badStatus();case"LOAD"->s==TruckAppointmentStatus.ARRIVED_WAREHOUSE?TruckAppointmentStatus.LOADING:badStatus();case"DEPART"->s==TruckAppointmentStatus.LOADING?TruckAppointmentStatus.DEPARTED:badStatus();case"COMPLETE"->s==TruckAppointmentStatus.DEPARTED?TruckAppointmentStatus.COMPLETED:badStatus();case"EXCEPTION"->s!=TruckAppointmentStatus.COMPLETED&&s!=TruckAppointmentStatus.CANCELLED?TruckAppointmentStatus.EXCEPTION:badStatus();case"CANCEL"->s!=TruckAppointmentStatus.COMPLETED?TruckAppointmentStatus.CANCELLED:badStatus();default->badStatus();};}
private TruckAppointmentStatus badStatus(){throw conflict("不允许的状态动作");} private TruckAppointmentEntity required(long id){TruckAppointmentEntity e=repo.find(id);if(e==null)throw new BizException(ResultCode.NOT_FOUND,"拖车预约不存在");return e;} private void checkVersion(TruckAppointmentEntity e,int v){if(e.getVersion()!=v)throw conflict("版本已变化,请刷新后重试");} private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);} private static BizException bad(String m){return new BizException(ResultCode.BAD_REQUEST,m);}
private void copy(TruckAppointmentEntity e,SaveRequest r){e.setOutboundOrderId(r.outboundOrderId());e.setOceanBookingId(r.oceanBookingId());e.setContainerBookingId(r.containerBookingId());e.setLoadingPlanId(r.loadingPlanId());e.setWarehouseId(r.warehouseId());e.setCustomerId(r.customerId());e.setCustomerName(r.customerName());e.setCarrierId(r.carrierId());e.setCarrierName(r.carrierName());e.setFleetContact(r.fleetContact());e.setFleetPhone(r.fleetPhone());e.setDriverName(r.driverName());e.setDriverPhone(r.driverPhone());e.setDriverIdCard(r.driverIdCard());e.setVehicleNo(r.vehicleNo());e.setStoreName(r.storeName());e.setStoreContact(r.storeContact());e.setStorePhone(r.storePhone());e.setStoreAddress(r.storeAddress());e.setStoreCity(r.storeCity());e.setStoreZipcode(r.storeZipcode());e.setPackageCount(r.packageCount());e.setGrossWeightKg(r.grossWeightKg());e.setVolumeCbm(r.volumeCbm());e.setPalletCount(r.palletCount());e.setPoNo(r.poNo());e.setPlannedLoadingAt(r.plannedLoadingAt());e.setTransitMinutes(r.transitMinutes());e.setPayableAmount(r.payableAmount());e.setReceivableAmount(r.receivableAmount());e.setCurrency(r.currency()==null?"RMB":r.currency());e.setRemark(r.remark());}
private void record(TruckAppointmentEntity e,String from,String to,String action,String reason,long uid,String name){TruckAppointmentHistoryEntity h=new TruckAppointmentHistoryEntity();h.setId(IdWorker.getId());h.setAppointmentId(e.getId());h.setRevision(e.getRevision());h.setAction(action);h.setFromStatus(from);h.setToStatus(to);h.setReason(reason);h.setOperatorId(uid);h.setOperatorName(name);h.setCreatedAt(LocalDateTime.now());repo.history(h);}
private void operation(TruckAppointmentEntity e,String action,String key,String hash){if(key==null||key.isBlank())return;TruckAppointmentOperationEntity o=new TruckAppointmentOperationEntity();o.setId(IdWorker.getId());o.setAppointmentId(e.getId());o.setAction(action);o.setIdempotencyKey(key);o.setRequestHash(hash==null?"":hash);o.setCreatedAt(LocalDateTime.now());repo.operation(o);}
}