暂存提交
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.lastleg.discard;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@MapperScan("com.baiyue.lastleg.discard.mapper")
|
||||
public class DiscardMapperConfiguration {
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.baiyue.lastleg.discard.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.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardResponses;
|
||||
import com.baiyue.lastleg.discard.service.CargoDiscardService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/cargo-discards")
|
||||
public class CargoDiscardController {
|
||||
@Resource private CargoDiscardService service;
|
||||
|
||||
@GetMapping("/inventory-candidates")
|
||||
public ApiResponse<List<CargoDiscardResponses.Candidate>> candidates(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@RequestParam(required = false) String inboundCode) {
|
||||
return ApiResponse.success(service.candidates(scope(user, customerId), inboundCode));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<CargoDiscardResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @Valid @RequestBody CargoDiscardRequests.Create request) {
|
||||
return ApiResponse.success(service.create(scope(user, customerId), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<CargoDiscardResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @Valid CargoDiscardRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{discardNo}")
|
||||
public ApiResponse<CargoDiscardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String discardNo) {
|
||||
return ApiResponse.success(service.detail(scope(user, customerId), discardNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{discardNo}/review")
|
||||
public ApiResponse<CargoDiscardResponses.Detail> review(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String discardNo,
|
||||
@Valid @RequestBody CargoDiscardRequests.Review request) {
|
||||
requireApprovalPermission(user);
|
||||
return ApiResponse.success(service.review(scope(user, customerId), discardNo, operator(user), request));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requested) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requested == null || requested.isBlank() || "0".equals(requested)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requested.trim();
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
private void requireApprovalPermission(AuthenticatedUser user) {
|
||||
if (user == null || user.permissions() == null
|
||||
|| !user.permissions().contains(CargoDiscardService.APPROVAL_PERMISSION)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cargo discard approval permission is required");
|
||||
}
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.lastleg.discard.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class CargoDiscardRequests {
|
||||
private CargoDiscardRequests() {}
|
||||
|
||||
public record Create(@NotEmpty @Size(max = 200) List<@Valid Item> items,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record Item(@NotNull Long orderId, @NotBlank @Size(max = 128) String skuCode,
|
||||
@Positive int discardPackages) {}
|
||||
|
||||
public record Review(@NotBlank String decision, @Size(max = 1000) String remark) {}
|
||||
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String discardNo, String inboundCode, String overseasWarehouseId,
|
||||
String status, LocalDateTime createdFrom, LocalDateTime createdTo) {}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.lastleg.discard.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class CargoDiscardResponses {
|
||||
private CargoDiscardResponses() {}
|
||||
|
||||
public record Candidate(Long orderId, String inboundCode, String inboundOrderNo,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
LocalDateTime inboundCreatedAt, long availablePackages,
|
||||
List<CandidateSku> skus) {}
|
||||
public record CandidateSku(String skuCode, String productName, long availablePackages) {}
|
||||
|
||||
public record Summary(Long id, String discardNo, String inboundCodes,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
int totalPackages, BigDecimal totalFee, String currency, String status,
|
||||
String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt) {}
|
||||
|
||||
public record Item(Long id, Long orderId, String inboundCode, String skuCode,
|
||||
String productName, int availablePackagesSnapshot, int discardPackages) {}
|
||||
|
||||
public record History(String fromStatus, String toStatus, String operatorId,
|
||||
String remark, LocalDateTime createdAt) {}
|
||||
|
||||
public record Detail(Summary summary, BigDecimal feePerPackage, String remark,
|
||||
String reviewedBy, String reviewRemark, List<Item> items,
|
||||
List<History> histories) {}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.lastleg.discard.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.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_cargo_discard_application")
|
||||
public class CargoDiscardApplication {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String discardNo;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseName;
|
||||
private String inboundCodes;
|
||||
private Integer totalPackages;
|
||||
private BigDecimal feePerPackage;
|
||||
private BigDecimal totalFee;
|
||||
private String currency;
|
||||
private String status;
|
||||
private String remark;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String createdBy;
|
||||
private String reviewedBy;
|
||||
private String reviewRemark;
|
||||
private LocalDateTime reviewedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.lastleg.discard.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_cargo_discard_carton")
|
||||
public class CargoDiscardCarton {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long applicationId;
|
||||
private Long itemId;
|
||||
private String customerId;
|
||||
private Long cartonId;
|
||||
private String cartonMark;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.discard.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_cargo_discard_item")
|
||||
public class CargoDiscardItem {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long applicationId;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private String inboundCode;
|
||||
private Long profileId;
|
||||
private Long skuInventoryId;
|
||||
private String skuCode;
|
||||
private String productName;
|
||||
private Integer availablePackagesSnapshot;
|
||||
private Integer discardPackages;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.discard.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_cargo_discard_status_history")
|
||||
public class CargoDiscardStatusHistory {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long applicationId;
|
||||
private String customerId;
|
||||
private String fromStatus;
|
||||
private String toStatus;
|
||||
private String operatorId;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.discard.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface CargoDiscardApplicationMapper extends BaseMapper<CargoDiscardApplication> {
|
||||
@Select("SELECT * FROM ll_cargo_discard_application WHERE customer_id=#{customerId} AND discard_no=#{discardNo} LIMIT 1")
|
||||
CargoDiscardApplication findNo(@Param("customerId") String customerId, @Param("discardNo") String discardNo);
|
||||
|
||||
@Select("SELECT * FROM ll_cargo_discard_application WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
CargoDiscardApplication findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
|
||||
@Select("SELECT * FROM ll_cargo_discard_application WHERE customer_id=#{customerId} AND discard_no=#{discardNo} LIMIT 1 FOR UPDATE")
|
||||
CargoDiscardApplication lockNo(@Param("customerId") String customerId, @Param("discardNo") String discardNo);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.discard.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardCarton;
|
||||
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 CargoDiscardCartonMapper extends BaseMapper<CargoDiscardCarton> {
|
||||
@Select("SELECT * FROM ll_cargo_discard_carton WHERE customer_id=#{customerId} AND application_id=#{applicationId} ORDER BY id")
|
||||
List<CargoDiscardCarton> listApplication(@Param("customerId") String customerId,
|
||||
@Param("applicationId") Long applicationId);
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package com.baiyue.lastleg.discard.mapper;
|
||||
|
||||
import com.baiyue.lastleg.discard.model.CargoDiscardCandidateRow;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface CargoDiscardInventoryMapper {
|
||||
@Select("<script>SELECT o.id order_id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id," +
|
||||
"o.overseas_warehouse_name,o.created_at,p.id profile_id,s.id sku_inventory_id,s.sku_code,s.product_name," +
|
||||
"COUNT(c.id) available_packages FROM ll_order o " +
|
||||
"JOIN ll_dropship_order_profile p ON p.customer_id=o.customer_id AND p.order_id=o.id " +
|
||||
"JOIN ll_dropship_sku_inventory s ON s.customer_id=o.customer_id AND s.order_id=o.id AND s.profile_id=p.id " +
|
||||
"LEFT JOIN ll_dropship_carton c ON c.customer_id=o.customer_id AND c.order_id=o.id " +
|
||||
"AND c.profile_id=p.id AND c.sku_code=s.sku_code AND c.status='AVAILABLE' " +
|
||||
"WHERE o.customer_id=#{customerId} AND o.order_type='ONE_CLICK_DROPSHIP' " +
|
||||
"<if test='inboundCode != null and inboundCode != \"\"'>AND o.inbound_code LIKE CONCAT('%',#{inboundCode},'%') </if>" +
|
||||
"GROUP BY o.id,o.inbound_code,o.inbound_order_no,o.overseas_warehouse_id,o.overseas_warehouse_name,o.created_at," +
|
||||
"p.id,s.id,s.sku_code,s.product_name HAVING COUNT(c.id)>0 ORDER BY o.created_at DESC,o.id DESC,p.id,s.sku_code LIMIT 500</script>")
|
||||
List<CargoDiscardCandidateRow> candidates(@Param("customerId") String customerId,
|
||||
@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
|
||||
"AND sku_code=#{skuCode} AND status='AVAILABLE'")
|
||||
long countAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
|
||||
"AND profile_id=#{profileId} AND sku_code=#{skuCode} AND status='AVAILABLE'")
|
||||
long countAvailableProfile(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("profileId") Long profileId, @Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
|
||||
"AND sku_code=#{skuCode} AND status='AVAILABLE' ORDER BY profile_id,carton_sequence,id LIMIT #{limit} FOR UPDATE")
|
||||
List<DropshipCarton> lockAvailable(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("skuCode") String skuCode, @Param("limit") int limit);
|
||||
|
||||
@Update("UPDATE ll_dropship_carton SET status=#{toStatus},version=version+1,updated_at=NOW(3) " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{cartonId} AND status=#{fromStatus}")
|
||||
int changeCartonStatus(@Param("customerId") String customerId, @Param("cartonId") Long cartonId,
|
||||
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
||||
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND id=#{cartonId} LIMIT 1 FOR UPDATE")
|
||||
DropshipCarton lockCarton(@Param("customerId") String customerId, @Param("cartonId") Long cartonId);
|
||||
|
||||
@Select("SELECT * FROM ll_dropship_sku_inventory WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE")
|
||||
DropshipSkuInventory lockSkuInventory(@Param("customerId") String customerId, @Param("id") Long id);
|
||||
|
||||
@Select("SELECT * FROM ll_dropship_sku_inventory WHERE customer_id=#{customerId} AND order_id=#{orderId} " +
|
||||
"AND profile_id=#{profileId} AND sku_code=#{skuCode} LIMIT 1 FOR UPDATE")
|
||||
DropshipSkuInventory lockSkuInventoryDimension(@Param("customerId") String customerId,
|
||||
@Param("orderId") Long orderId,
|
||||
@Param("profileId") Long profileId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Update("UPDATE ll_dropship_sku_inventory SET package_count=package_count-#{packages},updated_at=NOW(3) " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{id} AND package_count>=#{packages}")
|
||||
int deductSkuInventory(@Param("customerId") String customerId, @Param("id") Long id,
|
||||
@Param("packages") int packages);
|
||||
|
||||
@Select("SELECT package_count FROM ll_dropship_sku_inventory WHERE customer_id=#{customerId} AND id=#{id}")
|
||||
Long skuPackages(@Param("customerId") String customerId, @Param("id") Long id);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.discard.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
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 CargoDiscardItemMapper extends BaseMapper<CargoDiscardItem> {
|
||||
@Select("SELECT * FROM ll_cargo_discard_item WHERE customer_id=#{customerId} AND application_id=#{applicationId} ORDER BY inbound_code,sku_code,profile_id")
|
||||
List<CargoDiscardItem> listApplication(@Param("customerId") String customerId,
|
||||
@Param("applicationId") Long applicationId);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.discard.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardStatusHistory;
|
||||
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 CargoDiscardStatusHistoryMapper extends BaseMapper<CargoDiscardStatusHistory> {
|
||||
@Select("SELECT * FROM ll_cargo_discard_status_history WHERE customer_id=#{customerId} AND application_id=#{applicationId} ORDER BY created_at,id")
|
||||
List<CargoDiscardStatusHistory> listApplication(@Param("customerId") String customerId,
|
||||
@Param("applicationId") Long applicationId);
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.lastleg.discard.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class CargoDiscardCandidateRow {
|
||||
private Long orderId;
|
||||
private String inboundCode;
|
||||
private String inboundOrderNo;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseName;
|
||||
private LocalDateTime createdAt;
|
||||
private Long profileId;
|
||||
private Long skuInventoryId;
|
||||
private String skuCode;
|
||||
private String productName;
|
||||
private Long availablePackages;
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.baiyue.lastleg.discard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardCarton;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Final inventory mutation for a warehouse-approved cargo discard. */
|
||||
@Service
|
||||
public class CargoDiscardInventoryAdjustmentService {
|
||||
@Resource private CargoDiscardInventoryMapper inventory;
|
||||
@Resource private DropshipCartonLedgerMapper cartonLedgers;
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public AdjustmentResult discard(String customerId, CargoDiscardApplication application,
|
||||
List<CargoDiscardItem> items,
|
||||
Map<Long, List<CargoDiscardCarton>> linksByItem,
|
||||
String operatorId, LocalDateTime occurredAt) {
|
||||
long beforePackages = 0;
|
||||
long discardedPackages = 0;
|
||||
long afterPackages = 0;
|
||||
for (CargoDiscardItem item : items) {
|
||||
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
|
||||
if (sku == null) throw conflict("SKU inventory is missing during discard approval: " + item.getSkuCode());
|
||||
long before = value(sku.getPackageCount());
|
||||
int discarded = item.getDiscardPackages();
|
||||
if (before < discarded || inventory.deductSkuInventory(customerId, sku.getId(), discarded) != 1) {
|
||||
throw conflict("SKU inventory is insufficient during discard approval: " + item.getSkuCode());
|
||||
}
|
||||
List<CargoDiscardCarton> links = linksByItem.getOrDefault(item.getId(), List.of());
|
||||
if (links.size() != discarded) throw conflict("Reserved discard cartons are incomplete");
|
||||
for (CargoDiscardCarton link : links) {
|
||||
DropshipCarton carton = inventory.lockCarton(customerId, link.getCartonId());
|
||||
if (carton == null || inventory.changeCartonStatus(customerId, carton.getId(),
|
||||
"DISCARD_PENDING", "DISCARDED") != 1) throw conflict("Reserved cargo status changed before approval");
|
||||
cartonLedgers.insert(discardFact(customerId, application, item, carton, operatorId, occurredAt));
|
||||
}
|
||||
Long persistedAfter = inventory.skuPackages(customerId, sku.getId());
|
||||
long expectedAfter = before - discarded;
|
||||
if (persistedAfter == null || persistedAfter != expectedAfter) {
|
||||
throw conflict("Cargo discard package conservation check failed for SKU " + item.getSkuCode());
|
||||
}
|
||||
beforePackages = Math.addExact(beforePackages, before);
|
||||
discardedPackages = Math.addExact(discardedPackages, discarded);
|
||||
afterPackages = Math.addExact(afterPackages, persistedAfter);
|
||||
}
|
||||
if (beforePackages - discardedPackages != afterPackages
|
||||
|| discardedPackages != application.getTotalPackages()) {
|
||||
throw conflict("Cargo discard application package conservation check failed");
|
||||
}
|
||||
return new AdjustmentResult(beforePackages, discardedPackages, afterPackages);
|
||||
}
|
||||
|
||||
private DropshipCartonLedger discardFact(String customerId, CargoDiscardApplication application,
|
||||
CargoDiscardItem item, DropshipCarton carton,
|
||||
String operatorId, LocalDateTime occurredAt) {
|
||||
DropshipCartonLedger fact = new DropshipCartonLedger();
|
||||
fact.setId(IdWorker.getId()); fact.setCustomerId(customerId); fact.setOrderId(item.getOrderId());
|
||||
fact.setCartonId(carton.getId()); fact.setCartonMark(carton.getCartonMark()); fact.setSkuCode(item.getSkuCode());
|
||||
fact.setOperationType("DISCARD"); fact.setBeforePackages(1L); fact.setDeltaPackages(-1L); fact.setAfterPackages(0L);
|
||||
fact.setBusinessOrderNo(application.getDiscardNo());
|
||||
fact.setIdempotencyKey("CARGO_DISCARD:" + application.getId() + ":" + carton.getId());
|
||||
fact.setRemark("Warehouse discard approved by " + operatorId + remarkSuffix(application.getRemark()));
|
||||
fact.setOccurredAt(occurredAt); fact.setCreatedAt(occurredAt);
|
||||
return fact;
|
||||
}
|
||||
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private static String remarkSuffix(String value) {
|
||||
return value == null || value.isBlank() ? "" : "; " + value.trim();
|
||||
}
|
||||
private static ResponseStatusException conflict(String message) {
|
||||
return new ResponseStatusException(HttpStatus.CONFLICT, message);
|
||||
}
|
||||
|
||||
public record AdjustmentResult(long beforePackages, long discardedPackages, long afterPackages) {
|
||||
public String auditRemark(String reviewRemark) {
|
||||
String prefix = reviewRemark == null || reviewRemark.isBlank() ? "" : reviewRemark.trim() + "; ";
|
||||
return prefix + "inventoryBefore=" + beforePackages + ", discarded=" + discardedPackages
|
||||
+ ", inventoryAfter=" + afterPackages;
|
||||
}
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
package com.baiyue.lastleg.discard.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.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardResponses;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardCarton;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardStatusHistory;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardApplicationMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardCartonMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.discard.model.CargoDiscardCandidateRow;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.math.BigDecimal;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class CargoDiscardService {
|
||||
public static final String APPROVAL_PERMISSION = "lastleg:cargo-discard:approve";
|
||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||
public static final String APPROVED = "APPROVED";
|
||||
public static final String REJECTED = "REJECTED";
|
||||
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, APPROVED, REJECTED);
|
||||
|
||||
@Resource private CargoDiscardApplicationMapper applications;
|
||||
@Resource private CargoDiscardItemMapper items;
|
||||
@Resource private CargoDiscardCartonMapper discardCartons;
|
||||
@Resource private CargoDiscardStatusHistoryMapper histories;
|
||||
@Resource private CargoDiscardInventoryMapper inventory;
|
||||
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Value("${baiyue.lastleg.cargo-discard.fee-per-package-usd:5.00}")
|
||||
private BigDecimal feePerPackage = new BigDecimal("5.00");
|
||||
|
||||
public List<CargoDiscardResponses.Candidate> candidates(String customerId, String inboundCode) {
|
||||
List<CargoDiscardCandidateRow> rows = inventory.candidates(customerId, trim(inboundCode));
|
||||
Map<Long, CandidateAccumulator> grouped = new LinkedHashMap<>();
|
||||
for (CargoDiscardCandidateRow row : rows) {
|
||||
CandidateAccumulator value = grouped.computeIfAbsent(row.getOrderId(), ignored -> new CandidateAccumulator(row));
|
||||
value.add(row);
|
||||
}
|
||||
return grouped.values().stream().map(CandidateAccumulator::response).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CargoDiscardResponses.Detail create(String customerId, String operatorId,
|
||||
CargoDiscardRequests.Create request) {
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
CargoDiscardApplication replay = applications.findKey(customerId, key);
|
||||
if (replay != null) {
|
||||
if (!requestHash.equals(replay.getRequestHash())) {
|
||||
throw conflict("Idempotency key was reused with different cargo discard data");
|
||||
}
|
||||
return detail(customerId, replay.getDiscardNo());
|
||||
}
|
||||
Set<String> requestedDimensions = new LinkedHashSet<>();
|
||||
Map<Long, LastlegOrder> orderCache = new HashMap<>();
|
||||
String warehouseId = null;
|
||||
String warehouseName = null;
|
||||
int total = 0;
|
||||
for (CargoDiscardRequests.Item input : request.items()) {
|
||||
String dimension = input.orderId() + ":" + input.skuCode().trim();
|
||||
if (!requestedDimensions.add(dimension)) throw badRequest("Duplicate order/SKU discard item: " + dimension);
|
||||
LastlegOrder order = requireDropshipOrder(customerId, input.orderId());
|
||||
orderCache.put(order.getId(), order);
|
||||
if (warehouseId == null) {
|
||||
warehouseId = required(order.getOverseasWarehouseId(), "Order has no operating overseas warehouse");
|
||||
warehouseName = trim(order.getOverseasWarehouseName());
|
||||
} else if (!warehouseId.equals(order.getOverseasWarehouseId())) {
|
||||
throw conflict("One cargo discard application cannot span operating warehouses");
|
||||
}
|
||||
total = Math.addExact(total, input.discardPackages());
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
CargoDiscardApplication application = new CargoDiscardApplication();
|
||||
application.setId(IdWorker.getId()); application.setCustomerId(customerId);
|
||||
application.setDiscardNo("FH" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + application.getId());
|
||||
application.setOverseasWarehouseId(warehouseId); application.setOverseasWarehouseName(warehouseName);
|
||||
application.setInboundCodes(orderCache.values().stream().map(LastlegOrder::getInboundCode)
|
||||
.distinct().sorted().collect(java.util.stream.Collectors.joining(",")));
|
||||
application.setTotalPackages(total); application.setFeePerPackage(feePerPackage);
|
||||
application.setTotalFee(feePerPackage.multiply(BigDecimal.valueOf(total)));
|
||||
application.setCurrency("USD"); application.setStatus(PENDING_REVIEW);
|
||||
application.setRemark(trim(request.remark())); application.setIdempotencyKey(key);
|
||||
application.setRequestHash(requestHash); application.setCreatedBy(operatorId);
|
||||
application.setCreatedAt(now); application.setUpdatedAt(now);
|
||||
applications.insert(application);
|
||||
|
||||
for (CargoDiscardRequests.Item input : request.items()) {
|
||||
reserve(customerId, application, orderCache.get(input.orderId()), input, now);
|
||||
}
|
||||
history(customerId, application.getId(), null, PENDING_REVIEW, operatorId, request.remark(), now);
|
||||
return detail(customerId, application.getDiscardNo());
|
||||
}
|
||||
|
||||
public PageResult<CargoDiscardResponses.Summary> page(String customerId, CargoDiscardRequests.Query query) {
|
||||
validateQuery(query);
|
||||
LambdaQueryWrapper<CargoDiscardApplication> wrapper = new LambdaQueryWrapper<CargoDiscardApplication>()
|
||||
.eq(CargoDiscardApplication::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.discardNo()), CargoDiscardApplication::getDiscardNo, query.discardNo())
|
||||
.like(StringUtils.hasText(query.inboundCode()), CargoDiscardApplication::getInboundCodes, query.inboundCode())
|
||||
.eq(StringUtils.hasText(query.overseasWarehouseId()), CargoDiscardApplication::getOverseasWarehouseId,
|
||||
query.overseasWarehouseId())
|
||||
.eq(StringUtils.hasText(query.status()), CargoDiscardApplication::getStatus, query.status())
|
||||
.ge(query.createdFrom() != null, CargoDiscardApplication::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, CargoDiscardApplication::getCreatedAt, query.createdTo())
|
||||
.orderByDesc(CargoDiscardApplication::getCreatedAt).orderByDesc(CargoDiscardApplication::getId);
|
||||
Page<CargoDiscardApplication> result = applications.selectPage(Page.of(query.page(), query.size()), wrapper);
|
||||
List<CargoDiscardResponses.Summary> records = result.getRecords().stream().map(this::summary).toList();
|
||||
return new PageResult<>(records, query.page(), query.size(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
public CargoDiscardResponses.Detail detail(String customerId, String discardNo) {
|
||||
CargoDiscardApplication application = applications.findNo(customerId, discardNo);
|
||||
if (application == null) throw notFound("Cargo discard application not found");
|
||||
List<CargoDiscardResponses.Item> itemViews = items.listApplication(customerId, application.getId()).stream()
|
||||
.map(value -> new CargoDiscardResponses.Item(value.getId(), value.getOrderId(), value.getInboundCode(),
|
||||
value.getSkuCode(), value.getProductName(), value.getAvailablePackagesSnapshot(), value.getDiscardPackages()))
|
||||
.toList();
|
||||
List<CargoDiscardResponses.History> historyViews = histories.listApplication(customerId, application.getId()).stream()
|
||||
.map(value -> new CargoDiscardResponses.History(value.getFromStatus(), value.getToStatus(),
|
||||
value.getOperatorId(), value.getRemark(), value.getCreatedAt())).toList();
|
||||
return new CargoDiscardResponses.Detail(summary(application), application.getFeePerPackage(),
|
||||
application.getRemark(), application.getReviewedBy(), application.getReviewRemark(), itemViews, historyViews);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CargoDiscardResponses.Detail review(String customerId, String discardNo, String operatorId,
|
||||
CargoDiscardRequests.Review request) {
|
||||
String decision = request.decision().trim().toUpperCase();
|
||||
String next = switch (decision) {
|
||||
case "APPROVE", APPROVED -> APPROVED;
|
||||
case "REJECT", REJECTED -> REJECTED;
|
||||
default -> throw badRequest("Review decision must be APPROVE or REJECT");
|
||||
};
|
||||
CargoDiscardApplication application = applications.lockNo(customerId, discardNo);
|
||||
if (application == null) throw notFound("Cargo discard application not found");
|
||||
if (next.equals(application.getStatus())) return detail(customerId, discardNo);
|
||||
if (!PENDING_REVIEW.equals(application.getStatus())) throw conflict("Cargo discard application was already reviewed");
|
||||
|
||||
List<CargoDiscardItem> applicationItems = items.listApplication(customerId, application.getId());
|
||||
List<CargoDiscardCarton> links = discardCartons.listApplication(customerId, application.getId());
|
||||
Map<Long, List<CargoDiscardCarton>> linksByItem = links.stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(CargoDiscardCarton::getItemId));
|
||||
String historyRemark = request.remark();
|
||||
if (APPROVED.equals(next)) {
|
||||
CargoDiscardInventoryAdjustmentService.AdjustmentResult result = adjustments.discard(
|
||||
customerId, application, applicationItems, linksByItem, operatorId, now());
|
||||
historyRemark = result.auditRemark(request.remark());
|
||||
} else {
|
||||
release(customerId, links);
|
||||
}
|
||||
|
||||
LocalDateTime reviewedAt = now();
|
||||
application.setStatus(next); application.setReviewedBy(operatorId);
|
||||
application.setReviewRemark(trim(request.remark())); application.setReviewedAt(reviewedAt);
|
||||
application.setUpdatedAt(reviewedAt); applications.updateById(application);
|
||||
history(customerId, application.getId(), PENDING_REVIEW, next, operatorId, historyRemark, reviewedAt);
|
||||
return detail(customerId, discardNo);
|
||||
}
|
||||
|
||||
private void reserve(String customerId, CargoDiscardApplication application, LastlegOrder order,
|
||||
CargoDiscardRequests.Item input, LocalDateTime now) {
|
||||
String skuCode = input.skuCode().trim();
|
||||
if (inventory.countAvailable(customerId, order.getId(), skuCode) < input.discardPackages()) {
|
||||
throw conflict("Insufficient available packages for SKU " + skuCode + " on inbound " + order.getInboundCode());
|
||||
}
|
||||
List<DropshipCarton> selected = inventory.lockAvailable(customerId, order.getId(), skuCode, input.discardPackages());
|
||||
if (selected.size() != input.discardPackages()) throw conflict("Available cargo changed; retry the discard request");
|
||||
Map<Long, List<DropshipCarton>> byProfile = selected.stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(DropshipCarton::getProfileId, LinkedHashMap::new,
|
||||
java.util.stream.Collectors.toList()));
|
||||
for (Map.Entry<Long, List<DropshipCarton>> entry : byProfile.entrySet()) {
|
||||
DropshipSkuInventory sku = inventory.lockSkuInventoryDimension(customerId, order.getId(), entry.getKey(), skuCode);
|
||||
if (sku == null) throw conflict("SKU inventory disappeared while reserving cargo");
|
||||
CargoDiscardItem item = new CargoDiscardItem();
|
||||
item.setId(IdWorker.getId()); item.setApplicationId(application.getId()); item.setCustomerId(customerId);
|
||||
item.setOrderId(order.getId()); item.setInboundCode(order.getInboundCode()); item.setProfileId(entry.getKey());
|
||||
item.setSkuInventoryId(sku.getId()); item.setSkuCode(skuCode); item.setProductName(sku.getProductName());
|
||||
item.setAvailablePackagesSnapshot(Math.toIntExact(inventory.countAvailableProfile(
|
||||
customerId, order.getId(), entry.getKey(), skuCode)));
|
||||
item.setDiscardPackages(entry.getValue().size()); item.setCreatedAt(now); items.insert(item);
|
||||
for (DropshipCarton carton : entry.getValue()) {
|
||||
if (inventory.changeCartonStatus(customerId, carton.getId(), "AVAILABLE", "DISCARD_PENDING") != 1) {
|
||||
throw conflict("Cargo was reserved concurrently; retry the discard request");
|
||||
}
|
||||
CargoDiscardCarton link = new CargoDiscardCarton();
|
||||
link.setId(IdWorker.getId()); link.setApplicationId(application.getId()); link.setItemId(item.getId());
|
||||
link.setCustomerId(customerId); link.setCartonId(carton.getId()); link.setCartonMark(carton.getCartonMark());
|
||||
link.setCreatedAt(now); discardCartons.insert(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void release(String customerId, List<CargoDiscardCarton> links) {
|
||||
for (CargoDiscardCarton link : links) {
|
||||
if (inventory.changeCartonStatus(customerId, link.getCartonId(), "DISCARD_PENDING", "AVAILABLE") != 1) {
|
||||
throw conflict("Reserved cargo status changed before rejection");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LastlegOrder requireDropshipOrder(String customerId, Long orderId) {
|
||||
LastlegOrder order = orders.selectById(orderId);
|
||||
if (order == null || !customerId.equals(order.getCustomerId())) throw notFound("Inbound order not found");
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) {
|
||||
throw conflict("Cargo discard currently requires SKU/carton inventory from a one-click dropship order");
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
private CargoDiscardResponses.Summary summary(CargoDiscardApplication value) {
|
||||
return new CargoDiscardResponses.Summary(value.getId(), value.getDiscardNo(), value.getInboundCodes(),
|
||||
value.getOverseasWarehouseId(), value.getOverseasWarehouseName(), value.getTotalPackages(),
|
||||
value.getTotalFee(), value.getCurrency(), value.getStatus(), value.getCreatedBy(), value.getCreatedAt(),
|
||||
value.getReviewedAt());
|
||||
}
|
||||
|
||||
private void history(String customerId, Long applicationId, String from, String to, String operatorId,
|
||||
String remark, LocalDateTime createdAt) {
|
||||
CargoDiscardStatusHistory value = new CargoDiscardStatusHistory();
|
||||
value.setId(IdWorker.getId()); value.setApplicationId(applicationId); value.setCustomerId(customerId);
|
||||
value.setFromStatus(from); value.setToStatus(to); value.setOperatorId(operatorId);
|
||||
value.setRemark(trim(remark)); value.setCreatedAt(createdAt); histories.insert(value);
|
||||
}
|
||||
|
||||
private void validateQuery(CargoDiscardRequests.Query query) {
|
||||
if (StringUtils.hasText(query.status()) && !STATUSES.contains(query.status())) throw badRequest("Unsupported discard status");
|
||||
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
|
||||
throw badRequest("createdFrom must not be after createdTo");
|
||||
}
|
||||
}
|
||||
|
||||
private String hash(Object value) {
|
||||
try {
|
||||
byte[] json = objectMapper.writeValueAsBytes(value);
|
||||
return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json));
|
||||
} catch (JsonProcessingException | NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("Cannot hash cargo discard request", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalDateTime now() { return LocalDateTime.now(); }
|
||||
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
|
||||
private static String required(String value, String message) { String result = trim(value); if (result == null) throw conflict(message); return result; }
|
||||
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(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
|
||||
private static final class CandidateAccumulator {
|
||||
private final CargoDiscardCandidateRow first;
|
||||
private final Map<String, CandidateSkuAccumulator> skus = new LinkedHashMap<>();
|
||||
private CandidateAccumulator(CargoDiscardCandidateRow first) { this.first = first; }
|
||||
private void add(CargoDiscardCandidateRow row) {
|
||||
skus.computeIfAbsent(row.getSkuCode(), ignored -> new CandidateSkuAccumulator(row.getSkuCode(), row.getProductName()))
|
||||
.packages += row.getAvailablePackages() == null ? 0 : row.getAvailablePackages();
|
||||
}
|
||||
private CargoDiscardResponses.Candidate response() {
|
||||
List<CargoDiscardResponses.CandidateSku> skuViews = skus.values().stream()
|
||||
.map(value -> new CargoDiscardResponses.CandidateSku(value.skuCode, value.productName, value.packages)).toList();
|
||||
long packages = skuViews.stream().mapToLong(CargoDiscardResponses.CandidateSku::availablePackages).sum();
|
||||
return new CargoDiscardResponses.Candidate(first.getOrderId(), first.getInboundCode(), first.getInboundOrderNo(),
|
||||
first.getOverseasWarehouseId(), first.getOverseasWarehouseName(), first.getCreatedAt(), packages, skuViews);
|
||||
}
|
||||
}
|
||||
private static final class CandidateSkuAccumulator {
|
||||
private final String skuCode;
|
||||
private final String productName;
|
||||
private long packages;
|
||||
private CandidateSkuAccumulator(String skuCode, String productName) { this.skuCode = skuCode; this.productName = productName; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user