优化
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.firstleg.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
/** 仅用于确认证据文件属于当前客户和入库单,不在 firstleg 复制文件元数据。 */
|
||||
@FeignClient(name = "file-service", contextId = "firstlegLoadingEvidenceFileClient",
|
||||
url = "${baiyue.clients.file.url:}", path = "/internal/file/v1/loading-evidence")
|
||||
public interface LoadingEvidenceFileClient {
|
||||
@GetMapping("/{tenantId}/{inboundId}/{fileId}")
|
||||
ApiResponse<FileMetadata> validate(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long fileId);
|
||||
|
||||
record FileMetadata(Long fileId, String fileName, String businessType, String contentType) {}
|
||||
}
|
||||
+4
-6
@@ -10,7 +10,6 @@ import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
|
||||
import com.baiyue.firstleg.entity.MeasureReviewRecordEntity;
|
||||
import com.baiyue.firstleg.service.MeasurementReviewService;
|
||||
import com.baiyue.firstleg.service.AutomaticInboundCompletionService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -36,12 +35,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/platform/v1/inbounds")
|
||||
public class PlatformInboundMeasurementReviewController {
|
||||
@Resource private MeasurementReviewService service;
|
||||
@Resource private AutomaticInboundCompletionService automaticCompletion;
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_APPROVE", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "测量审核通过")
|
||||
@Operation(summary = "测量审核通过",
|
||||
description = "客服审核通过后自动计费、发布费用给客户并完成入库;客户无需确认费用。")
|
||||
description = "客服确认测量数据后直接进入待分柜;费用和其他资料可在后续继续维护。")
|
||||
@PostMapping("/{inboundId}/measurements/approve")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:measurement-review')")
|
||||
public ApiResponse<FeeSnapshotResponse> approve(
|
||||
@@ -53,9 +51,9 @@ public class PlatformInboundMeasurementReviewController {
|
||||
@Valid @RequestBody MeasurementReviewRequest request) {
|
||||
validateScope(user, tenantId);
|
||||
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
|
||||
return ApiResponse.success(automaticCompletion.completeApproved(
|
||||
tenantId, inboundId, user.userId(), user.username(), request.measurementId(),
|
||||
request.expectedVersion() + 1));
|
||||
// 统一订单主线要求测量审核后直接进入 WAIT_SPLIT,不把计费、库位或其他资料
|
||||
// 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASURE_REJECT", resourceType = "INBOUND",
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.baiyue.firstleg.loading.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteDeliveryRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.UpsertEvidenceRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.PickingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.EvidenceListResponse;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.LoadingActionResponse;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.PickingResponse;
|
||||
import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/** 平台端装柜证据、装柜完成和派送完成操作。 */
|
||||
@Tag(name = "订单装柜与派送")
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/inbounds")
|
||||
public class PlatformLoadingDeliveryController {
|
||||
@Resource private LoadingDeliveryService service;
|
||||
|
||||
@Operation(summary = "新增或修改装柜证据")
|
||||
@PostMapping("/{inboundId}/loading/evidence")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:edit')")
|
||||
@BusinessAudit(event = "INBOUND_LOADING_EVIDENCE_UPSERT", resourceType = "INBOUND", resourceId = "#inboundId", description = "维护装柜证据")
|
||||
public ApiResponse<LoadingActionResponse> evidence(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String key, @Valid @RequestBody UpsertEvidenceRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.upsertEvidence(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询装柜证据")
|
||||
@GetMapping("/{inboundId}/loading/evidence")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:view')")
|
||||
public ApiResponse<EvidenceListResponse> evidence(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.evidence(tenantId, inboundId));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成装柜")
|
||||
@PostMapping("/{inboundId}/loading/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:complete')")
|
||||
@BusinessAudit(event = "INBOUND_COMPLETE_LOADING", resourceType = "INBOUND", resourceId = "#inboundId", description = "确认装柜完成")
|
||||
public ApiResponse<LoadingActionResponse> completeLoading(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CompleteLoadingRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.completeLoading(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "确认派送完成")
|
||||
@PostMapping("/{inboundId}/delivery/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:delivery:complete')")
|
||||
@BusinessAudit(event = "INBOUND_COMPLETE_DELIVERY", resourceType = "INBOUND", resourceId = "#inboundId", description = "确认派送完成")
|
||||
public ApiResponse<LoadingActionResponse> completeDelivery(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CompleteDeliveryRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.completeDelivery(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "PDA开始拣货")
|
||||
@PostMapping("/{inboundId}/loading/picking/start")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:picking:edit')")
|
||||
@BusinessAudit(event = "INBOUND_START_PICKING", resourceType = "INBOUND", resourceId = "#inboundId", description = "开始PDA拣货")
|
||||
public ApiResponse<PickingResponse> startPicking(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String key, @Valid @RequestBody PickingRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.startPicking(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "PDA完成拣货")
|
||||
@PostMapping("/{inboundId}/loading/picking/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:picking:edit')")
|
||||
@BusinessAudit(event = "INBOUND_COMPLETE_PICKING", resourceType = "INBOUND", resourceId = "#inboundId", description = "完成PDA拣货")
|
||||
public ApiResponse<PickingResponse> completePicking(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String key, @Valid @RequestBody PickingRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.completePicking(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询PDA拣货任务")
|
||||
@GetMapping("/{inboundId}/loading/picking")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:picking:view')")
|
||||
public ApiResponse<PickingResponse> picking(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.picking(tenantId, inboundId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.firstleg.loading.domain;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 装柜证据类型。照片语义尚待产品确认,当前只固定四个步骤和一个铅封证据槽位。
|
||||
*/
|
||||
public final class LoadingEvidenceType {
|
||||
public static final String PHOTO_STEP_1 = "PHOTO_STEP_1";
|
||||
public static final String PHOTO_STEP_2 = "PHOTO_STEP_2";
|
||||
public static final String PHOTO_STEP_3 = "PHOTO_STEP_3";
|
||||
public static final String PHOTO_STEP_4 = "PHOTO_STEP_4";
|
||||
public static final String SEAL = "SEAL";
|
||||
public static final Set<String> VALUES = Set.of(PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL);
|
||||
|
||||
private LoadingEvidenceType() {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.firstleg.loading.domain;
|
||||
|
||||
public final class PickingStatus {
|
||||
public static final String PENDING = "PENDING";
|
||||
public static final String IN_PROGRESS = "IN_PROGRESS";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
private PickingStatus() {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.firstleg.loading.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public final class LoadingRequests {
|
||||
private LoadingRequests() {}
|
||||
|
||||
public record UpsertEvidenceRequest(
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@NotBlank @Size(max = 32) String evidenceType,
|
||||
@Positive Long fileId,
|
||||
@Size(max = 255) String fileName,
|
||||
@Size(max = 64) String sealNo,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record CompleteLoadingRequest(
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record CompleteDeliveryRequest(
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record PickingRequest(@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@PositiveOrZero Integer expectedCount,
|
||||
@PositiveOrZero Integer pickedCount,
|
||||
@Size(max = 1000) String remark) {}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.firstleg.loading.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class LoadingResponses {
|
||||
private LoadingResponses() {}
|
||||
|
||||
public record EvidenceResponse(Long id, Long inboundId, Long tenantId, String evidenceType,
|
||||
Long fileId, String fileName, String sealNo, String remark,
|
||||
Integer version, LocalDateTime createdAt, LocalDateTime updatedAt) {}
|
||||
|
||||
public record EvidenceListResponse(Long inboundId, List<EvidenceResponse> evidences) {}
|
||||
|
||||
public record LoadingActionResponse(String mainStatus, EvidenceListResponse evidences) {}
|
||||
|
||||
public record PickingResponse(Long id, Long inboundId, String status, Integer version,
|
||||
Integer expectedCount, Integer pickedCount, String remark) {}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.firstleg.loading.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_loading_evidence")
|
||||
public class LoadingEvidenceEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private String evidenceType;
|
||||
private Long fileId;
|
||||
private String fileName;
|
||||
private String sealNo;
|
||||
private String remark;
|
||||
private Integer version;
|
||||
private Long createdBy;
|
||||
private String createdByName;
|
||||
private Long updatedBy;
|
||||
private String updatedByName;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.firstleg.loading.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_loading_operation")
|
||||
public class LoadingOperationEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Long operatorId;
|
||||
private String operation;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String status;
|
||||
private String resultJson;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime completedAt;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.firstleg.loading.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_loading_pick_task")
|
||||
public class LoadingPickTaskEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private String status;
|
||||
private Integer version;
|
||||
private Integer expectedCount;
|
||||
private Integer pickedCount;
|
||||
private String remark;
|
||||
private Long updatedBy;
|
||||
private String updatedByName;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.firstleg.loading.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingEvidenceMapper extends BaseMapper<LoadingEvidenceEntity> {
|
||||
@Update("UPDATE fl_loading_evidence SET file_id=#{fileId}, file_name=#{fileName}, seal_no=#{sealNo}, "
|
||||
+ "remark=#{remark}, updated_by=#{operatorId}, updated_by_name=#{operatorName}, version=version+1 "
|
||||
+ "WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND evidence_type=#{evidenceType} "
|
||||
+ "AND version=#{expectedVersion}")
|
||||
int updateEvidence(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("evidenceType") String evidenceType, @Param("fileId") Long fileId,
|
||||
@Param("fileName") String fileName, @Param("sealNo") String sealNo,
|
||||
@Param("remark") String remark, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName, @Param("expectedVersion") int expectedVersion);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.firstleg.loading.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingInboundMapper {
|
||||
@Select("SELECT id, tenant_id AS tenantId, process_status AS processStatus, version "
|
||||
+ "FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
|
||||
Snapshot find(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Update("UPDATE fl_inbound_order SET process_status='LOADED', updated_by=#{operatorId}, version=version+1 "
|
||||
+ "WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 AND process_status='LOADING' AND version=#{expectedVersion}")
|
||||
int completeLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
|
||||
|
||||
@Update("UPDATE fl_inbound_order SET process_status='DELIVERED', updated_by=#{operatorId}, version=version+1 "
|
||||
+ "WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 AND process_status='LOADED' AND version=#{expectedVersion}")
|
||||
int completeDelivery(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
|
||||
|
||||
record Snapshot(Long id, Long tenantId, String processStatus, Integer version) {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.firstleg.loading.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingOperationEntity;
|
||||
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 LoadingOperationMapper extends BaseMapper<LoadingOperationEntity> {
|
||||
@Insert("INSERT IGNORE INTO fl_loading_operation "
|
||||
+ "(id,tenant_id,inbound_id,operator_id,operation,idempotency_key,request_hash,status,created_at) "
|
||||
+ "VALUES(#{id},#{tenantId},#{inboundId},#{operatorId},#{operation},#{key},#{requestHash},'PROCESSING',CURRENT_TIMESTAMP(3))")
|
||||
int claim(@Param("id") long id, @Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("operatorId") long operatorId, @Param("operation") String operation,
|
||||
@Param("key") String key, @Param("requestHash") String requestHash);
|
||||
|
||||
@Update("UPDATE fl_loading_operation SET status='SUCCESS', result_json=#{resultJson}, completed_at=CURRENT_TIMESTAMP(3) "
|
||||
+ "WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND operator_id=#{operatorId} "
|
||||
+ "AND operation=#{operation} AND idempotency_key=#{key} AND status='PROCESSING'")
|
||||
int complete(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("operatorId") long operatorId, @Param("operation") String operation,
|
||||
@Param("key") String key, @Param("resultJson") String resultJson);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.firstleg.loading.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingPickTaskEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity> {
|
||||
@Update("UPDATE fl_loading_pick_task SET status=#{toStatus}, updated_by=#{operatorId}, updated_by_name=#{operatorName}, "
|
||||
+ "remark=COALESCE(#{remark}, remark), expected_count=COALESCE(#{expectedCount}, expected_count), "
|
||||
+ "picked_count=COALESCE(#{pickedCount}, picked_count), version=version+1 WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} "
|
||||
+ "AND status=#{fromStatus} AND version=#{expectedVersion}")
|
||||
int changeStatus(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName,
|
||||
@Param("remark") String remark, @Param("expectedCount") Integer expectedCount,
|
||||
@Param("pickedCount") Integer pickedCount,
|
||||
@Param("expectedVersion") int expectedVersion);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.firstleg.loading.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingOperationEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingEvidenceMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingOperationMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingPickTaskMapper;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingPickTaskEntity;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class LoadingRepository {
|
||||
@Resource private LoadingInboundMapper inbounds;
|
||||
@Resource private LoadingEvidenceMapper evidences;
|
||||
@Resource private LoadingOperationMapper operations;
|
||||
@Resource private LoadingPickTaskMapper picks;
|
||||
|
||||
public LoadingInboundMapper.Snapshot findInbound(long tenantId, long inboundId) {
|
||||
return inbounds.find(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public int completeLoading(long tenantId, long inboundId, long operatorId, int expectedVersion) {
|
||||
return inbounds.completeLoading(tenantId, inboundId, operatorId, expectedVersion);
|
||||
}
|
||||
|
||||
public int completeDelivery(long tenantId, long inboundId, long operatorId, int expectedVersion) {
|
||||
return inbounds.completeDelivery(tenantId, inboundId, operatorId, expectedVersion);
|
||||
}
|
||||
|
||||
public LoadingEvidenceEntity findEvidence(long tenantId, long inboundId, String type) {
|
||||
return evidences.selectOne(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
.eq(LoadingEvidenceEntity::getInboundId, inboundId)
|
||||
.eq(LoadingEvidenceEntity::getEvidenceType, type));
|
||||
}
|
||||
|
||||
public List<LoadingEvidenceEntity> listEvidence(long tenantId, long inboundId) {
|
||||
return evidences.selectList(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||
.eq(LoadingEvidenceEntity::getInboundId, inboundId)
|
||||
.orderByAsc(LoadingEvidenceEntity::getEvidenceType));
|
||||
}
|
||||
|
||||
public void createEvidence(LoadingEvidenceEntity entity) { evidences.insert(entity); }
|
||||
|
||||
public int updateEvidence(long tenantId, long inboundId, String type, Long fileId, String fileName,
|
||||
String sealNo, String remark, long operatorId, String operatorName, int version) {
|
||||
return evidences.updateEvidence(tenantId, inboundId, type, fileId, fileName, sealNo, remark,
|
||||
operatorId, operatorName, version);
|
||||
}
|
||||
|
||||
public LoadingOperationEntity findOperation(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key) {
|
||||
return operations.selectOne(new LambdaQueryWrapper<LoadingOperationEntity>()
|
||||
.eq(LoadingOperationEntity::getTenantId, tenantId)
|
||||
.eq(LoadingOperationEntity::getInboundId, inboundId)
|
||||
.eq(LoadingOperationEntity::getOperatorId, operatorId)
|
||||
.eq(LoadingOperationEntity::getOperation, operation)
|
||||
.eq(LoadingOperationEntity::getIdempotencyKey, key));
|
||||
}
|
||||
|
||||
public boolean claimOperation(long tenantId, long inboundId, long operatorId, String operation,
|
||||
String key, String requestHash) {
|
||||
return operations.claim(IdWorker.getId(), tenantId, inboundId, operatorId, operation, key, requestHash) == 1;
|
||||
}
|
||||
|
||||
public void completeOperation(long tenantId, long inboundId, long operatorId, String operation,
|
||||
String key, String resultJson) {
|
||||
operations.complete(tenantId, inboundId, operatorId, operation, key, resultJson);
|
||||
}
|
||||
|
||||
public LoadingPickTaskEntity findPickTask(long tenantId, long inboundId) {
|
||||
return picks.selectOne(new LambdaQueryWrapper<LoadingPickTaskEntity>()
|
||||
.eq(LoadingPickTaskEntity::getTenantId, tenantId).eq(LoadingPickTaskEntity::getInboundId, inboundId));
|
||||
}
|
||||
public void createPickTask(LoadingPickTaskEntity task) { picks.insert(task); }
|
||||
public int changePickStatus(long tenantId, long inboundId, String from, String to, long operatorId,
|
||||
String operatorName, String remark, Integer expectedCount,
|
||||
Integer pickedCount, int version) {
|
||||
return picks.changeStatus(tenantId, inboundId, from, to, operatorId, operatorName, remark,
|
||||
expectedCount, pickedCount, version);
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
package com.baiyue.firstleg.loading.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.LoadingEvidenceFileClient;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.loading.domain.LoadingEvidenceType;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.*;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.*;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingOperationEntity;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.loading.domain.PickingStatus;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.PickingResponse;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper.Snapshot;
|
||||
import com.baiyue.firstleg.loading.repository.LoadingRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.util.HexFormat;
|
||||
import java.util.List;
|
||||
|
||||
/** 装柜证据、装柜完成和派送完成;不检查证据完整度,不阻断业务流转。 */
|
||||
@Service
|
||||
public class LoadingDeliveryService {
|
||||
private static final String UPSERT_EVIDENCE = "UPSERT_LOADING_EVIDENCE";
|
||||
private static final String COMPLETE_LOADING = "COMPLETE_LOADING";
|
||||
private static final String COMPLETE_DELIVERY = "COMPLETE_DELIVERY";
|
||||
|
||||
@Resource private LoadingRepository repository;
|
||||
@Resource private InboundStatusHistoryMapper histories;
|
||||
@Resource private LoadingEvidenceFileClient files;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse upsertEvidence(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, String key, UpsertEvidenceRequest request) {
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, UPSERT_EVIDENCE, key, hash);
|
||||
if (replay != null) return replay;
|
||||
requireLoadingStage(tenantId, inboundId);
|
||||
if (!LoadingEvidenceType.VALUES.contains(request.evidenceType())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "不支持的装柜证据类型");
|
||||
}
|
||||
var file = request.fileId() == null ? null : validateFile(tenantId, inboundId, request.fileId(), request.evidenceType());
|
||||
String fileName = file == null ? request.fileName() : file.fileName();
|
||||
LoadingEvidenceEntity existing = repository.findEvidence(tenantId, inboundId, request.evidenceType());
|
||||
if (existing == null) {
|
||||
if (request.expectedVersion() != 0) throw concurrent();
|
||||
LoadingEvidenceEntity entity = new LoadingEvidenceEntity();
|
||||
entity.setTenantId(tenantId); entity.setInboundId(inboundId);
|
||||
entity.setEvidenceType(request.evidenceType()); entity.setFileId(request.fileId());
|
||||
entity.setFileName(fileName); entity.setSealNo(request.sealNo()); entity.setRemark(request.remark());
|
||||
entity.setVersion(0); entity.setCreatedBy(operatorId); entity.setCreatedByName(operatorName);
|
||||
entity.setUpdatedBy(operatorId); entity.setUpdatedByName(operatorName);
|
||||
repository.createEvidence(entity);
|
||||
} else if (repository.updateEvidence(tenantId, inboundId, request.evidenceType(), request.fileId(), fileName,
|
||||
request.sealNo(), request.remark(), operatorId, operatorName, request.expectedVersion()) != 1) {
|
||||
throw concurrent();
|
||||
}
|
||||
LoadingActionResponse result = new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId));
|
||||
return complete(tenantId, inboundId, operatorId, UPSERT_EVIDENCE, key, result);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse completeLoading(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, String key, CompleteLoadingRequest request) {
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, COMPLETE_LOADING, key, hash);
|
||||
if (replay != null) return replay;
|
||||
Snapshot inbound = requireInbound(tenantId, inboundId);
|
||||
if ("LOADED".equals(inbound.processStatus()) || "DELIVERED".equals(inbound.processStatus())) {
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse(inbound.processStatus(), evidenceResponse(tenantId, inboundId)));
|
||||
}
|
||||
if (!"LOADING".equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, "当前订单不在装柜中");
|
||||
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
|
||||
addHistory(tenantId, inboundId, "LOADING", "LOADED", COMPLETE_LOADING, request.remark(), operatorId, operatorName);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, inboundId)));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse completeDelivery(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, String key, CompleteDeliveryRequest request) {
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, COMPLETE_DELIVERY, key, hash);
|
||||
if (replay != null) return replay;
|
||||
Snapshot inbound = requireInbound(tenantId, inboundId);
|
||||
if ("DELIVERED".equals(inbound.processStatus())) {
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_DELIVERY, key,
|
||||
new LoadingActionResponse("DELIVERED", evidenceResponse(tenantId, inboundId)));
|
||||
}
|
||||
if (!"LOADED".equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, "当前订单不在装柜完成");
|
||||
if (repository.completeDelivery(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
|
||||
addHistory(tenantId, inboundId, "LOADED", "DELIVERED", COMPLETE_DELIVERY, request.remark(), operatorId, operatorName);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_DELIVERY, key,
|
||||
new LoadingActionResponse("DELIVERED", evidenceResponse(tenantId, inboundId)));
|
||||
}
|
||||
|
||||
public EvidenceListResponse evidence(long tenantId, long inboundId) {
|
||||
requireLoadingStage(tenantId, inboundId);
|
||||
return evidenceResponse(tenantId, inboundId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PickingResponse startPicking(long tenantId, long inboundId, long operatorId, String operatorName,
|
||||
String key, PickingRequest request) {
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, "START_PICKING", key, hash);
|
||||
if (replay != null) return pickResponse(tenantId, inboundId);
|
||||
requireLoadingStage(tenantId, inboundId);
|
||||
LoadingPickTaskEntity task = repository.findPickTask(tenantId, inboundId);
|
||||
if (task == null) {
|
||||
if (request.expectedVersion() != 0) throw concurrent();
|
||||
task = new LoadingPickTaskEntity(); task.setTenantId(tenantId); task.setInboundId(inboundId);
|
||||
task.setStatus(PickingStatus.IN_PROGRESS); task.setVersion(0); task.setExpectedCount(request.expectedCount());
|
||||
task.setPickedCount(request.pickedCount());
|
||||
task.setRemark(request.remark()); task.setUpdatedBy(operatorId); task.setUpdatedByName(operatorName);
|
||||
repository.createPickTask(task);
|
||||
} else if (repository.changePickStatus(tenantId, inboundId, task.getStatus(), PickingStatus.IN_PROGRESS,
|
||||
operatorId, operatorName, request.remark(), request.expectedCount(), request.pickedCount(), request.expectedVersion()) != 1) throw concurrent();
|
||||
PickingResponse result = pickResponse(tenantId, inboundId);
|
||||
complete(tenantId, inboundId, operatorId, "START_PICKING", key,
|
||||
new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId)));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PickingResponse completePicking(long tenantId, long inboundId, long operatorId, String operatorName,
|
||||
String key, PickingRequest request) {
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, "COMPLETE_PICKING", key, hash);
|
||||
if (replay != null) return pickResponse(tenantId, inboundId);
|
||||
requireLoadingStage(tenantId, inboundId);
|
||||
LoadingPickTaskEntity task = repository.findPickTask(tenantId, inboundId);
|
||||
if (task == null) {
|
||||
task = new LoadingPickTaskEntity(); task.setTenantId(tenantId); task.setInboundId(inboundId);
|
||||
task.setStatus(PickingStatus.COMPLETED); task.setVersion(0); task.setExpectedCount(request.expectedCount());
|
||||
task.setPickedCount(request.pickedCount());
|
||||
task.setRemark(request.remark()); task.setUpdatedBy(operatorId); task.setUpdatedByName(operatorName);
|
||||
repository.createPickTask(task);
|
||||
} else if (repository.changePickStatus(tenantId, inboundId,
|
||||
task.getStatus(), PickingStatus.COMPLETED, operatorId, operatorName, request.remark(), request.expectedCount(), request.pickedCount(), request.expectedVersion()) != 1) throw concurrent();
|
||||
PickingResponse result = pickResponse(tenantId, inboundId);
|
||||
complete(tenantId, inboundId, operatorId, "COMPLETE_PICKING", key,
|
||||
new LoadingActionResponse(requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId)));
|
||||
return result;
|
||||
}
|
||||
|
||||
public PickingResponse picking(long tenantId, long inboundId) {
|
||||
requireInbound(tenantId, inboundId);
|
||||
return pickResponse(tenantId, inboundId);
|
||||
}
|
||||
|
||||
private PickingResponse pickResponse(long tenantId, long inboundId) {
|
||||
LoadingPickTaskEntity task = repository.findPickTask(tenantId, inboundId);
|
||||
if (task == null) return null;
|
||||
return new PickingResponse(task.getId(), task.getInboundId(), task.getStatus(), task.getVersion(),
|
||||
task.getExpectedCount(), task.getPickedCount(), task.getRemark());
|
||||
}
|
||||
|
||||
private LoadingEvidenceFileClient.FileMetadata validateFile(long tenantId, long inboundId, long fileId, String evidenceType) {
|
||||
try {
|
||||
var response = files.validate(internalToken, tenantId, inboundId, fileId);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| !("LOADING_" + evidenceType).equals(response.data().businessType())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "证据文件不存在或不属于当前订单");
|
||||
}
|
||||
return response.data();
|
||||
} catch (FeignException ex) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
private Snapshot requireInbound(long tenantId, long inboundId) {
|
||||
Snapshot value = repository.findInbound(tenantId, inboundId);
|
||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "订单不存在");
|
||||
return value;
|
||||
}
|
||||
|
||||
private Snapshot requireLoadingStage(long tenantId, long inboundId) {
|
||||
Snapshot value = requireInbound(tenantId, inboundId);
|
||||
if (!("LOADING".equals(value.processStatus()) || "LOADED".equals(value.processStatus())
|
||||
|| "DELIVERED".equals(value.processStatus()))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "订单尚未进入装柜阶段");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private EvidenceListResponse evidenceResponse(long tenantId, long inboundId) {
|
||||
List<EvidenceResponse> values = repository.listEvidence(tenantId, inboundId).stream().map(e ->
|
||||
new EvidenceResponse(e.getId(), e.getInboundId(), e.getTenantId(), e.getEvidenceType(), e.getFileId(),
|
||||
e.getFileName(), e.getSealNo(), e.getRemark(), e.getVersion(), e.getCreatedAt(), e.getUpdatedAt())).toList();
|
||||
return new EvidenceListResponse(inboundId, values);
|
||||
}
|
||||
|
||||
private LoadingActionResponse claimOrReplay(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key, String hash) {
|
||||
validateKey(key);
|
||||
if (repository.claimOperation(tenantId, inboundId, operatorId, operation, key, hash)) return null;
|
||||
LoadingOperationEntity existing = repository.findOperation(tenantId, inboundId, operatorId, operation, key);
|
||||
if (existing == null || !hash.equals(existing.getRequestHash())) throw new BizException(ResultCode.CONFLICT, "幂等键已用于其他请求");
|
||||
if (!"SUCCESS".equals(existing.getStatus()) || !StringUtils.hasText(existing.getResultJson())) throw new BizException(ResultCode.CONFLICT, "相同请求正在处理中,请稍后重试");
|
||||
try { return objectMapper.readValue(existing.getResultJson(), LoadingActionResponse.class); }
|
||||
catch (JsonProcessingException e) { throw new IllegalStateException("无法读取装柜幂等结果", e); }
|
||||
}
|
||||
|
||||
private LoadingActionResponse complete(long tenantId, long inboundId, long operatorId, String operation,
|
||||
String key, LoadingActionResponse result) {
|
||||
try { repository.completeOperation(tenantId, inboundId, operatorId, operation, key, objectMapper.writeValueAsString(result)); }
|
||||
catch (JsonProcessingException e) { throw new IllegalStateException("无法保存装柜幂等结果", e); }
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addHistory(long tenantId, long inboundId, String from, String to, String action, String reason,
|
||||
long operatorId, String operatorName) {
|
||||
InboundStatusHistoryEntity history = new InboundStatusHistoryEntity();
|
||||
history.setTenantId(tenantId); history.setInboundId(inboundId); history.setFromStatus(from); history.setToStatus(to);
|
||||
history.setAction(action); history.setReason(reason); history.setOperatorId(operatorId); history.setOperatorName(operatorName);
|
||||
histories.insert(history);
|
||||
}
|
||||
|
||||
private String hash(Object request) {
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(objectMapper.writeValueAsString(request).getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(digest);
|
||||
} catch (Exception e) { throw new IllegalStateException("无法计算幂等请求摘要", e); }
|
||||
}
|
||||
|
||||
private static void validateKey(String key) {
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "Idempotency-Key不能为空且不能超过128个字符");
|
||||
}
|
||||
private static BizException concurrent() { return new BizException(ResultCode.CONFLICT, "数据已被其他人修改,请刷新后重试"); }
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import org.apache.ibatis.annotations.Update;
|
||||
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET process_status = 'WAIT_PRICING', measure_review_status = 'APPROVED',
|
||||
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
|
||||
inbounded_at = COALESCE(inbounded_at, CURRENT_TIMESTAMP(3)),
|
||||
updated_by = #{operatorId}, version = version + 1
|
||||
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
|
||||
AND process_status = 'MEASURE_PENDING' AND measure_review_status = 'PENDING'
|
||||
|
||||
@@ -34,8 +34,6 @@ public class InboundMeasurementService {
|
||||
private static final String WEIGHT_NEEDS_MANUAL = "NEEDS_MANUAL";
|
||||
/** 测量完成后的默认普通托盘类型;实际拆托和库位占用由提交编排服务执行。 */
|
||||
private static final String RECOMMENDED_PALLET_TYPE = "STANDARD";
|
||||
private static final Set<String> EDITABLE_INBOUND_STATUSES =
|
||||
Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
|
||||
private static final BigDecimal LB_TO_KG = new BigDecimal("0.45359237");
|
||||
|
||||
@Resource
|
||||
@@ -58,7 +56,6 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long operatorId,
|
||||
InboundMeasurementRequests.SaveDraftRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
requireEditable(inbound);
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
tenantId, inboundId, request.measurementId(), request.version(),
|
||||
MeasurementBatchService.SOURCE_MANUAL, "PLATFORM", null, null, null,
|
||||
@@ -78,35 +75,42 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long operatorId,
|
||||
InboundMeasurementRequests.SubmitRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
requireEditable(inbound);
|
||||
InboundMeasurementEntity measurement = requireMeasurement(tenantId, inboundId, request.measurementId());
|
||||
requireVersion(request.version(), measurement.getVersion());
|
||||
if (!Set.of(MeasurementBatchService.STATUS_DRAFT, MeasurementBatchService.STATUS_RECEIVED)
|
||||
boolean alreadyAdvanced = !Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED")
|
||||
.contains(inbound.getProcessStatus());
|
||||
if (!alreadyAdvanced && !Set.of(MeasurementBatchService.STATUS_DRAFT, MeasurementBatchService.STATUS_RECEIVED)
|
||||
.contains(measurement.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当前测量批次不能重复提交审核");
|
||||
}
|
||||
List<InboundMeasurementItemEntity> items = repository.listItems(tenantId, measurement.getId());
|
||||
if (items.isEmpty()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "测量明细不能为空");
|
||||
}
|
||||
if (items.stream().noneMatch(item -> item.getSourceImageFileId() != null)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "至少需要一张测量图片");
|
||||
}
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
||||
BigDecimal confirmedWeight = confirmedWeightForSubmission(weights);
|
||||
|
||||
measurement.setStatus(STATUS_SUBMITTED);
|
||||
if (!repository.updateMeasurement(measurement)) {
|
||||
throw concurrentModification();
|
||||
if (!alreadyAdvanced) {
|
||||
measurement.setStatus(STATUS_SUBMITTED);
|
||||
if (!repository.updateMeasurement(measurement)) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
}
|
||||
|
||||
inbound.setProcessStatus("MEASURE_PENDING");
|
||||
inbound.setMeasureReviewStatus("PENDING");
|
||||
inbound.setTotalPackageCount(measurement.getTotalPackageCount());
|
||||
inbound.setActualGrossWeightKg(confirmedWeight);
|
||||
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
||||
inbound.setVolumeWeightKg(measurement.getVolumeWeightKg());
|
||||
inbound.setMeasureCompletedAt(LocalDateTime.now());
|
||||
if (!alreadyAdvanced) {
|
||||
inbound.setProcessStatus("MEASURE_PENDING");
|
||||
inbound.setMeasureReviewStatus("PENDING");
|
||||
inbound.setMeasureCompletedAt(LocalDateTime.now());
|
||||
}
|
||||
if (measurement.getTotalPackageCount() != null) {
|
||||
inbound.setTotalPackageCount(measurement.getTotalPackageCount());
|
||||
}
|
||||
if (confirmedWeight != null) {
|
||||
inbound.setActualGrossWeightKg(confirmedWeight);
|
||||
}
|
||||
if (measurement.getTotalVolumeCbm() != null) {
|
||||
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
||||
}
|
||||
if (measurement.getVolumeWeightKg() != null) {
|
||||
inbound.setVolumeWeightKg(measurement.getVolumeWeightKg());
|
||||
}
|
||||
inbound.setUpdatedBy(operatorId);
|
||||
if (!repository.updateInbound(inbound)) {
|
||||
throw concurrentModification();
|
||||
@@ -121,7 +125,6 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long operatorId,
|
||||
InboundMeasurementRequests.CreateWeightResultRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
requireEditable(inbound);
|
||||
if (repository.findWeightByFile(tenantId, inboundId, request.fileId()) != null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "同一称重图片不能重复计重");
|
||||
}
|
||||
@@ -150,7 +153,6 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long resultId, long operatorId, String operatorName,
|
||||
InboundMeasurementRequests.CorrectWeightResultRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
requireEditable(inbound);
|
||||
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
|
||||
requireVersion(request.version(), weight.getVersion());
|
||||
String adjustedUnit = normalizeUnit(request.adjustedWeightUnit());
|
||||
@@ -181,7 +183,6 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long resultId, long operatorId, String operatorName,
|
||||
InboundMeasurementRequests.ConfirmWeightResultRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
requireEditable(inbound);
|
||||
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
|
||||
requireVersion(request.version(), weight.getVersion());
|
||||
if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) {
|
||||
@@ -225,18 +226,12 @@ public class InboundMeasurementService {
|
||||
}
|
||||
|
||||
private BigDecimal confirmedWeightForSubmission(List<InboundWeightResultEntity> weights) {
|
||||
if (weights.isEmpty()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "至少需要一张已确认的称重图片");
|
||||
}
|
||||
if (weights.stream().anyMatch(weight -> !WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus()))) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "所有有效称重图片必须先完成确认");
|
||||
}
|
||||
BigDecimal total = weights.stream().map(InboundWeightResultEntity::getFinalWeightKg)
|
||||
BigDecimal total = weights.stream()
|
||||
.filter(weight -> WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus()))
|
||||
.map(InboundWeightResultEntity::getFinalWeightKg)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add).setScale(3, RoundingMode.HALF_UP);
|
||||
if (!isPositive(total)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "确认后的实际总重量必须大于 0");
|
||||
}
|
||||
return total;
|
||||
return isPositive(total) ? total : null;
|
||||
}
|
||||
|
||||
private void insertRevision(InboundWeightResultEntity weight, int revisionNo, String action,
|
||||
@@ -328,12 +323,6 @@ public class InboundMeasurementService {
|
||||
return weight;
|
||||
}
|
||||
|
||||
private void requireEditable(InboundOrderEntity inbound) {
|
||||
if (!EDITABLE_INBOUND_STATUSES.contains(inbound.getProcessStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当前入库状态不允许维护测量数据");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireVersion(Integer expected, Integer actual) {
|
||||
if (expected == null || !expected.equals(actual)) {
|
||||
throw concurrentModification();
|
||||
|
||||
@@ -38,7 +38,8 @@ public class MeasurementBatchService {
|
||||
|
||||
@Transactional
|
||||
public InboundMeasurementEntity saveBatch(MeasurementBatchCommand command) {
|
||||
if (repository.findInbound(command.tenantId(), command.inboundId()) == null) {
|
||||
var inbound = repository.findInbound(command.tenantId(), command.inboundId());
|
||||
if (inbound == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
}
|
||||
validateSource(command);
|
||||
@@ -50,7 +51,13 @@ public class MeasurementBatchService {
|
||||
applyTotals(measurement, calculatedItems);
|
||||
repository.insertMeasurement(measurement);
|
||||
} else {
|
||||
measurement = requireEditableManualBatch(command);
|
||||
measurement = requireEditableBatch(command);
|
||||
// 修正待测量或待审核资料时重新形成可提交的内部草稿;已入库订单只更新资料,
|
||||
// 不回退其主状态,也不把已经审核的批次重新标成待审核。
|
||||
if (Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED")
|
||||
.contains(inbound.getProcessStatus())) {
|
||||
measurement.setStatus(STATUS_DRAFT);
|
||||
}
|
||||
applyHeader(measurement, command);
|
||||
applyTotals(measurement, calculatedItems);
|
||||
if (!repository.updateMeasurement(measurement)) {
|
||||
@@ -90,15 +97,12 @@ public class MeasurementBatchService {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
private InboundMeasurementEntity requireEditableManualBatch(MeasurementBatchCommand command) {
|
||||
private InboundMeasurementEntity requireEditableBatch(MeasurementBatchCommand command) {
|
||||
InboundMeasurementEntity measurement = repository.findMeasurement(
|
||||
command.tenantId(), command.inboundId(), command.measurementId());
|
||||
if (measurement == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "测量批次不存在");
|
||||
}
|
||||
if (!SOURCE_MANUAL.equals(measurement.getSourceType()) || !STATUS_DRAFT.equals(measurement.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "仅人工草稿允许直接修改");
|
||||
}
|
||||
if (!Integer.valueOf(command.expectedVersion()).equals(measurement.getVersion())) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import org.springframework.util.StringUtils;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 测量审核应用服务。审核是 MEASURE_PENDING 到计费阶段的唯一正常推进关口。
|
||||
* 测量审核应用服务。客服确认测量数据后将入库单推进到产品主状态 WAIT_SPLIT;
|
||||
* 计费、库位和其他资料不作为本版本的强制门禁。
|
||||
*/
|
||||
@Service
|
||||
public class MeasurementReviewService {
|
||||
@@ -79,7 +80,7 @@ public class MeasurementReviewService {
|
||||
repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), result,
|
||||
request.reason(), false, null, false, operatorId, operatorName));
|
||||
repository.insertHistory(history(tenantId, inboundId, "MEASURE_PENDING",
|
||||
approved ? "WAIT_PRICING" : "MEASURE_REJECTED",
|
||||
approved ? "WAIT_SPLIT" : "MEASURE_REJECTED",
|
||||
approved ? "MEASURE_APPROVE" : "MEASURE_REJECT", request.reason(), operatorId, operatorName));
|
||||
idempotency.complete(context, inboundId);
|
||||
}
|
||||
|
||||
+2
-3
@@ -11,15 +11,14 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class MeasurementSubmissionService {
|
||||
@Resource
|
||||
private InboundMeasurementService measurements;
|
||||
@Resource
|
||||
private MeasurementLocationOccupationService locations;
|
||||
|
||||
@Transactional
|
||||
public InboundMeasurementResponses.MeasurementSubmitted submit(
|
||||
long tenantId, long inboundId, long operatorId,
|
||||
InboundMeasurementRequests.SubmitRequest request) {
|
||||
var result = measurements.submit(tenantId, inboundId, operatorId, request);
|
||||
locations.occupy(tenantId, inboundId, operatorId, result.measurementId(), result.version());
|
||||
// 当前版本不把托盘/库位占用作为测量提交的强制门禁;入库主状态由客服确认测量时推进,
|
||||
// 仓储资源可在后续作业阶段继续维护。
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.baiyue.firstleg.split.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.SmartSplitRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitResponses.PreloadPlanResponse;
|
||||
import com.baiyue.firstleg.split.dto.SplitResponses.SplitActionResponse;
|
||||
import com.baiyue.firstleg.split.service.SplitPreloadService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 平台端分柜、预装清单和装柜中主状态推进接口。 */
|
||||
@Tag(name = "订单分柜与预装")
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/inbounds")
|
||||
public class PlatformSplitPreloadController {
|
||||
@Resource private SplitPreloadService service;
|
||||
|
||||
@Operation(summary = "智能分柜并生成或覆盖预装清单")
|
||||
@PostMapping("/{inboundId}/split/smart")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:split')")
|
||||
@BusinessAudit(event = "INBOUND_SMART_SPLIT", resourceType = "INBOUND", resourceId = "#inboundId",
|
||||
description = "智能分柜")
|
||||
public ApiResponse<SplitActionResponse> smart(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
|
||||
@PathVariable long inboundId, @RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody SmartSplitRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.smartSplit(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "提交预装清单确认")
|
||||
@PostMapping("/{inboundId}/preload/confirming")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:preload')")
|
||||
@BusinessAudit(event = "INBOUND_PRELOAD_CONFIRMING", resourceType = "INBOUND", resourceId = "#inboundId",
|
||||
description = "提交预装清单确认")
|
||||
public ApiResponse<SplitActionResponse> confirming(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
|
||||
@PathVariable long inboundId, @RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ConfirmPreloadRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.startConfirming(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "确认预装清单并进入装柜中")
|
||||
@PostMapping("/{inboundId}/preload/confirm")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:preload')")
|
||||
@BusinessAudit(event = "INBOUND_CONFIRM_PRELOAD", resourceType = "INBOUND", resourceId = "#inboundId",
|
||||
description = "确认预装清单")
|
||||
public ApiResponse<SplitActionResponse> confirm(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
|
||||
@PathVariable long inboundId, @RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ConfirmPreloadRequest request) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.confirmPreload(tenantId, inboundId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询预装清单")
|
||||
@GetMapping("/{inboundId}/preload")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:preload-view')")
|
||||
public ApiResponse<PreloadPlanResponse> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
|
||||
@PathVariable long inboundId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.detail(tenantId, inboundId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.firstleg.split.domain;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/** 预装清单子状态,固定为三态。 */
|
||||
public final class PreloadStatus {
|
||||
public static final String PENDING = "PENDING";
|
||||
public static final String CONFIRMING = "CONFIRMING";
|
||||
public static final String CONFIRMED = "CONFIRMED";
|
||||
public static final Set<String> VALUES = Set.of(PENDING, CONFIRMING, CONFIRMED);
|
||||
private PreloadStatus() {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.firstleg.split.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/** 分柜与预装清单写入请求。资料字段本版本不做完整性门禁。 */
|
||||
public final class SplitRequests {
|
||||
private SplitRequests() {}
|
||||
|
||||
public record SmartSplitRequest(
|
||||
@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
List<@Valid PreloadItem> items,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record ConfirmPreloadRequest(@NotNull @PositiveOrZero Integer expectedVersion) {}
|
||||
|
||||
public record PreloadItem(
|
||||
@Size(max = 64) String containerCode,
|
||||
@Size(max = 128) String cargoReference,
|
||||
@Positive Integer packageCount,
|
||||
@PositiveOrZero BigDecimal volumeCbm,
|
||||
@Size(max = 500) String remark) {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.firstleg.split.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class SplitResponses {
|
||||
private SplitResponses() {}
|
||||
|
||||
public record PreloadPlanResponse(Long id, Long inboundId, Long tenantId, String status,
|
||||
Integer version, List<SplitRequests.PreloadItem> items,
|
||||
String remark, LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt) {}
|
||||
|
||||
public record SplitActionResponse(String mainStatus, PreloadPlanResponse preloadPlan) {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.firstleg.split.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_preload_plan")
|
||||
public class PreloadPlanEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private String status;
|
||||
private Integer version;
|
||||
private String itemsJson;
|
||||
private String remark;
|
||||
private Long createdBy;
|
||||
private String createdByName;
|
||||
private Long updatedBy;
|
||||
private String updatedByName;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.firstleg.split.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_split_operation")
|
||||
public class SplitOperationEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Long operatorId;
|
||||
private String operation;
|
||||
private String idempotencyKey;
|
||||
private String status;
|
||||
private String resultJson;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.firstleg.split.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.split.entity.PreloadPlanEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface PreloadPlanMapper extends BaseMapper<PreloadPlanEntity> {
|
||||
@Update("UPDATE fl_preload_plan SET items_json=#{itemsJson}, remark=#{remark}, status='PENDING', "
|
||||
+ "updated_by=#{operatorId}, updated_by_name=#{operatorName}, version=version+1 "
|
||||
+ "WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND version=#{expectedVersion}")
|
||||
int replace(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("itemsJson") String itemsJson, @Param("remark") String remark,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName,
|
||||
@Param("expectedVersion") int expectedVersion);
|
||||
|
||||
@Update("UPDATE fl_preload_plan SET status=#{toStatus}, updated_by=#{operatorId}, "
|
||||
+ "updated_by_name=#{operatorName}, version=version+1 "
|
||||
+ "WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND status=#{fromStatus} "
|
||||
+ "AND version=#{expectedVersion}")
|
||||
int changeStatus(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName,
|
||||
@Param("expectedVersion") int expectedVersion);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.firstleg.split.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface SplitInboundMapper {
|
||||
@Select("SELECT id, tenant_id AS tenantId, process_status AS processStatus, version FROM fl_inbound_order "
|
||||
+ "WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
|
||||
InboundSnapshot find(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, "
|
||||
+ "version=version+1 WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 "
|
||||
+ "AND process_status='WAIT_SPLIT' AND version=#{expectedVersion}")
|
||||
int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
|
||||
|
||||
record InboundSnapshot(Long id, Long tenantId, String processStatus, Integer version) {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.firstleg.split.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.split.entity.SplitOperationEntity;
|
||||
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 SplitOperationMapper extends BaseMapper<SplitOperationEntity> {
|
||||
@Insert("INSERT IGNORE INTO fl_split_operation "
|
||||
+ "(id,tenant_id,inbound_id,operator_id,operation,idempotency_key,status,created_at) "
|
||||
+ "VALUES(#{id},#{tenantId},#{inboundId},#{operatorId},#{operation},#{key},'PROCESSING',CURRENT_TIMESTAMP(3))")
|
||||
int claim(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("operatorId") long operatorId,
|
||||
@Param("operation") String operation, @Param("key") String key);
|
||||
|
||||
@Update("UPDATE fl_split_operation SET status='SUCCESS', result_json=#{resultJson} "
|
||||
+ "WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND operator_id=#{operatorId} "
|
||||
+ "AND operation=#{operation} AND idempotency_key=#{key} AND status='PROCESSING'")
|
||||
int complete(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("operatorId") long operatorId, @Param("operation") String operation,
|
||||
@Param("key") String key, @Param("resultJson") String resultJson);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.baiyue.firstleg.split.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.split.entity.PreloadPlanEntity;
|
||||
import com.baiyue.firstleg.split.entity.SplitOperationEntity;
|
||||
import com.baiyue.firstleg.split.mapper.PreloadPlanMapper;
|
||||
import com.baiyue.firstleg.split.mapper.SplitInboundMapper;
|
||||
import com.baiyue.firstleg.split.mapper.SplitOperationMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
|
||||
@Repository
|
||||
public class SplitRepository {
|
||||
@Resource private SplitInboundMapper inbounds;
|
||||
@Resource private PreloadPlanMapper plans;
|
||||
@Resource private SplitOperationMapper operations;
|
||||
|
||||
public SplitInboundMapper.InboundSnapshot findInbound(long tenantId, long inboundId) {
|
||||
return inbounds.find(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public int beginLoading(long tenantId, long inboundId, long operatorId, int expectedVersion) {
|
||||
return inbounds.beginLoading(tenantId, inboundId, operatorId, expectedVersion);
|
||||
}
|
||||
|
||||
public PreloadPlanEntity findPlan(long tenantId, long inboundId) {
|
||||
return plans.selectOne(new LambdaQueryWrapper<PreloadPlanEntity>()
|
||||
.eq(PreloadPlanEntity::getTenantId, tenantId)
|
||||
.eq(PreloadPlanEntity::getInboundId, inboundId));
|
||||
}
|
||||
|
||||
public void createPlan(PreloadPlanEntity plan) {
|
||||
plans.insert(plan);
|
||||
}
|
||||
|
||||
public int replacePlan(long tenantId, long inboundId, String itemsJson, String remark,
|
||||
long operatorId, String operatorName, int expectedVersion) {
|
||||
return plans.replace(tenantId, inboundId, itemsJson, remark, operatorId, operatorName, expectedVersion);
|
||||
}
|
||||
|
||||
public int changePlanStatus(long tenantId, long inboundId, String fromStatus, String toStatus,
|
||||
long operatorId, String operatorName, int expectedVersion) {
|
||||
return plans.changeStatus(tenantId, inboundId, fromStatus, toStatus,
|
||||
operatorId, operatorName, expectedVersion);
|
||||
}
|
||||
|
||||
public SplitOperationEntity findOperation(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key) {
|
||||
return operations.selectOne(new LambdaQueryWrapper<SplitOperationEntity>()
|
||||
.eq(SplitOperationEntity::getTenantId, tenantId)
|
||||
.eq(SplitOperationEntity::getInboundId, inboundId)
|
||||
.eq(SplitOperationEntity::getOperatorId, operatorId)
|
||||
.eq(SplitOperationEntity::getOperation, operation)
|
||||
.eq(SplitOperationEntity::getIdempotencyKey, key));
|
||||
}
|
||||
|
||||
public void saveOperation(SplitOperationEntity operation) {
|
||||
operations.insert(operation);
|
||||
}
|
||||
|
||||
public boolean claimOperation(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key) {
|
||||
return operations.claim(IdWorker.getId(), tenantId, inboundId, operatorId, operation, key) == 1;
|
||||
}
|
||||
|
||||
public void completeOperation(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key, String resultJson) {
|
||||
operations.complete(tenantId, inboundId, operatorId, operation, key, resultJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.baiyue.firstleg.split.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.split.domain.PreloadStatus;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.PreloadItem;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.SmartSplitRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitResponses.PreloadPlanResponse;
|
||||
import com.baiyue.firstleg.split.dto.SplitResponses.SplitActionResponse;
|
||||
import com.baiyue.firstleg.split.entity.PreloadPlanEntity;
|
||||
import com.baiyue.firstleg.split.entity.SplitOperationEntity;
|
||||
import com.baiyue.firstleg.split.mapper.SplitInboundMapper.InboundSnapshot;
|
||||
import com.baiyue.firstleg.split.repository.SplitRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 分柜、预装清单及其主状态推进;不校验资料完整度和客户确认。 */
|
||||
@Service
|
||||
public class SplitPreloadService {
|
||||
private static final String SMART_SPLIT = "SMART_SPLIT";
|
||||
private static final String START_CONFIRMING = "START_PRELOAD_CONFIRMING";
|
||||
private static final String CONFIRM_PRELOAD = "CONFIRM_PRELOAD";
|
||||
|
||||
@Resource private SplitRepository repository;
|
||||
@Resource private InboundStatusHistoryMapper histories;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public SplitActionResponse smartSplit(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, String key, SmartSplitRequest request) {
|
||||
SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, SMART_SPLIT, key);
|
||||
if (replay != null) return replay;
|
||||
InboundSnapshot inbound = requireInbound(tenantId, inboundId);
|
||||
if (!"WAIT_SPLIT".equals(inbound.processStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "仅待分柜订单可以生成预装清单");
|
||||
}
|
||||
PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId);
|
||||
String itemsJson = write(request.items() == null ? List.of() : request.items());
|
||||
if (plan == null) {
|
||||
if (request.expectedVersion() != 0) throw concurrent();
|
||||
plan = new PreloadPlanEntity();
|
||||
plan.setTenantId(tenantId);
|
||||
plan.setInboundId(inboundId);
|
||||
plan.setStatus(PreloadStatus.PENDING);
|
||||
plan.setVersion(0);
|
||||
plan.setItemsJson(itemsJson);
|
||||
plan.setRemark(request.remark());
|
||||
plan.setCreatedBy(operatorId);
|
||||
plan.setCreatedByName(operatorName);
|
||||
plan.setUpdatedBy(operatorId);
|
||||
plan.setUpdatedByName(operatorName);
|
||||
repository.createPlan(plan);
|
||||
} else if (repository.replacePlan(tenantId, inboundId, itemsJson, request.remark(),
|
||||
operatorId, operatorName, request.expectedVersion()) != 1) {
|
||||
throw concurrent();
|
||||
}
|
||||
PreloadPlanResponse result = response(repository.findPlan(tenantId, inboundId));
|
||||
return complete(tenantId, inboundId, operatorId, SMART_SPLIT, key,
|
||||
new SplitActionResponse("WAIT_SPLIT", result));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SplitActionResponse startConfirming(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, String key,
|
||||
ConfirmPreloadRequest request) {
|
||||
SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, START_CONFIRMING, key);
|
||||
if (replay != null) return replay;
|
||||
InboundSnapshot inbound = requireInbound(tenantId, inboundId);
|
||||
requireStatus(inbound, "WAIT_SPLIT", "仅待分柜订单可以提交预装清单确认");
|
||||
PreloadPlanEntity plan = requirePlan(tenantId, inboundId);
|
||||
if (PreloadStatus.CONFIRMING.equals(plan.getStatus())) {
|
||||
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key,
|
||||
new SplitActionResponse("WAIT_SPLIT", response(plan)));
|
||||
}
|
||||
if (!PreloadStatus.PENDING.equals(plan.getStatus())
|
||||
|| repository.changePlanStatus(tenantId, inboundId, PreloadStatus.PENDING,
|
||||
PreloadStatus.CONFIRMING, operatorId, operatorName, request.expectedVersion()) != 1) {
|
||||
throw concurrent();
|
||||
}
|
||||
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key,
|
||||
new SplitActionResponse("WAIT_SPLIT", response(repository.findPlan(tenantId, inboundId))));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SplitActionResponse confirmPreload(long tenantId, long inboundId, long operatorId,
|
||||
String operatorName, String key,
|
||||
ConfirmPreloadRequest request) {
|
||||
SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key);
|
||||
if (replay != null) return replay;
|
||||
InboundSnapshot inbound = requireInbound(tenantId, inboundId);
|
||||
if ("LOADING".equals(inbound.processStatus())) {
|
||||
return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key,
|
||||
new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId))));
|
||||
}
|
||||
requireStatus(inbound, "WAIT_SPLIT", "仅待分柜订单可以确认预装清单");
|
||||
PreloadPlanEntity plan = requirePlan(tenantId, inboundId);
|
||||
if (PreloadStatus.CONFIRMED.equals(plan.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "预装清单已确认但订单状态尚未推进,请刷新重试");
|
||||
}
|
||||
if (repository.changePlanStatus(tenantId, inboundId, plan.getStatus(), PreloadStatus.CONFIRMED,
|
||||
operatorId, operatorName, request.expectedVersion()) != 1) {
|
||||
throw concurrent();
|
||||
}
|
||||
if (repository.beginLoading(tenantId, inboundId, operatorId, inbound.version()) != 1) {
|
||||
throw concurrent();
|
||||
}
|
||||
addHistory(tenantId, inboundId, "WAIT_SPLIT", "LOADING", CONFIRM_PRELOAD,
|
||||
operatorId, operatorName);
|
||||
return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key,
|
||||
new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId))));
|
||||
}
|
||||
|
||||
public PreloadPlanResponse detail(long tenantId, long inboundId) {
|
||||
requireInbound(tenantId, inboundId);
|
||||
return response(requirePlan(tenantId, inboundId));
|
||||
}
|
||||
|
||||
private SplitActionResponse claimOrReplay(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key) {
|
||||
validateKey(key);
|
||||
if (repository.claimOperation(tenantId, inboundId, operatorId, operation, key)) return null;
|
||||
SplitOperationEntity existing = repository.findOperation(tenantId, inboundId, operatorId, operation, key);
|
||||
if (existing == null || !"SUCCESS".equals(existing.getStatus())
|
||||
|| !StringUtils.hasText(existing.getResultJson())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "相同幂等请求正在处理中,请稍后重试");
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(existing.getResultJson(), SplitActionResponse.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("无法读取分柜幂等结果", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SplitActionResponse complete(long tenantId, long inboundId, long operatorId,
|
||||
String operation, String key, SplitActionResponse result) {
|
||||
repository.completeOperation(tenantId, inboundId, operatorId, operation, key, write(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
private InboundSnapshot requireInbound(long tenantId, long inboundId) {
|
||||
InboundSnapshot inbound = repository.findInbound(tenantId, inboundId);
|
||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "订单不存在");
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private PreloadPlanEntity requirePlan(long tenantId, long inboundId) {
|
||||
PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId);
|
||||
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "预装清单不存在");
|
||||
return plan;
|
||||
}
|
||||
|
||||
private static void requireStatus(InboundSnapshot inbound, String expected, String message) {
|
||||
if (!expected.equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
|
||||
private void addHistory(long tenantId, long inboundId, String from, String to, String action,
|
||||
long operatorId, String operatorName) {
|
||||
InboundStatusHistoryEntity history = new InboundStatusHistoryEntity();
|
||||
history.setTenantId(tenantId);
|
||||
history.setInboundId(inboundId);
|
||||
history.setFromStatus(from);
|
||||
history.setToStatus(to);
|
||||
history.setAction(action);
|
||||
history.setOperatorId(operatorId);
|
||||
history.setOperatorName(operatorName);
|
||||
histories.insert(history);
|
||||
}
|
||||
|
||||
private PreloadPlanResponse response(PreloadPlanEntity plan) {
|
||||
if (plan == null) return null;
|
||||
return new PreloadPlanResponse(plan.getId(), plan.getInboundId(), plan.getTenantId(),
|
||||
plan.getStatus(), plan.getVersion(), readItems(plan.getItemsJson()), plan.getRemark(),
|
||||
plan.getCreatedAt(), plan.getUpdatedAt());
|
||||
}
|
||||
|
||||
private List<PreloadItem> readItems(String json) {
|
||||
if (!StringUtils.hasText(json)) return List.of();
|
||||
try {
|
||||
return objectMapper.readValue(json, new TypeReference<List<PreloadItem>>() {});
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("无法读取预装清单", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String write(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("无法保存预装清单", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateKey(String key) {
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "Idempotency-Key不能为空且不能超过128个字符");
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException concurrent() {
|
||||
return new BizException(ResultCode.CONFLICT, "数据已被其他人修改,请刷新后重试");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
/* 装柜证据和派送完成;不设置证据完整度门禁,允许后续覆盖编辑。 */
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_evidence (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1..4/SEAL',
|
||||
file_id BIGINT UNSIGNED NULL COMMENT 'File 服务逻辑文件ID',
|
||||
file_name VARCHAR(255) NULL,
|
||||
seal_no VARCHAR(64) NULL,
|
||||
remark VARCHAR(1000) NULL,
|
||||
version INT NOT NULL DEFAULT 0,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_evidence_type (tenant_id, inbound_id, evidence_type),
|
||||
KEY idx_loading_evidence_time (tenant_id, inbound_id, updated_at),
|
||||
CONSTRAINT fk_loading_evidence_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
) ENGINE=InnoDB COMMENT='订单装柜四步照片和铅封证据';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(48) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
|
||||
result_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
completed_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key),
|
||||
KEY idx_loading_operation_time (tenant_id, inbound_id, created_at),
|
||||
CONSTRAINT fk_loading_operation_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
) ENGINE=InnoDB COMMENT='装柜与派送写操作幂等记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/IN_PROGRESS/COMPLETED',
|
||||
version INT NOT NULL DEFAULT 0,
|
||||
expected_count INT UNSIGNED NULL,
|
||||
picked_count INT UNSIGNED NULL,
|
||||
remark VARCHAR(1000) NULL,
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_pick_tenant_inbound (tenant_id, inbound_id),
|
||||
KEY idx_loading_pick_status (tenant_id, status, updated_at),
|
||||
CONSTRAINT fk_loading_pick_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
) ENGINE=InnoDB COMMENT='PDA拣货任务及结果';
|
||||
@@ -0,0 +1,39 @@
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
/* 三期分柜与预装清单;独立于现有测量、计费表,可重复部署。 */
|
||||
CREATE TABLE IF NOT EXISTS fl_preload_plan (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '预装清单ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单/订单ID',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/CONFIRMING/CONFIRMED',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
|
||||
items_json JSON NOT NULL COMMENT '可编辑分柜明细快照',
|
||||
remark VARCHAR(1000) NULL COMMENT '预装备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_preload_tenant_inbound (tenant_id, inbound_id),
|
||||
KEY idx_preload_status (tenant_id, status, updated_at),
|
||||
CONSTRAINT fk_preload_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
) ENGINE=InnoDB COMMENT='订单分柜预装清单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_split_operation (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(40) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT 'PROCESSING/SUCCESS',
|
||||
result_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_split_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key),
|
||||
KEY idx_split_operation_time (tenant_id, inbound_id, created_at),
|
||||
CONSTRAINT fk_split_operation_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
) ENGINE=InnoDB COMMENT='分柜操作幂等与审计索引';
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.baiyue.firstleg.loading.service;
|
||||
|
||||
import com.baiyue.firstleg.client.LoadingEvidenceFileClient;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.UpsertEvidenceRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingResponses.LoadingActionResponse;
|
||||
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper.Snapshot;
|
||||
import com.baiyue.firstleg.loading.repository.LoadingRepository;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LoadingDeliveryServiceTest {
|
||||
@Mock LoadingRepository repository;
|
||||
@Mock InboundStatusHistoryMapper histories;
|
||||
@Mock LoadingEvidenceFileClient files;
|
||||
@InjectMocks LoadingDeliveryService service;
|
||||
|
||||
@BeforeEach
|
||||
void injectObjectMapper() throws Exception {
|
||||
var field = LoadingDeliveryService.class.getDeclaredField("objectMapper");
|
||||
field.setAccessible(true);
|
||||
field.set(service, new ObjectMapper());
|
||||
var token = LoadingDeliveryService.class.getDeclaredField("internalToken");
|
||||
token.setAccessible(true);
|
||||
token.set(service, "token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesLoadingWithoutEvidenceGate() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
|
||||
when(repository.completeLoading(10, 20, 7, 3)).thenReturn(1);
|
||||
when(repository.listEvidence(10, 20)).thenReturn(List.of());
|
||||
LoadingActionResponse result = service.completeLoading(10, 20, 7, "admin", "k1", new CompleteLoadingRequest(3, null));
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADED");
|
||||
verify(histories).insert(any(InboundStatusHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void evidenceCanBeEditedAndDoesNotChangeMainStatus() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
|
||||
when(repository.findEvidence(10, 20, "PHOTO_STEP_1")).thenReturn(null);
|
||||
when(repository.listEvidence(10, 20)).thenReturn(List.of());
|
||||
LoadingActionResponse result = service.upsertEvidence(10, 20, 7, "admin", "k2",
|
||||
new UpsertEvidenceRequest(0, "PHOTO_STEP_1", null, "photo.jpg", null, "updated"));
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADING");
|
||||
verify(repository).createEvidence(any(LoadingEvidenceEntity.class));
|
||||
verify(repository, never()).completeLoading(anyLong(), anyLong(), anyLong(), anyInt());
|
||||
}
|
||||
}
|
||||
+36
-6
@@ -138,7 +138,7 @@ class InboundMeasurementServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitRejectsAnyUnconfirmedWeightWithoutChangingState() {
|
||||
void submitDoesNotBlockOnUnconfirmedWeight() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
@@ -151,13 +151,43 @@ class InboundMeasurementServiceTest {
|
||||
when(repository.listItems(1L, 30L)).thenReturn(List.of(item));
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of(
|
||||
weight(20L, "20", "KG", "PENDING_CONFIRM", 0)));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("必须先完成确认");
|
||||
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 0));
|
||||
|
||||
assertThat(result.processStatus()).isEqualTo("MEASURE_PENDING");
|
||||
assertThat(inbound.getActualGrossWeightKg()).isNull();
|
||||
verify(repository).updateMeasurement(measurement);
|
||||
verify(repository).updateInbound(inbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
|
||||
inbound.setMeasureReviewStatus("APPROVED");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
measurement.setStatus("APPROVED");
|
||||
measurement.setVersion(2);
|
||||
measurement.setTotalPackageCount(4);
|
||||
measurement.setTotalVolumeCbm(new BigDecimal("2.0000"));
|
||||
measurement.setVolumeWeightKg(new BigDecimal("334.000"));
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findMeasurement(1L, 10L, 30L)).thenReturn(measurement);
|
||||
when(repository.listItems(1L, 30L)).thenReturn(List.of());
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
|
||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||
|
||||
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 2));
|
||||
|
||||
assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(result.measureReviewStatus()).isEqualTo("APPROVED");
|
||||
assertThat(measurement.getStatus()).isEqualTo("APPROVED");
|
||||
verify(repository, never()).updateMeasurement(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
verify(repository).updateInbound(inbound);
|
||||
}
|
||||
|
||||
private InboundOrderEntity inbound(String status) {
|
||||
|
||||
@@ -104,6 +104,31 @@ class MeasurementBatchServiceTest {
|
||||
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedManualMeasurementCanBeEditedAsDraftBeforeReviewCompletes() {
|
||||
InboundMeasurementEntity existing = new InboundMeasurementEntity();
|
||||
existing.setId(100L);
|
||||
existing.setTenantId(1L);
|
||||
existing.setInboundId(10L);
|
||||
existing.setSourceType("MANUAL");
|
||||
existing.setStatus("APPROVED");
|
||||
existing.setVersion(3);
|
||||
when(repository.findMeasurement(1L, 10L, 100L)).thenReturn(existing);
|
||||
when(repository.updateMeasurement(existing)).thenReturn(true);
|
||||
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
1L, 10L, 100L, 3, "MANUAL", "PLATFORM", null, null, null,
|
||||
LocalDateTime.now(), null, "CKW-2", new BigDecimal("21"),
|
||||
new BigDecimal("0.2"), "服装", 9L, List.of(item(BigDecimal.ONE)));
|
||||
|
||||
InboundMeasurementEntity saved = service.saveBatch(command);
|
||||
|
||||
assertThat(saved.getStatus()).isEqualTo("DRAFT");
|
||||
verify(repository).updateMeasurement(existing);
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), any());
|
||||
}
|
||||
|
||||
private MeasurementBatchCommand.Item item(BigDecimal unitWeight) {
|
||||
return new MeasurementBatchCommand.Item(
|
||||
"001", "A", 2, new BigDecimal("40"), new BigDecimal("30"),
|
||||
|
||||
+7
-1
@@ -7,10 +7,12 @@ import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
@@ -70,7 +72,11 @@ class MeasurementReviewServiceTest {
|
||||
verify(fixture.repository).transitionReview(10001L, 10L, 9L, 3, true);
|
||||
verify(fixture.repository).transitionMeasurement(10001L, 10L, 20L, "APPROVED");
|
||||
verify(fixture.repository).insertReview(any());
|
||||
verify(fixture.repository).insertHistory(any());
|
||||
ArgumentCaptor<InboundStatusHistoryEntity> history =
|
||||
ArgumentCaptor.forClass(InboundStatusHistoryEntity.class);
|
||||
verify(fixture.repository).insertHistory(history.capture());
|
||||
assertThat(history.getValue().getFromStatus()).isEqualTo("MEASURE_PENDING");
|
||||
assertThat(history.getValue().getToStatus()).isEqualTo("WAIT_SPLIT");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-8
@@ -9,10 +9,9 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(MeasurementSubmissionService.class)
|
||||
@@ -38,7 +37,7 @@ class MeasurementSubmissionServiceTest {
|
||||
|
||||
assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted);
|
||||
|
||||
verify(locations).occupy(10001L, 88L, 9L, 30L, 3);
|
||||
verifyNoInteractions(locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -47,10 +46,7 @@ class MeasurementSubmissionServiceTest {
|
||||
var submitted = new InboundMeasurementResponses.MeasurementSubmitted(
|
||||
30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null);
|
||||
when(measurements.submit(10001L, 88L, 9L, request)).thenReturn(submitted);
|
||||
doThrow(new IllegalStateException("warehouse unavailable"))
|
||||
.when(locations).occupy(10001L, 88L, 9L, 30L, 3);
|
||||
|
||||
assertThatThrownBy(() -> service.submit(10001L, 88L, 9L, request))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted);
|
||||
verifyNoInteractions(locations);
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.firstleg.split.controller;
|
||||
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.SmartSplitRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformSplitPreloadControllerContractTest {
|
||||
@Test
|
||||
void shouldDeclareStableOperationsAndButtonPermissions() throws Exception {
|
||||
assertAuthority("smart", "hasAuthority('firstleg:inbound:split')", SmartSplitRequest.class);
|
||||
assertAuthority("confirming", "hasAuthority('firstleg:inbound:preload')", ConfirmPreloadRequest.class);
|
||||
assertAuthority("confirm", "hasAuthority('firstleg:inbound:preload')", ConfirmPreloadRequest.class);
|
||||
assertThat(PlatformSplitPreloadController.class
|
||||
.getDeclaredMethod("detail", com.baiyue.common.security.AuthenticatedUser.class,
|
||||
long.class, long.class).getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('firstleg:inbound:preload-view')");
|
||||
}
|
||||
|
||||
private void assertAuthority(String name, String expected, Class<?> requestType) throws Exception {
|
||||
Method method = PlatformSplitPreloadController.class.getDeclaredMethod(name,
|
||||
com.baiyue.common.security.AuthenticatedUser.class, long.class, long.class,
|
||||
String.class, requestType);
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value()).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.baiyue.firstleg.split.service;
|
||||
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.PreloadItem;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.SmartSplitRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitResponses.SplitActionResponse;
|
||||
import com.baiyue.firstleg.split.entity.PreloadPlanEntity;
|
||||
import com.baiyue.firstleg.split.mapper.SplitInboundMapper.InboundSnapshot;
|
||||
import com.baiyue.firstleg.split.repository.SplitRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SplitPreloadServiceTest {
|
||||
@Mock SplitRepository repository;
|
||||
@Mock InboundStatusHistoryMapper histories;
|
||||
@InjectMocks SplitPreloadService service;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@BeforeEach
|
||||
void objectMapper() throws Exception {
|
||||
var field = SplitPreloadService.class.getDeclaredField("objectMapper");
|
||||
field.setAccessible(true);
|
||||
field.set(service, objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void smartSplitCreatesEditablePendingPlan() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 3));
|
||||
when(repository.findPlan(10, 20)).thenReturn(null);
|
||||
SmartSplitRequest request = new SmartSplitRequest(0,
|
||||
List.of(new PreloadItem("C1", "A", 2, BigDecimal.ONE, null)), "r");
|
||||
SplitActionResponse result = service.smartSplit(10, 20, 7, "admin", "k2", request);
|
||||
assertThat(result.mainStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).createPlan(any(PreloadPlanEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmsPlanAndMovesInboundToLoading() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 4));
|
||||
PreloadPlanEntity plan = new PreloadPlanEntity();
|
||||
plan.setTenantId(10L); plan.setInboundId(20L); plan.setStatus("CONFIRMING"); plan.setVersion(1);
|
||||
plan.setItemsJson("[]");
|
||||
when(repository.findPlan(10, 20)).thenReturn(plan);
|
||||
when(repository.changePlanStatus(10, 20, "CONFIRMING", "CONFIRMED", 7, "admin", 1)).thenReturn(1);
|
||||
when(repository.beginLoading(10, 20, 7, 4)).thenReturn(1);
|
||||
SplitActionResponse result = service.confirmPreload(10, 20, 7, "admin", "k3",
|
||||
new ConfirmPreloadRequest(1));
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADING");
|
||||
verify(repository).beginLoading(10, 20, 7, 4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user