feat(lastleg): manage three delivery document categories
This commit is contained in:
+82
@@ -0,0 +1,82 @@
|
||||
package com.baiyue.lastleg.delivery.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryDocumentDtos.*;
|
||||
import com.baiyue.lastleg.delivery.model.DeliveryDocumentCategory;
|
||||
import com.baiyue.lastleg.delivery.service.DeliveryDocumentService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/documents")
|
||||
public class DeliveryDocumentController {
|
||||
@Resource private DeliveryDocumentService service;
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_LIST + "')")
|
||||
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String deliveryNo, @RequestParam String customerId) {
|
||||
scope(user, customerId); return ApiResponse.success(authorized(service.detail(customerId, deliveryNo)));
|
||||
}
|
||||
@PutMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_DOCUMENTS + "')")
|
||||
public ApiResponse<Detail> save(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String deliveryNo, @RequestParam String customerId, @Valid @RequestBody Save request) {
|
||||
scope(user, customerId); return ApiResponse.success(authorized(service.save(customerId, deliveryNo, operator(user), request)));
|
||||
}
|
||||
@PostMapping(value="/{category}/files", consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_DOCUMENTS + "')")
|
||||
public ApiResponse<File> upload(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String deliveryNo, @RequestParam String customerId,
|
||||
@PathVariable DeliveryDocumentCategory category, @RequestPart("file") MultipartFile file) {
|
||||
scope(user, customerId); return ApiResponse.success(service.upload(customerId, deliveryNo, category, operator(user), file));
|
||||
}
|
||||
@GetMapping("/history")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_LIST + "')")
|
||||
public ApiResponse<List<History>> history(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String deliveryNo, @RequestParam String customerId) {
|
||||
scope(user, customerId); return ApiResponse.success(service.history(customerId, deliveryNo).stream()
|
||||
.map(h -> new History(h.version(), h.operatorId(), h.savedAt(), authorized(h.snapshot()))).toList());
|
||||
}
|
||||
@GetMapping("/{category}/files/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_LIST + "')")
|
||||
public ResponseEntity<byte[]> download(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String deliveryNo, @RequestParam String customerId,
|
||||
@PathVariable DeliveryDocumentCategory category, @PathVariable long fileId,
|
||||
@RequestParam(required=false) Long revisionVersion) {
|
||||
scope(user, customerId);
|
||||
DeliveryDocumentService.Download downloaded = service.download(customerId, deliveryNo, category, fileId, revisionVersion);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||
.filename(downloaded.file().fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.body(downloaded.content());
|
||||
}
|
||||
private static Detail authorized(Detail detail) {
|
||||
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
boolean canManage = authentication != null && authentication.isAuthenticated()
|
||||
&& authentication.getAuthorities().stream().anyMatch(a -> OverseasPermissions.DELIVERY_DOCUMENTS.equals(a.getAuthority()));
|
||||
return new Detail(detail.deliveryNo(), detail.customerId(), detail.version(), detail.bolNo(),
|
||||
detail.uploadedCategoryCount(), detail.totalCategoryCount(), detail.categories(),
|
||||
canManage ? detail.allowedActions() : List.of(), detail.replayed());
|
||||
}
|
||||
private static void scope(AuthenticatedUser user, String customerId) {
|
||||
if (user == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (!"PLATFORM".equals(user.appCode()) || !Long.valueOf(0).equals(user.tenantId()))
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "仅平台可管理派送资料");
|
||||
try { if (Long.parseLong(customerId) <= 0) throw new NumberFormatException(); }
|
||||
catch (NumberFormatException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请指定有效客户"); }
|
||||
}
|
||||
private static String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : user.userId().toString(); }
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.lastleg.delivery.dto;
|
||||
|
||||
import com.baiyue.lastleg.delivery.model.DeliveryDocumentCategory;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class DeliveryDocumentDtos {
|
||||
private DeliveryDocumentDtos() { }
|
||||
public record Save(@NotNull @Min(0) Long expectedVersion,
|
||||
@NotBlank @Size(max=128) String idempotencyKey,
|
||||
@Size(max=128) String bolNo,
|
||||
@NotNull @Size(max=3) List<@NotNull @Valid Binding> documents) { }
|
||||
public record Binding(@NotNull DeliveryDocumentCategory category,
|
||||
@NotNull @Size(max=20) List<@NotNull @Min(1) Long> fileIds) { }
|
||||
public record File(Long fileId, String fileName, String contentType, long sizeBytes,
|
||||
LocalDateTime uploadedAt) { }
|
||||
public record Category(DeliveryDocumentCategory category, String status, int fileCount, List<File> files) { }
|
||||
public record CategorySummary(DeliveryDocumentCategory category, String status, int fileCount) { }
|
||||
public record Summary(int uploadedCategoryCount, int totalCategoryCount, List<CategorySummary> categories) { }
|
||||
public record Detail(String deliveryNo, String customerId, long version, String bolNo,
|
||||
int uploadedCategoryCount, int totalCategoryCount, List<Category> categories,
|
||||
List<String> allowedActions, boolean replayed) { }
|
||||
public record History(long version, String operatorId, LocalDateTime savedAt, Detail snapshot) { }
|
||||
}
|
||||
+2
-1
@@ -24,7 +24,8 @@ public final class DeliveryResponses {
|
||||
int packageCount, BigDecimal totalWeightKg, BigDecimal totalVolumeCbm,
|
||||
LocalDateTime plannedDeliveryAt, LocalDateTime dispatchedAt,
|
||||
LocalDateTime expectedArrivalAt, LocalDateTime signedAt,
|
||||
String remark, LocalDateTime createdAt, long version, List<String> allowedActions) {}
|
||||
String remark, LocalDateTime createdAt, long version, List<String> allowedActions,
|
||||
String bolNo, DeliveryDocumentDtos.Summary documentSummary) {}
|
||||
|
||||
public record DeliverySku(String inboundCode, String customerName, String destinationWarehouseCode,
|
||||
String receivingAddress, String shipmentNo, String fbaTrackingNo,
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.delivery.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_delivery_document_revision")
|
||||
public class DeliveryDocumentRevision {
|
||||
@TableId(type=IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long deliveryOrderId;
|
||||
private Long version;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String snapshotJson;
|
||||
private String operatorId;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -25,6 +25,8 @@ public class DeliveryOrder {
|
||||
private String customerName;
|
||||
private String status;
|
||||
private String waybillNo;
|
||||
private String bolNo;
|
||||
private String documentsJson;
|
||||
private String deliveryMethod;
|
||||
private String carrierName;
|
||||
private String pickupMethod;
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.baiyue.lastleg.delivery.gateway;
|
||||
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryDocumentDtos.File;
|
||||
import com.baiyue.lastleg.delivery.model.DeliveryDocumentCategory;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public interface DeliveryDocumentFileGateway {
|
||||
File upload(String customerId, long deliveryId, DeliveryDocumentCategory category, String operatorId, MultipartFile file);
|
||||
File metadata(String customerId, long deliveryId, DeliveryDocumentCategory category, long fileId);
|
||||
byte[] download(String customerId, long deliveryId, DeliveryDocumentCategory category, long fileId);
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.baiyue.lastleg.delivery.gateway;
|
||||
|
||||
import com.baiyue.lastleg.config.LastlegRestClientFactory;
|
||||
import com.baiyue.lastleg.config.LastlegRestClientProperties;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryDocumentDtos.File;
|
||||
import com.baiyue.lastleg.delivery.model.DeliveryDocumentCategory;
|
||||
import jakarta.annotation.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class HttpDeliveryDocumentFileGateway implements DeliveryDocumentFileGateway {
|
||||
private static final String PATH = "/internal/file/v1/business-files/{customer}/{type}/{delivery}";
|
||||
@Resource private RestClient.Builder restClientBuilder;
|
||||
@Resource private LastlegRestClientFactory restClientFactory;
|
||||
@Resource private LastlegRestClientProperties restClientProperties;
|
||||
@Value("${baiyue.clients.file.url:http://127.0.0.1:9205}") private String baseUrl;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
private RestClient client;
|
||||
@PostConstruct void initializeClient() {
|
||||
client = restClientFactory.build(restClientBuilder, baseUrl, restClientProperties.getCustomerVisitFile());
|
||||
}
|
||||
@Override public File upload(String customerId, long deliveryId, DeliveryDocumentCategory category,
|
||||
String operatorId, MultipartFile file) {
|
||||
if (file == null || file.isEmpty() || file.getSize() > 52428800L)
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "派送资料须为非空且不超过50MiB的文件");
|
||||
MultipartBodyBuilder body = new MultipartBodyBuilder();
|
||||
try {
|
||||
body.part("file", new ByteArrayResource(file.getBytes()) {
|
||||
@Override public String getFilename() { return file.getOriginalFilename(); }
|
||||
}).contentType(file.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM
|
||||
: MediaType.parseMediaType(file.getContentType()));
|
||||
} catch (IOException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "无法读取派送资料", e); }
|
||||
Envelope response = client.post().uri(PATH, customerId, category.businessType(), deliveryId)
|
||||
.header("X-Internal-Service-Token", internalToken).header("X-Operator-Id", operatorId)
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA).body(body.build()).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw failure(result.getStatusCode()); })
|
||||
.body(Envelope.class);
|
||||
return verified(response, customerId, deliveryId, category, null);
|
||||
}
|
||||
@Override public File metadata(String customerId, long deliveryId, DeliveryDocumentCategory category, long fileId) {
|
||||
Envelope response = client.get().uri(PATH + "/{file}", customerId, category.businessType(), deliveryId, fileId)
|
||||
.header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw failure(result.getStatusCode()); })
|
||||
.body(Envelope.class);
|
||||
return verified(response, customerId, deliveryId, category, fileId);
|
||||
}
|
||||
@Override public byte[] download(String customerId, long deliveryId, DeliveryDocumentCategory category, long fileId) {
|
||||
return client.get().uri(PATH + "/{file}/content", customerId, category.businessType(), deliveryId, fileId)
|
||||
.header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw failure(result.getStatusCode()); })
|
||||
.body(byte[].class);
|
||||
}
|
||||
private static File verified(Envelope response, String customerId, long deliveryId,
|
||||
DeliveryDocumentCategory category, Long expectedFileId) {
|
||||
if (response == null) throw failure(HttpStatus.SERVICE_UNAVAILABLE);
|
||||
if (!"200".equals(response.code())) throw new ResponseStatusException(
|
||||
response.code() != null && response.code().startsWith("A04") ? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"派送资料文件不可用或归属校验失败");
|
||||
Metadata file = response.data();
|
||||
if (file == null || file.fileId() == null || file.fileId() <= 0 || file.createdAt() == null
|
||||
|| file.fileName() == null || file.sizeBytes() == null || file.sizeBytes() <= 0
|
||||
|| file.sizeBytes() > 52428800L || !Objects.equals(String.valueOf(file.tenantId()), customerId)
|
||||
|| !category.businessType().equals(file.businessType()) || !String.valueOf(deliveryId).equals(file.businessId())
|
||||
|| (expectedFileId != null && !expectedFileId.equals(file.fileId())))
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "文件不属于当前客户、派送单或资料类别");
|
||||
return new File(file.fileId(), file.fileName(), file.contentType(), file.sizeBytes(), file.createdAt());
|
||||
}
|
||||
private static ResponseStatusException failure(HttpStatusCode status) {
|
||||
return new ResponseStatusException(status.is4xxClientError() ? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"派送资料文件请求失败");
|
||||
}
|
||||
private record Envelope(String code, String message, Metadata data) { }
|
||||
private record Metadata(Long fileId, Long tenantId, String businessType, String businessId,
|
||||
String fileName, String contentType, Long sizeBytes, LocalDateTime createdAt) { }
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.lastleg.delivery.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.delivery.entity.DeliveryDocumentRevision;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DeliveryDocumentRevisionMapper extends BaseMapper<DeliveryDocumentRevision> {
|
||||
@Select("SELECT * FROM ll_delivery_document_revision WHERE customer_id=#{customerId} AND delivery_order_id=#{deliveryId} AND idempotency_key=#{key}")
|
||||
DeliveryDocumentRevision findCommand(@Param("customerId") String customerId, @Param("deliveryId") long deliveryId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_delivery_document_revision WHERE customer_id=#{customerId} AND delivery_order_id=#{deliveryId} ORDER BY version DESC")
|
||||
List<DeliveryDocumentRevision> history(@Param("customerId") String customerId, @Param("deliveryId") long deliveryId);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.lastleg.delivery.model;
|
||||
|
||||
public enum DeliveryDocumentCategory {
|
||||
LOADING_PHOTO("DELIVERY_LOADING_PHOTO"), BOL("DELIVERY_BOL"), RECEIPT("DELIVERY_RECEIPT");
|
||||
|
||||
private final String businessType;
|
||||
DeliveryDocumentCategory(String businessType) { this.businessType = businessType; }
|
||||
public String businessType() { return businessType; }
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.delivery.model;
|
||||
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryDocumentDtos.*;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.*;
|
||||
|
||||
/** Saved metadata only; listing never calls File and never counts pending uploads. */
|
||||
public final class DeliveryDocumentSnapshot {
|
||||
private static final ObjectMapper JSON = new ObjectMapper().findAndRegisterModules();
|
||||
private DeliveryDocumentSnapshot() { }
|
||||
public static List<Category> categories(String json) {
|
||||
if (json == null || json.isBlank()) return Arrays.stream(DeliveryDocumentCategory.values())
|
||||
.map(category -> new Category(category, "MISSING", 0, List.of())).toList();
|
||||
try { return JSON.readValue(json, new TypeReference<List<Category>>() { }); }
|
||||
catch (JsonProcessingException e) { throw new IllegalStateException("派送资料快照损坏", e); }
|
||||
}
|
||||
public static Summary summary(String json) {
|
||||
List<Category> categories = categories(json);
|
||||
return new Summary((int) categories.stream().filter(c -> !c.files().isEmpty()).count(), 3,
|
||||
categories.stream().map(c -> new CategorySummary(c.category(), c.status(), c.fileCount())).toList());
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package com.baiyue.lastleg.delivery.service;
|
||||
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryDocumentDtos;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryDocumentDtos.*;
|
||||
import com.baiyue.lastleg.delivery.entity.*;
|
||||
import com.baiyue.lastleg.delivery.gateway.DeliveryDocumentFileGateway;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.delivery.model.*;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class DeliveryDocumentService {
|
||||
@Resource private DeliveryOrderMapper orders;
|
||||
@Resource private DeliveryDocumentRevisionMapper revisions;
|
||||
@Resource private DeliveryOperationMapper operations;
|
||||
@Resource private DeliveryDocumentFileGateway files;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
public Detail detail(String customerId, String deliveryNo) { return detail(require(customerId, deliveryNo), false); }
|
||||
|
||||
public DeliveryDocumentDtos.File upload(String customerId, String deliveryNo, DeliveryDocumentCategory category,
|
||||
String operatorId, MultipartFile file) {
|
||||
DeliveryOrder order = require(customerId, deliveryNo);
|
||||
return files.upload(customerId, order.getId(), category, operatorId, file);
|
||||
}
|
||||
|
||||
/** Lock the same aggregate as status commands, validate the whole set, then commit one snapshot. */
|
||||
@Transactional
|
||||
public Detail save(String customerId, String deliveryNo, String operatorId, Save request) {
|
||||
DeliveryOrder order = orders.lockByNo(customerId, deliveryNo);
|
||||
if (order == null) throw missing();
|
||||
String hash = hash(request);
|
||||
DeliveryDocumentRevision replay = revisions.findCommand(customerId, order.getId(), request.idempotencyKey());
|
||||
if (replay != null) {
|
||||
if (!hash.equals(replay.getRequestHash())) throw conflict("相同幂等键的派送资料内容不同");
|
||||
Detail saved = read(replay.getSnapshotJson());
|
||||
return new Detail(saved.deliveryNo(), saved.customerId(), saved.version(), saved.bolNo(),
|
||||
saved.uploadedCategoryCount(), 3, saved.categories(), saved.allowedActions(), true);
|
||||
}
|
||||
if (!Objects.equals(version(order), request.expectedVersion())) throw conflict("派送单版本已变化,请刷新后重试");
|
||||
Map<DeliveryDocumentCategory, List<DeliveryDocumentDtos.File>> bindings = new EnumMap<>(DeliveryDocumentCategory.class);
|
||||
Set<Long> unique = new HashSet<>();
|
||||
for (Binding binding : request.documents()) {
|
||||
if (bindings.containsKey(binding.category())) throw bad("资料类别不能重复");
|
||||
List<DeliveryDocumentDtos.File> metadata = new ArrayList<>();
|
||||
for (Long fileId : binding.fileIds()) {
|
||||
if (!unique.add(fileId)) throw bad("同一文件不能重复绑定");
|
||||
metadata.add(files.metadata(customerId, order.getId(), binding.category(), fileId));
|
||||
}
|
||||
bindings.put(binding.category(), List.copyOf(metadata));
|
||||
}
|
||||
List<Category> categories = Arrays.stream(DeliveryDocumentCategory.values()).map(category -> {
|
||||
List<DeliveryDocumentDtos.File> values = bindings.getOrDefault(category, List.of());
|
||||
return new Category(category, values.isEmpty() ? "MISSING" : "UPLOADED", values.size(), values);
|
||||
}).toList();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
order.setBolNo(request.bolNo() == null ? "" : request.bolNo().trim());
|
||||
order.setDocumentsJson(write(categories));
|
||||
order.setVersion(version(order) + 1);
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
Detail result = detail(order, false);
|
||||
DeliveryDocumentRevision revision = new DeliveryDocumentRevision();
|
||||
revision.setCustomerId(customerId); revision.setDeliveryOrderId(order.getId()); revision.setVersion(order.getVersion());
|
||||
revision.setIdempotencyKey(request.idempotencyKey()); revision.setRequestHash(hash); revision.setSnapshotJson(write(result));
|
||||
revision.setOperatorId(operatorId); revision.setCreatedAt(now); revisions.insert(revision);
|
||||
DeliveryOperation operation = new DeliveryOperation();
|
||||
operation.setCustomerId(customerId); operation.setDeliveryOrderId(order.getId()); operation.setOperatorId(operatorId);
|
||||
operation.setOperationNode("SAVE_DOCUMENTS"); operation.setResult("保存派送资料:" + result.uploadedCategoryCount() + "/3类");
|
||||
operation.setCreatedAt(now); operations.insert(operation);
|
||||
return result;
|
||||
}
|
||||
public List<History> history(String customerId, String deliveryNo) {
|
||||
DeliveryOrder order = require(customerId, deliveryNo);
|
||||
return revisions.history(customerId, order.getId()).stream().map(revision -> new History(revision.getVersion(),
|
||||
revision.getOperatorId(), revision.getCreatedAt(), read(revision.getSnapshotJson()))).toList();
|
||||
}
|
||||
public Download download(String customerId, String deliveryNo, DeliveryDocumentCategory category, long fileId, Long revisionVersion) {
|
||||
DeliveryOrder order = require(customerId, deliveryNo);
|
||||
Detail snapshot = detail(order, false);
|
||||
if (revisionVersion != null) {
|
||||
snapshot = revisions.history(customerId, order.getId()).stream().filter(r -> revisionVersion.equals(r.getVersion()))
|
||||
.findFirst().map(r -> read(r.getSnapshotJson())).orElseThrow(DeliveryDocumentService::missing);
|
||||
}
|
||||
DeliveryDocumentDtos.File file = snapshot.categories().stream().filter(c -> c.category() == category)
|
||||
.flatMap(c -> c.files().stream()).filter(f -> f.fileId() == fileId).findFirst().orElseThrow(DeliveryDocumentService::missing);
|
||||
files.metadata(customerId, order.getId(), category, fileId);
|
||||
return new Download(file, files.download(customerId, order.getId(), category, fileId));
|
||||
}
|
||||
public record Download(DeliveryDocumentDtos.File file, byte[] content) { }
|
||||
private DeliveryOrder require(String customerId, String deliveryNo) {
|
||||
DeliveryOrder order = orders.findByNo(customerId, deliveryNo);
|
||||
if (order == null) throw missing();
|
||||
return order;
|
||||
}
|
||||
private Detail detail(DeliveryOrder order, boolean replayed) {
|
||||
List<Category> categories = DeliveryDocumentSnapshot.categories(order.getDocumentsJson());
|
||||
return new Detail(order.getDeliveryNo(), order.getCustomerId(), version(order), order.getBolNo(),
|
||||
(int) categories.stream().filter(c -> !c.files().isEmpty()).count(), 3, categories, List.of("MANAGE_DOCUMENTS"), replayed);
|
||||
}
|
||||
private static long version(DeliveryOrder order) { return Objects.requireNonNullElse(order.getVersion(), 0L); }
|
||||
private String hash(Object value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(write(value).getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
|
||||
}
|
||||
private String write(Object value) {
|
||||
try { return objectMapper.writeValueAsString(value); }
|
||||
catch (JsonProcessingException e) { throw new IllegalStateException("无法保存派送资料", e); }
|
||||
}
|
||||
private Detail read(String value) {
|
||||
try { return objectMapper.readValue(value, Detail.class); }
|
||||
catch (JsonProcessingException e) { throw new IllegalStateException("派送资料历史损坏", e); }
|
||||
}
|
||||
private static ResponseStatusException missing() { return new ResponseStatusException(HttpStatus.NOT_FOUND, "派送单或已绑定资料不存在"); }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
private static ResponseStatusException bad(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
}
|
||||
+1
-1
@@ -614,7 +614,7 @@ public class DeliveryTransferService {
|
||||
candidate.getInboundCode(), candidate.getSkuCode(), candidate.getProductName(),
|
||||
value(candidate.getAvailablePackages()));
|
||||
}
|
||||
private DeliveryResponses.DeliveryRow deliveryRow(DeliveryOrder d) { return new DeliveryResponses.DeliveryRow(d.getId(), d.getCustomerId(), d.getCustomerName(), d.getDeliveryNo(), d.getOutboundNo(), d.getDispatchNo(), d.getOverseasWarehouseId(), d.getOverseasWarehouseName(), d.getStatus(), d.getWaybillNo(), d.getDeliveryMethod(), d.getCarrierName(), d.getPickupMethod(), d.getLoadingType(), d.getVehicleType(), d.getVehicleSize(), d.getVehiclePlate(), d.getDriverName(), d.getDriverPhone(), d.getAppointmentAccount(), d.getSellerName(), value(d.getPalletCount()), value(d.getPackageCount()), d.getTotalWeightKg(), d.getTotalVolumeCbm(), d.getPlannedDeliveryAt(), d.getDispatchedAt(), d.getExpectedArrivalAt(), d.getSignedAt(), d.getRemark(), d.getCreatedAt(), value(d.getVersion()), DeliveryStatusPolicy.allowedActions(d.getStatus())); }
|
||||
private DeliveryResponses.DeliveryRow deliveryRow(DeliveryOrder d) { return new DeliveryResponses.DeliveryRow(d.getId(), d.getCustomerId(), d.getCustomerName(), d.getDeliveryNo(), d.getOutboundNo(), d.getDispatchNo(), d.getOverseasWarehouseId(), d.getOverseasWarehouseName(), d.getStatus(), d.getWaybillNo(), d.getDeliveryMethod(), d.getCarrierName(), d.getPickupMethod(), d.getLoadingType(), d.getVehicleType(), d.getVehicleSize(), d.getVehiclePlate(), d.getDriverName(), d.getDriverPhone(), d.getAppointmentAccount(), d.getSellerName(), value(d.getPalletCount()), value(d.getPackageCount()), d.getTotalWeightKg(), d.getTotalVolumeCbm(), d.getPlannedDeliveryAt(), d.getDispatchedAt(), d.getExpectedArrivalAt(), d.getSignedAt(), d.getRemark(), d.getCreatedAt(), value(d.getVersion()), DeliveryStatusPolicy.allowedActions(d.getStatus()), d.getBolNo(), com.baiyue.lastleg.delivery.model.DeliveryDocumentSnapshot.summary(d.getDocumentsJson())); }
|
||||
private DeliveryResponses.DeliverySku sku(LastlegOrderDetail d, LastlegOrder order) { return new DeliveryResponses.DeliverySku(order == null ? null : order.getInboundCode(), order == null ? null : order.getCustomerName(), d.getDestinationWarehouseCode(), null, d.getShipmentNo(), d.getFbaTrackingNo(), d.getSkuCode(), firstText(d.getProductNameZh(), d.getProductNameEn()), value(d.getPackageCount()), d.getGrossWeightKg(), d.getVolumeCbm()); }
|
||||
private DeliveryResponses.DeliveryFeeRow feeRow(DeliveryFee f) { return new DeliveryResponses.DeliveryFeeRow(f.getId(), f.getFeeDirection(), f.getFeeType(), f.getCurrency(), f.getAmount(), f.getRemark()); }
|
||||
private DeliveryResponses.DeliveryOperationRow operationRow(DeliveryOperation o) { return new DeliveryResponses.DeliveryOperationRow(o.getId(), o.getCreatedAt(), o.getOperationNode(), firstText(o.getOperatorName(), o.getOperatorId()), o.getResult()); }
|
||||
|
||||
@@ -22,6 +22,7 @@ public final class OverseasPermissions {
|
||||
public static final String DELIVERY_LIST = "platform:overseas-warehouse:delivery:list";
|
||||
public static final String DELIVERY_RETURN = "platform:overseas-warehouse:delivery:return";
|
||||
public static final String DELIVERY_STATUS = "platform:overseas-warehouse:delivery:status";
|
||||
public static final String DELIVERY_DOCUMENTS = "platform:overseas-warehouse:delivery:documents";
|
||||
public static final String DISPATCH_LIST = "platform:overseas-warehouse:dispatch:list";
|
||||
public static final String DISPATCH_EDIT = "platform:overseas-warehouse:dispatch:edit";
|
||||
public static final String DISPATCH_CANCEL = "platform:overseas-warehouse:dispatch:cancel";
|
||||
|
||||
Reference in New Issue
Block a user