This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
@@ -0,0 +1,9 @@
package com.baiyue.lastleg.looseboard;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.baiyue.lastleg.looseboard.mapper")
public class LooseboardMapperConfiguration {
}
@@ -0,0 +1,97 @@
package com.baiyue.lastleg.looseboard.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardResponses;
import com.baiyue.lastleg.looseboard.service.LocalLooseboardService;
import com.baiyue.lastleg.security.OverseasPermissions;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
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.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/platform/v1/last-leg/local-looseboard-orders")
public class PlatformLocalLooseboardController {
@Resource private LocalLooseboardService service;
@GetMapping
@PreAuthorize("hasAuthority('" + OverseasPermissions.LOOSEBOARD_LIST + "')")
public ApiResponse<PageResult<LocalLooseboardResponses.Summary>> page(
@AuthenticationPrincipal AuthenticatedUser user, @Valid LocalLooseboardRequests.Query query) {
requirePlatform(user);
return ApiResponse.success(service.page(query));
}
@PostMapping
@PreAuthorize("hasAuthority('" + OverseasPermissions.LOOSEBOARD_CREATE + "')")
public ApiResponse<LocalLooseboardResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam String customerId, @Valid @RequestBody LocalLooseboardRequests.Create request) {
requirePermission(user, OverseasPermissions.LOOSEBOARD_CREATE);
return ApiResponse.success(service.create(requiredCustomer(customerId), operator(user), request));
}
@GetMapping("/{orderNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LOOSEBOARD_LIST + "')")
public ApiResponse<LocalLooseboardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam String customerId, @PathVariable String orderNo) {
requirePlatform(user);
return ApiResponse.success(service.detail(requiredCustomer(customerId), orderNo));
}
@PutMapping("/{orderNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LOOSEBOARD_EDIT + "')")
public ApiResponse<LocalLooseboardResponses.Detail> update(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam String customerId, @PathVariable String orderNo,
@Valid @RequestBody LocalLooseboardRequests.Update request) {
requirePermission(user, OverseasPermissions.LOOSEBOARD_EDIT);
return ApiResponse.success(service.update(requiredCustomer(customerId), orderNo, operator(user), request));
}
@PatchMapping("/{orderNo}/status")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LOOSEBOARD_STATUS + "')")
public ApiResponse<LocalLooseboardResponses.Detail> changeStatus(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam String customerId, @PathVariable String orderNo,
@Valid @RequestBody LocalLooseboardRequests.ChangeStatus request) {
requirePermission(user, OverseasPermissions.LOOSEBOARD_STATUS);
return ApiResponse.success(service.changeStatus(requiredCustomer(customerId), orderNo, operator(user), request));
}
private void requirePlatform(AuthenticatedUser user) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform identity is required");
}
}
private void requirePermission(AuthenticatedUser user, String permission) {
requirePlatform(user);
if (user.permissions() == null || !user.permissions().contains(permission)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, permission + " permission is required");
}
}
private String requiredCustomer(String customerId) {
if (customerId == null || customerId.isBlank() || "0".equals(customerId.trim())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Target customerId is required");
}
return customerId.trim();
}
private String operator(AuthenticatedUser user) {
return user.userId() == null ? user.username() : String.valueOf(user.userId());
}
}
@@ -0,0 +1,72 @@
package com.baiyue.lastleg.looseboard.dto;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
public final class LocalLooseboardRequests {
private LocalLooseboardRequests() { }
public record Create(
@NotBlank @Pattern(regexp = "SHARED_TRUCK|PRIORITY_CONSOLIDATION") String transportMode,
@NotBlank @Pattern(regexp = "YES|NO") String palletRequired,
@NotBlank @Pattern(regexp = "PORT_PICKUP|WAREHOUSE_TO_WAREHOUSE|WAREHOUSE_TO_DOOR") String transportType,
@NotBlank @Size(max = 255) String origin,
@Size(max = 500) String originAddress,
@NotBlank @Size(max = 255) String destination,
@Size(max = 500) String destinationAddress,
@Size(max = 32) String containerType,
@NotNull @Min(0) Integer packageCount,
@NotNull @DecimalMin("0.00") BigDecimal totalWeightKg,
@NotNull @DecimalMin("0.00") BigDecimal totalVolumeCbm,
@DecimalMin("0.00") BigDecimal budgetAmount,
@Size(min = 3, max = 3) String currency,
@Size(max = 128) String contactName,
@Size(max = 128) String contactInfo,
LocalDate requestedDate,
@Size(max = 1000) String remark,
@NotBlank @Size(max = 255) String idempotencyKey) { }
public record Update(
@NotNull @Min(0) Long version,
@NotBlank @Pattern(regexp = "SHARED_TRUCK|PRIORITY_CONSOLIDATION") String transportMode,
@NotBlank @Pattern(regexp = "YES|NO") String palletRequired,
@NotBlank @Pattern(regexp = "PORT_PICKUP|WAREHOUSE_TO_WAREHOUSE|WAREHOUSE_TO_DOOR") String transportType,
@NotBlank @Size(max = 255) String origin,
@Size(max = 500) String originAddress,
@NotBlank @Size(max = 255) String destination,
@Size(max = 500) String destinationAddress,
@Size(max = 32) String containerType,
@NotNull @Min(0) Integer packageCount,
@NotNull @DecimalMin("0.00") BigDecimal totalWeightKg,
@NotNull @DecimalMin("0.00") BigDecimal totalVolumeCbm,
@DecimalMin("0.00") BigDecimal budgetAmount,
@Size(min = 3, max = 3) String currency,
@Size(max = 128) String contactName,
@Size(max = 128) String contactInfo,
LocalDate requestedDate,
@Size(max = 1000) String remark) { }
public record Query(
@Min(1) long page,
@Min(1) @Max(200) long size,
String customerId,
String orderNo,
String routeKeyword,
String status,
String transportMode,
LocalDate requestedFrom,
LocalDate requestedTo) { }
public record ChangeStatus(
@NotNull @Min(0) Long version,
@NotBlank @Size(max = 32) String status,
@Size(max = 1000) String remark) { }
}
@@ -0,0 +1,25 @@
package com.baiyue.lastleg.looseboard.dto;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
public final class LocalLooseboardResponses {
private LocalLooseboardResponses() { }
public record Summary(
Long id, String customerId, String orderNo, String transportMode, String palletRequired,
String transportType, String origin, String destination, String containerType, int packageCount,
BigDecimal totalWeightKg, BigDecimal totalVolumeCbm, BigDecimal budgetAmount, String currency,
String contactName, String contactInfo, LocalDate requestedDate, String status, long version,
LocalDateTime createdAt, LocalDateTime updatedAt) { }
public record History(
String operationType, String fromStatus, String toStatus, String operatorId,
String changeDetail, String remark, LocalDateTime createdAt) { }
public record Detail(
Summary summary, String originAddress, String destinationAddress, String remark,
String createdBy, List<History> histories) { }
}
@@ -0,0 +1,23 @@
package com.baiyue.lastleg.looseboard.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_local_looseboard_history")
public class LocalLooseboardHistory {
@TableId(type = IdType.INPUT) private Long id;
private Long orderId;
private String customerId;
private String operationType;
private String fromStatus;
private String toStatus;
private String operatorId;
private String changeDetail;
private String remark;
private LocalDateTime createdAt;
}
@@ -0,0 +1,42 @@
package com.baiyue.lastleg.looseboard.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("ll_local_looseboard_order")
public class LocalLooseboardOrder {
@TableId(type = IdType.INPUT) private Long id;
private String customerId;
private String orderNo;
private String transportMode;
private String palletRequired;
private String transportType;
private String origin;
private String originAddress;
private String destination;
private String destinationAddress;
private String containerType;
private Integer packageCount;
private BigDecimal totalWeightKg;
private BigDecimal totalVolumeCbm;
private BigDecimal budgetAmount;
private String currency;
private String contactName;
private String contactInfo;
private LocalDate requestedDate;
private String status;
private String remark;
private String idempotencyKey;
private String requestHash;
private String createdBy;
private Long version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,15 @@
package com.baiyue.lastleg.looseboard.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardHistory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface LocalLooseboardHistoryMapper extends BaseMapper<LocalLooseboardHistory> {
@Select("SELECT * FROM ll_local_looseboard_history WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY created_at,id")
List<LocalLooseboardHistory> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
}
@@ -0,0 +1,19 @@
package com.baiyue.lastleg.looseboard.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardOrder;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface LocalLooseboardOrderMapper extends BaseMapper<LocalLooseboardOrder> {
@Select("SELECT * FROM ll_local_looseboard_order WHERE customer_id=#{customerId} AND order_no=#{orderNo} LIMIT 1")
LocalLooseboardOrder findNo(@Param("customerId") String customerId, @Param("orderNo") String orderNo);
@Select("SELECT * FROM ll_local_looseboard_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
LocalLooseboardOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
@Select("SELECT * FROM ll_local_looseboard_order WHERE customer_id=#{customerId} AND order_no=#{orderNo} LIMIT 1 FOR UPDATE")
LocalLooseboardOrder lockNo(@Param("customerId") String customerId, @Param("orderNo") String orderNo);
}
@@ -0,0 +1,302 @@
package com.baiyue.lastleg.looseboard.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardResponses;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardHistory;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardOrder;
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardHistoryMapper;
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardOrderMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@Service
public class LocalLooseboardService {
public static final String PENDING_ACCEPTANCE = "PENDING_ACCEPTANCE";
public static final String DISPATCHED = "DISPATCHED";
public static final String IN_TRANSIT = "IN_TRANSIT";
public static final String COMPLETED = "COMPLETED";
public static final String CANCELLED = "CANCELLED";
private static final Set<String> STATUSES = Set.of(PENDING_ACCEPTANCE, DISPATCHED, IN_TRANSIT, COMPLETED, CANCELLED);
private static final Set<String> MODES = Set.of("SHARED_TRUCK", "PRIORITY_CONSOLIDATION");
private static final Map<String, Set<String>> TRANSITIONS = Map.of(
PENDING_ACCEPTANCE, Set.of(DISPATCHED, CANCELLED),
DISPATCHED, Set.of(IN_TRANSIT, CANCELLED),
IN_TRANSIT, Set.of(COMPLETED));
@Resource private LocalLooseboardOrderMapper orders;
@Resource private LocalLooseboardHistoryMapper histories;
@Resource private ObjectMapper objectMapper;
@Transactional
public LocalLooseboardResponses.Detail create(String customerId, String operatorId,
LocalLooseboardRequests.Create request) {
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
LocalLooseboardOrder replay = orders.findKey(customerId, key);
if (replay != null) return replay(customerId, replay, requestHash);
LocalDateTime now = LocalDateTime.now();
LocalLooseboardOrder order = new LocalLooseboardOrder();
order.setId(IdWorker.getId());
order.setCustomerId(customerId);
order.setOrderNo("LB" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + order.getId());
apply(order, request.transportMode(), request.palletRequired(), request.transportType(), request.origin(),
request.originAddress(), request.destination(), request.destinationAddress(), request.containerType(),
request.packageCount(), request.totalWeightKg(), request.totalVolumeCbm(), request.budgetAmount(),
request.currency(), request.contactName(), request.contactInfo(), request.requestedDate(), request.remark());
order.setStatus(PENDING_ACCEPTANCE);
order.setIdempotencyKey(key);
order.setRequestHash(requestHash);
order.setCreatedBy(operatorId);
order.setVersion(0L);
order.setCreatedAt(now);
order.setUpdatedAt(now);
try {
orders.insert(order);
} catch (DuplicateKeyException exception) {
LocalLooseboardOrder concurrent = orders.findKey(customerId, key);
if (concurrent != null) return replay(customerId, concurrent, requestHash);
throw exception;
}
history(order, "CREATE", null, PENDING_ACCEPTANCE, operatorId, null, request.remark(), now);
return detail(customerId, order.getOrderNo());
}
public PageResult<LocalLooseboardResponses.Summary> page(LocalLooseboardRequests.Query query) {
validateQuery(query);
LambdaQueryWrapper<LocalLooseboardOrder> wrapper = new LambdaQueryWrapper<LocalLooseboardOrder>()
.eq(StringUtils.hasText(query.customerId()), LocalLooseboardOrder::getCustomerId, trim(query.customerId()))
.like(StringUtils.hasText(query.orderNo()), LocalLooseboardOrder::getOrderNo, trim(query.orderNo()))
.and(StringUtils.hasText(query.routeKeyword()), value -> value
.like(LocalLooseboardOrder::getOrigin, trim(query.routeKeyword()))
.or().like(LocalLooseboardOrder::getDestination, trim(query.routeKeyword())))
.eq(StringUtils.hasText(query.status()), LocalLooseboardOrder::getStatus,
StringUtils.hasText(query.status()) ? norm(query.status()) : null)
.eq(StringUtils.hasText(query.transportMode()), LocalLooseboardOrder::getTransportMode,
StringUtils.hasText(query.transportMode()) ? norm(query.transportMode()) : null)
.ge(query.requestedFrom() != null, LocalLooseboardOrder::getRequestedDate, query.requestedFrom())
.le(query.requestedTo() != null, LocalLooseboardOrder::getRequestedDate, query.requestedTo())
.orderByDesc(LocalLooseboardOrder::getCreatedAt).orderByDesc(LocalLooseboardOrder::getId);
Page<LocalLooseboardOrder> result = orders.selectPage(Page.of(query.page(), query.size()), wrapper);
return new PageResult<>(result.getRecords().stream().map(this::summary).toList(), result.getCurrent(),
result.getSize(), result.getTotal(), result.getPages());
}
public LocalLooseboardResponses.Detail detail(String customerId, String orderNo) {
LocalLooseboardOrder order = orders.findNo(customerId, orderNo);
if (order == null) throw notFound();
return response(order);
}
@Transactional
public LocalLooseboardResponses.Detail update(String customerId, String orderNo, String operatorId,
LocalLooseboardRequests.Update request) {
LocalLooseboardOrder order = lock(customerId, orderNo);
if (!request.version().equals(value(order.getVersion()))) throw conflict("Order was changed; refresh and retry");
if (!PENDING_ACCEPTANCE.equals(order.getStatus())) {
throw conflict("Only pending-acceptance looseboard orders can be edited");
}
Map<String, Object> before = snapshot(order);
apply(order, request.transportMode(), request.palletRequired(), request.transportType(), request.origin(),
request.originAddress(), request.destination(), request.destinationAddress(), request.containerType(),
request.packageCount(), request.totalWeightKg(), request.totalVolumeCbm(), request.budgetAmount(),
request.currency(), request.contactName(), request.contactInfo(), request.requestedDate(), request.remark());
Map<String, Object> after = snapshot(order);
if (before.equals(after)) return response(order);
LocalDateTime now = LocalDateTime.now();
touch(order, now);
orders.updateById(order);
history(order, "UPDATE", order.getStatus(), order.getStatus(), operatorId,
changeDetail(before, after), null, now);
return response(order);
}
@Transactional
public LocalLooseboardResponses.Detail changeStatus(String customerId, String orderNo, String operatorId,
LocalLooseboardRequests.ChangeStatus request) {
LocalLooseboardOrder order = lock(customerId, orderNo);
if (!request.version().equals(value(order.getVersion()))) throw conflict("Order was changed; refresh and retry");
String next = norm(request.status());
if (!STATUSES.contains(next)) throw badRequest("Unsupported looseboard order status");
String from = order.getStatus();
if (next.equals(from)) return response(order);
if (!TRANSITIONS.getOrDefault(from, Set.of()).contains(next)) {
throw conflict("Unsupported looseboard order transition: " + from + " -> " + next);
}
LocalDateTime now = LocalDateTime.now();
order.setStatus(next);
touch(order, now);
orders.updateById(order);
history(order, "STATUS", from, next, operatorId, null, request.remark(), now);
return response(order);
}
private void apply(LocalLooseboardOrder order, String mode, String palletRequired, String transportType,
String origin, String originAddress, String destination, String destinationAddress,
String containerType, Integer packageCount, java.math.BigDecimal totalWeightKg,
java.math.BigDecimal totalVolumeCbm, java.math.BigDecimal budgetAmount, String currency,
String contactName, String contactInfo, java.time.LocalDate requestedDate, String remark) {
order.setTransportMode(norm(mode));
order.setPalletRequired(norm(palletRequired));
order.setTransportType(norm(transportType));
order.setOrigin(origin.trim());
order.setOriginAddress(trim(originAddress));
order.setDestination(destination.trim());
order.setDestinationAddress(trim(destinationAddress));
order.setContainerType(trim(containerType));
order.setPackageCount(packageCount);
order.setTotalWeightKg(totalWeightKg);
order.setTotalVolumeCbm(totalVolumeCbm);
order.setBudgetAmount(budgetAmount);
order.setCurrency(StringUtils.hasText(currency) ? norm(currency) : "USD");
order.setContactName(trim(contactName));
order.setContactInfo(trim(contactInfo));
order.setRequestedDate(requestedDate);
order.setRemark(trim(remark));
}
private LocalLooseboardResponses.Detail replay(String customerId, LocalLooseboardOrder order, String hash) {
if (!hash.equals(order.getRequestHash())) throw conflict("Idempotency key was reused with different looseboard data");
return detail(customerId, order.getOrderNo());
}
private LocalLooseboardOrder lock(String customerId, String orderNo) {
LocalLooseboardOrder order = orders.lockNo(customerId, orderNo);
if (order == null) throw notFound();
return order;
}
private LocalLooseboardResponses.Detail response(LocalLooseboardOrder order) {
return new LocalLooseboardResponses.Detail(summary(order), order.getOriginAddress(),
order.getDestinationAddress(), order.getRemark(), order.getCreatedBy(),
histories.listOrder(order.getCustomerId(), order.getId()).stream()
.map(value -> new LocalLooseboardResponses.History(value.getOperationType(), value.getFromStatus(),
value.getToStatus(), value.getOperatorId(), value.getChangeDetail(), value.getRemark(),
value.getCreatedAt())).toList());
}
private LocalLooseboardResponses.Summary summary(LocalLooseboardOrder order) {
return new LocalLooseboardResponses.Summary(order.getId(), order.getCustomerId(), order.getOrderNo(),
order.getTransportMode(), order.getPalletRequired(), order.getTransportType(), order.getOrigin(),
order.getDestination(), order.getContainerType(), Objects.requireNonNullElse(order.getPackageCount(), 0),
order.getTotalWeightKg(), order.getTotalVolumeCbm(), order.getBudgetAmount(), order.getCurrency(),
order.getContactName(), order.getContactInfo(), order.getRequestedDate(), order.getStatus(),
value(order.getVersion()), order.getCreatedAt(), order.getUpdatedAt());
}
private void history(LocalLooseboardOrder order, String type, String from, String to, String operator,
String changeDetail, String remark, LocalDateTime now) {
LocalLooseboardHistory history = new LocalLooseboardHistory();
history.setId(IdWorker.getId());
history.setOrderId(order.getId());
history.setCustomerId(order.getCustomerId());
history.setOperationType(type);
history.setFromStatus(from);
history.setToStatus(to);
history.setOperatorId(operator);
history.setChangeDetail(changeDetail);
history.setRemark(trim(remark));
history.setCreatedAt(now);
histories.insert(history);
}
private void validateQuery(LocalLooseboardRequests.Query query) {
if (StringUtils.hasText(query.status()) && !STATUSES.contains(norm(query.status()))) {
throw badRequest("Unsupported looseboard order status");
}
if (StringUtils.hasText(query.transportMode()) && !MODES.contains(norm(query.transportMode()))) {
throw badRequest("Unsupported looseboard transport mode");
}
if (query.requestedFrom() != null && query.requestedTo() != null
&& query.requestedFrom().isAfter(query.requestedTo())) {
throw badRequest("requestedFrom must not be after requestedTo");
}
}
private Map<String, Object> snapshot(LocalLooseboardOrder order) {
Map<String, Object> value = new LinkedHashMap<>();
value.put("transportMode", order.getTransportMode());
value.put("palletRequired", order.getPalletRequired());
value.put("transportType", order.getTransportType());
value.put("origin", order.getOrigin());
value.put("originAddress", order.getOriginAddress());
value.put("destination", order.getDestination());
value.put("destinationAddress", order.getDestinationAddress());
value.put("containerType", order.getContainerType());
value.put("packageCount", order.getPackageCount());
value.put("totalWeightKg", order.getTotalWeightKg());
value.put("totalVolumeCbm", order.getTotalVolumeCbm());
value.put("budgetAmount", order.getBudgetAmount());
value.put("currency", order.getCurrency());
value.put("contactName", order.getContactName());
value.put("contactInfo", order.getContactInfo());
value.put("requestedDate", order.getRequestedDate());
value.put("remark", order.getRemark());
return value;
}
private String changeDetail(Map<String, Object> before, Map<String, Object> after) {
Map<String, Object> changes = new LinkedHashMap<>();
before.forEach((field, oldValue) -> {
Object newValue = after.get(field);
if (!Objects.equals(oldValue, newValue)) {
Map<String, Object> pair = new LinkedHashMap<>();
pair.put("before", oldValue);
pair.put("after", newValue);
changes.put(field, pair);
}
});
try {
return objectMapper.writeValueAsString(changes);
} catch (JsonProcessingException exception) {
throw new IllegalStateException("Cannot serialize looseboard change detail", exception);
}
}
private String hash(Object value) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(objectMapper.writeValueAsBytes(value)));
} catch (JsonProcessingException | NoSuchAlgorithmException exception) {
throw new IllegalStateException("Cannot hash looseboard request", exception);
}
}
private static void touch(LocalLooseboardOrder order, LocalDateTime now) {
order.setVersion(value(order.getVersion()) + 1);
order.setUpdatedAt(now);
}
private static long value(Long value) { return value == null ? 0 : value; }
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
private static String norm(String value) { return value.trim().toUpperCase(); }
private static ResponseStatusException badRequest(String message) {
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
}
private static ResponseStatusException conflict(String message) {
return new ResponseStatusException(HttpStatus.CONFLICT, message);
}
private static ResponseStatusException notFound() {
return new ResponseStatusException(HttpStatus.NOT_FOUND, "Local looseboard order not found");
}
}