Merge remote-tracking branch 'origin/test' into test
This commit is contained in:
@@ -4997,3 +4997,53 @@ ALTER TABLE wh_pallet_operation
|
|||||||
MODIFY COLUMN after_location_id BIGINT NULL COMMENT '变更后库位ID,无库位托盘为空';
|
MODIFY COLUMN after_location_id BIGINT NULL COMMENT '变更后库位ID,无库位托盘为空';
|
||||||
USE baiyue_lastleg;
|
USE baiyue_lastleg;
|
||||||
SELECT 'PASS' AS migration_status, '无库位托盘状态流转结构已就绪' AS message;
|
SELECT 'PASS' AS migration_status, '无库位托盘状态流转结构已就绪' AS message;
|
||||||
|
|
||||||
|
-- 2026-09-14 贴标申请区分箱唛总文件、SKU总文件;明细文件沿用ll_relabel_item.relabel_file_*。
|
||||||
|
DROP PROCEDURE IF EXISTS add_relabel_total_document_columns;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE add_relabel_total_document_columns()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_lastleg' AND table_name='ll_relabel_application' AND column_name='carton_total_file_id') THEN
|
||||||
|
ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_id VARCHAR(128) NULL COMMENT '箱唛总文件ID' AFTER total_file_url;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_lastleg' AND table_name='ll_relabel_application' AND column_name='carton_total_file_name') THEN
|
||||||
|
ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_name VARCHAR(255) NULL COMMENT '箱唛总文件名' AFTER carton_total_file_id;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_lastleg' AND table_name='ll_relabel_application' AND column_name='carton_total_file_url') THEN
|
||||||
|
ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_url VARCHAR(1000) NULL COMMENT '箱唛总文件地址' AFTER carton_total_file_name;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_lastleg' AND table_name='ll_relabel_application' AND column_name='sku_total_file_id') THEN
|
||||||
|
ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_id VARCHAR(128) NULL COMMENT 'SKU总文件ID' AFTER carton_total_file_url;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_lastleg' AND table_name='ll_relabel_application' AND column_name='sku_total_file_name') THEN
|
||||||
|
ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_name VARCHAR(255) NULL COMMENT 'SKU总文件名' AFTER sku_total_file_id;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_lastleg' AND table_name='ll_relabel_application' AND column_name='sku_total_file_url') THEN
|
||||||
|
ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_url VARCHAR(1000) NULL COMMENT 'SKU总文件地址' AFTER sku_total_file_name;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
CALL add_relabel_total_document_columns();
|
||||||
|
DROP PROCEDURE IF EXISTS add_relabel_total_document_columns;
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS assert_relabel_document_columns_ready;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE assert_relabel_document_columns_ready()
|
||||||
|
BEGIN
|
||||||
|
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema='baiyue_lastleg' AND table_name='ll_relabel_application'
|
||||||
|
AND column_name IN ('carton_total_file_id','carton_total_file_name','carton_total_file_url',
|
||||||
|
'sku_total_file_id','sku_total_file_name','sku_total_file_url')) <> 6 THEN
|
||||||
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='贴标申请箱唛/SKU总文件字段未完整部署';
|
||||||
|
END IF;
|
||||||
|
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema='baiyue_lastleg' AND table_name='ll_relabel_item'
|
||||||
|
AND column_name IN ('relabel_file_id','relabel_file_name','relabel_file_url')) <> 3 THEN
|
||||||
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='贴标申请明细文件字段未完整部署';
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
CALL assert_relabel_document_columns_ready();
|
||||||
|
DROP PROCEDURE IF EXISTS assert_relabel_document_columns_ready;
|
||||||
|
|
||||||
|
SELECT 'PASS' AS migration_status, '贴标申请箱唛总文件、SKU总文件及明细文件字段已就绪' AS message;
|
||||||
|
|||||||
+14
@@ -60,6 +60,20 @@ public class CargoDiscardController {
|
|||||||
return ApiResponse.success(service.review(scope(user, customerId), discardNo, operator(user), request));
|
return ApiResponse.success(service.review(scope(user, customerId), discardNo, operator(user), request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{discardNo}/cancel")
|
||||||
|
public ApiResponse<CargoDiscardResponses.Detail> cancel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestParam(required = false) String customerId, @PathVariable String discardNo) {
|
||||||
|
requireApprovalPermission(user);
|
||||||
|
return ApiResponse.success(service.cancel(scope(user, customerId), discardNo, operator(user), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{discardNo}/complete")
|
||||||
|
public ApiResponse<CargoDiscardResponses.Detail> complete(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestParam(required = false) String customerId, @PathVariable String discardNo) {
|
||||||
|
requireApprovalPermission(user);
|
||||||
|
return ApiResponse.success(service.complete(scope(user, customerId), discardNo, operator(user)));
|
||||||
|
}
|
||||||
|
|
||||||
private String scope(AuthenticatedUser user, String requested) {
|
private String scope(AuthenticatedUser user, String requested) {
|
||||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||||
|
|||||||
+3
@@ -30,6 +30,9 @@ public class MerchantCargoDiscardController {
|
|||||||
@GetMapping("/{discardNo}")
|
@GetMapping("/{discardNo}")
|
||||||
public ApiResponse<CargoDiscardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<CargoDiscardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable String discardNo) { return ApiResponse.success(service.detail(customer(user), discardNo)); }
|
@PathVariable String discardNo) { return ApiResponse.success(service.detail(customer(user), discardNo)); }
|
||||||
|
@PostMapping("/{discardNo}/cancel")
|
||||||
|
public ApiResponse<CargoDiscardResponses.Detail> cancel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@PathVariable String discardNo) { return ApiResponse.success(service.cancel(customer(user), discardNo, operator(user), false)); }
|
||||||
private String customer(AuthenticatedUser user) {
|
private String customer(AuthenticatedUser user) {
|
||||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||||
|
|||||||
@@ -21,5 +21,9 @@ public class CargoDiscardItem {
|
|||||||
private String productName;
|
private String productName;
|
||||||
private Integer availablePackagesSnapshot;
|
private Integer availablePackagesSnapshot;
|
||||||
private Integer discardPackages;
|
private Integer discardPackages;
|
||||||
|
private Integer actualDiscardPackages;
|
||||||
|
private String pdaStatus;
|
||||||
|
private String pdaOperatorId;
|
||||||
|
private LocalDateTime pdaCompletedAt;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -16,4 +16,7 @@ public interface CargoDiscardApplicationMapper extends BaseMapper<CargoDiscardAp
|
|||||||
|
|
||||||
@Select("SELECT * FROM ll_cargo_discard_application WHERE customer_id=#{customerId} AND discard_no=#{discardNo} LIMIT 1 FOR UPDATE")
|
@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);
|
CargoDiscardApplication lockNo(@Param("customerId") String customerId, @Param("discardNo") String discardNo);
|
||||||
|
|
||||||
|
@Select("SELECT * FROM ll_cargo_discard_application WHERE discard_no=#{discardNo} AND overseas_warehouse_id=#{warehouseId} LIMIT 1 FOR UPDATE")
|
||||||
|
CargoDiscardApplication findNoByWarehouse(@Param("discardNo") String discardNo, @Param("warehouseId") String warehouseId);
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package com.baiyue.lastleg.discard.pda.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.discard.pda.dto.PdaCargoDiscardDtos;
|
||||||
|
import com.baiyue.lastleg.discard.pda.service.PdaCargoDiscardService;
|
||||||
|
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/pda/v1/last-leg/cargo-discards")
|
||||||
|
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||||
|
public class PdaCargoDiscardController {
|
||||||
|
@Resource private PdaCargoDiscardService service;
|
||||||
|
@GetMapping public ApiResponse<List<PdaCargoDiscardDtos.PalletTask>> tasks(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||||
|
return ApiResponse.success(service.tasks(PdaWarehouseIdentity.require(user)));
|
||||||
|
}
|
||||||
|
@PostMapping("/resolve") public ApiResponse<PdaCargoDiscardDtos.PalletTask> resolve(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody PdaCargoDiscardDtos.Scan request) {
|
||||||
|
return ApiResponse.success(service.resolve(PdaWarehouseIdentity.require(user), request.palletNo()));
|
||||||
|
}
|
||||||
|
@PostMapping("/{discardNo}/pallets/{palletNo}/confirm")
|
||||||
|
public ApiResponse<PdaCargoDiscardDtos.PalletTask> confirm(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@PathVariable String discardNo, @PathVariable String palletNo, @Valid @RequestBody PdaCargoDiscardDtos.Confirm request) {
|
||||||
|
return ApiResponse.success(service.confirm(PdaWarehouseIdentity.require(user), discardNo, palletNo, request));
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.baiyue.lastleg.discard.pda.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.PositiveOrZero;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class PdaCargoDiscardDtos {
|
||||||
|
private PdaCargoDiscardDtos() { }
|
||||||
|
public record Scan(@NotBlank @Size(max = 128) String palletNo) { }
|
||||||
|
public record ActualLine(@NotNull Long itemId, @PositiveOrZero int actualPackages) { }
|
||||||
|
public record Confirm(@NotEmpty List<@Valid ActualLine> lines) { }
|
||||||
|
public record Sku(Long itemId, String skuCode, String productName, int plannedPackages,
|
||||||
|
Integer actualPackages, String status) { }
|
||||||
|
public record PalletTask(String discardNo, String customerId, String overseasWarehouseId,
|
||||||
|
String palletNo, Long profileId, String status, List<Sku> skus) { }
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package com.baiyue.lastleg.discard.pda.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baiyue.lastleg.discard.dto.CargoDiscardResponses;
|
||||||
|
import com.baiyue.lastleg.discard.entity.*;
|
||||||
|
import com.baiyue.lastleg.discard.mapper.*;
|
||||||
|
import com.baiyue.lastleg.discard.pda.dto.PdaCargoDiscardDtos;
|
||||||
|
import com.baiyue.lastleg.discard.service.CargoDiscardInventoryAdjustmentService;
|
||||||
|
import com.baiyue.lastleg.discard.service.CargoDiscardService;
|
||||||
|
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||||
|
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||||
|
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PdaCargoDiscardService {
|
||||||
|
@Resource private CargoDiscardApplicationMapper applications;
|
||||||
|
@Resource private CargoDiscardItemMapper items;
|
||||||
|
@Resource private CargoDiscardCartonMapper cartons;
|
||||||
|
@Resource private DropshipOrderProfileMapper profiles;
|
||||||
|
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
|
||||||
|
@Resource private CargoDiscardService discardService;
|
||||||
|
|
||||||
|
public List<PdaCargoDiscardDtos.PalletTask> tasks(PdaWarehouseIdentity identity) {
|
||||||
|
return applications.selectList(new LambdaQueryWrapper<CargoDiscardApplication>()
|
||||||
|
.eq(CargoDiscardApplication::getOverseasWarehouseId, String.valueOf(identity.overseasWarehouseId()))
|
||||||
|
.eq(CargoDiscardApplication::getStatus, CargoDiscardService.APPROVED)
|
||||||
|
.orderByAsc(CargoDiscardApplication::getCreatedAt))
|
||||||
|
.stream().flatMap(a -> palletTasks(a).stream()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PdaCargoDiscardDtos.PalletTask resolve(PdaWarehouseIdentity identity, String palletNo) {
|
||||||
|
return tasks(identity).stream().filter(t -> t.palletNo().equals(palletNo.trim())).findFirst()
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "该仓库没有此托盘的待废弃任务"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PdaCargoDiscardDtos.PalletTask confirm(PdaWarehouseIdentity identity, String discardNo,
|
||||||
|
String palletNo, PdaCargoDiscardDtos.Confirm request) {
|
||||||
|
CargoDiscardApplication application = applications.findNoByWarehouse(discardNo,
|
||||||
|
String.valueOf(identity.overseasWarehouseId()));
|
||||||
|
if (application == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "废弃任务不存在或不属于当前仓库");
|
||||||
|
if (!CargoDiscardService.APPROVED.equals(application.getStatus())) throw conflict("废弃任务当前不可进行 PDA 作业");
|
||||||
|
PdaCargoDiscardDtos.PalletTask task = palletTasks(application).stream().filter(t -> t.palletNo().equals(palletNo.trim()))
|
||||||
|
.findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "托盘不属于该废弃单"));
|
||||||
|
Map<Long,Integer> actuals = request.lines().stream().collect(Collectors.toMap(PdaCargoDiscardDtos.ActualLine::itemId,
|
||||||
|
PdaCargoDiscardDtos.ActualLine::actualPackages, (a,b) -> { throw conflict("PDA 明细重复"); }));
|
||||||
|
if (!actuals.keySet().equals(task.skus().stream().map(PdaCargoDiscardDtos.Sku::itemId).collect(Collectors.toSet()))) {
|
||||||
|
throw conflict("必须提交该托盘全部 SKU 的实际废弃数量");
|
||||||
|
}
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
for (PdaCargoDiscardDtos.Sku line : task.skus()) {
|
||||||
|
CargoDiscardItem item = items.selectById(line.itemId());
|
||||||
|
if ("COMPLETED".equals(item.getPdaStatus())) {
|
||||||
|
if (!Objects.equals(item.getActualDiscardPackages(), actuals.get(item.getId()))) throw conflict("已完成托盘的实际数量不能修改");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<CargoDiscardCarton> links = cartons.listApplication(application.getCustomerId(), application.getId()).stream()
|
||||||
|
.filter(c -> c.getItemId().equals(item.getId())).toList();
|
||||||
|
adjustments.discardPalletItem(application.getCustomerId(), application, item, links,
|
||||||
|
actuals.get(item.getId()), identity.operatorKey(), now);
|
||||||
|
item.setActualDiscardPackages(actuals.get(item.getId())); item.setPdaStatus("COMPLETED");
|
||||||
|
item.setPdaOperatorId(identity.operatorKey()); item.setPdaCompletedAt(now); items.updateById(item);
|
||||||
|
}
|
||||||
|
if (items.listApplication(application.getCustomerId(), application.getId()).stream()
|
||||||
|
.allMatch(i -> "COMPLETED".equals(i.getPdaStatus()))) {
|
||||||
|
int actualTotal = items.listApplication(application.getCustomerId(), application.getId()).stream()
|
||||||
|
.mapToInt(i -> i.getActualDiscardPackages() == null ? 0 : i.getActualDiscardPackages()).sum();
|
||||||
|
application.setTotalPackages(actualTotal);
|
||||||
|
application.setTotalFee(application.getFeePerPackage().multiply(java.math.BigDecimal.valueOf(actualTotal)));
|
||||||
|
discardService.updateLinkedStatus(application.getCustomerId(), application, CargoDiscardService.APPROVED,
|
||||||
|
CargoDiscardService.PDA_COMPLETED, identity.operatorKey(), "全部托盘 PDA 废弃作业完成,待仓库确认");
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PdaCargoDiscardDtos.PalletTask> palletTasks(CargoDiscardApplication application) {
|
||||||
|
return items.listApplication(application.getCustomerId(), application.getId()).stream()
|
||||||
|
.collect(Collectors.groupingBy(CargoDiscardItem::getProfileId, LinkedHashMap::new, Collectors.toList()))
|
||||||
|
.entrySet().stream().map(entry -> {
|
||||||
|
DropshipOrderProfile profile = profiles.selectById(entry.getKey());
|
||||||
|
if (profile == null || !application.getCustomerId().equals(profile.getCustomerId())) throw conflict("废弃明细关联托盘不存在");
|
||||||
|
List<PdaCargoDiscardDtos.Sku> skus = entry.getValue().stream().map(i -> new PdaCargoDiscardDtos.Sku(
|
||||||
|
i.getId(), i.getSkuCode(), i.getProductName(), i.getDiscardPackages(), i.getActualDiscardPackages(),
|
||||||
|
i.getPdaStatus() == null ? "PENDING" : i.getPdaStatus())).toList();
|
||||||
|
String status = skus.stream().allMatch(s -> "COMPLETED".equals(s.status())) ? "COMPLETED" : "PENDING";
|
||||||
|
return new PdaCargoDiscardDtos.PalletTask(application.getDiscardNo(), application.getCustomerId(),
|
||||||
|
application.getOverseasWarehouseId(), profile.getPalletNo(), profile.getId(), status, skus);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||||
|
}
|
||||||
+39
@@ -9,6 +9,9 @@ import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
|||||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
||||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||||
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
|
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
|
||||||
|
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||||
|
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||||
|
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -25,6 +28,42 @@ import java.util.Map;
|
|||||||
public class CargoDiscardInventoryAdjustmentService {
|
public class CargoDiscardInventoryAdjustmentService {
|
||||||
@Resource private CargoDiscardInventoryMapper inventory;
|
@Resource private CargoDiscardInventoryMapper inventory;
|
||||||
@Resource private DropshipCartonLedgerMapper cartonLedgers;
|
@Resource private DropshipCartonLedgerMapper cartonLedgers;
|
||||||
|
@Resource private DropshipOrderProfileMapper profiles;
|
||||||
|
@Resource private InventoryPositionMapper positions;
|
||||||
|
@Resource private InventoryBalanceMapper balances;
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.MANDATORY)
|
||||||
|
public AdjustmentResult discardPalletItem(String customerId, CargoDiscardApplication application,
|
||||||
|
CargoDiscardItem item, List<CargoDiscardCarton> links,
|
||||||
|
int actualPackages, String operatorId, LocalDateTime occurredAt) {
|
||||||
|
if (actualPackages < 0 || actualPackages > links.size()) throw conflict("实际废弃数量超出该托盘申请数量");
|
||||||
|
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
|
||||||
|
if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode());
|
||||||
|
long before = value(sku.getPackageCount());
|
||||||
|
if (before < actualPackages || inventory.deductSkuInventory(customerId, sku.getId(), actualPackages) != 1) {
|
||||||
|
throw conflict("托盘 SKU 库存不足: " + item.getSkuCode());
|
||||||
|
}
|
||||||
|
var profile = profiles.selectById(item.getProfileId());
|
||||||
|
var position = profile == null || profile.getInventoryPositionId() == null ? null
|
||||||
|
: positions.findTenantPosition(customerId, profile.getInventoryPositionId());
|
||||||
|
if (profile == null || !customerId.equals(profile.getCustomerId()) || position == null
|
||||||
|
|| positions.deductAvailable(customerId, profile.getInventoryPositionId(), actualPackages) != 1
|
||||||
|
|| balances.deductAvailable(customerId, position.getBalanceId(), actualPackages) != 1) {
|
||||||
|
throw conflict("托盘库存明细数量不足或托盘位置不存在");
|
||||||
|
}
|
||||||
|
for (int index = 0; index < links.size(); index++) {
|
||||||
|
CargoDiscardCarton link = links.get(index);
|
||||||
|
String target = index < actualPackages ? "DISCARDED" : "AVAILABLE";
|
||||||
|
DropshipCarton carton = inventory.lockCarton(customerId, link.getCartonId());
|
||||||
|
if (carton == null || inventory.changeCartonStatus(customerId, carton.getId(), "DISCARD_PENDING", target) != 1) {
|
||||||
|
throw conflict("托盘预留箱状态已变化");
|
||||||
|
}
|
||||||
|
if (index < actualPackages) cartonLedgers.insert(discardFact(customerId, application, item, carton, operatorId, occurredAt));
|
||||||
|
}
|
||||||
|
long after = value(inventory.skuPackages(customerId, sku.getId()));
|
||||||
|
if (after != before - actualPackages) throw conflict("托盘 SKU 库存扣减不守恒");
|
||||||
|
return new AdjustmentResult(before, actualPackages, after);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.MANDATORY)
|
@Transactional(propagation = Propagation.MANDATORY)
|
||||||
public AdjustmentResult discard(String customerId, CargoDiscardApplication application,
|
public AdjustmentResult discard(String customerId, CargoDiscardApplication application,
|
||||||
|
|||||||
+103
-6
@@ -19,7 +19,9 @@ import com.baiyue.lastleg.discard.model.CargoDiscardCandidateRow;
|
|||||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
|
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||||
|
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -49,7 +51,10 @@ public class CargoDiscardService {
|
|||||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||||
public static final String APPROVED = "APPROVED";
|
public static final String APPROVED = "APPROVED";
|
||||||
public static final String REJECTED = "REJECTED";
|
public static final String REJECTED = "REJECTED";
|
||||||
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, APPROVED, REJECTED);
|
public static final String CANCELLED = "CANCELLED";
|
||||||
|
public static final String PDA_COMPLETED = "PDA_COMPLETED";
|
||||||
|
public static final String COMPLETED = "COMPLETED";
|
||||||
|
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, APPROVED, REJECTED, CANCELLED, PDA_COMPLETED, COMPLETED);
|
||||||
|
|
||||||
@Resource private CargoDiscardApplicationMapper applications;
|
@Resource private CargoDiscardApplicationMapper applications;
|
||||||
@Resource private CargoDiscardItemMapper items;
|
@Resource private CargoDiscardItemMapper items;
|
||||||
@@ -58,6 +63,7 @@ public class CargoDiscardService {
|
|||||||
@Resource private CargoDiscardInventoryMapper inventory;
|
@Resource private CargoDiscardInventoryMapper inventory;
|
||||||
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
|
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
|
||||||
@Resource private LastlegOrderMapper orders;
|
@Resource private LastlegOrderMapper orders;
|
||||||
|
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
|
||||||
@Resource private ObjectMapper objectMapper;
|
@Resource private ObjectMapper objectMapper;
|
||||||
@Value("${baiyue.lastleg.cargo-discard.fee-per-package-usd:5.00}")
|
@Value("${baiyue.lastleg.cargo-discard.fee-per-package-usd:5.00}")
|
||||||
private BigDecimal feePerPackage = new BigDecimal("5.00");
|
private BigDecimal feePerPackage = new BigDecimal("5.00");
|
||||||
@@ -118,6 +124,37 @@ public class CargoDiscardService {
|
|||||||
application.setCreatedAt(now); application.setUpdatedAt(now);
|
application.setCreatedAt(now); application.setUpdatedAt(now);
|
||||||
applications.insert(application);
|
applications.insert(application);
|
||||||
|
|
||||||
|
// A discard request is also a customer-visible tail-leg order. Keep the
|
||||||
|
// application as the workflow record and link the order through its
|
||||||
|
// immutable source id so cancellation/status changes stay traceable.
|
||||||
|
LastlegOrder tailOrder = new LastlegOrder();
|
||||||
|
tailOrder.setId(IdWorker.getId());
|
||||||
|
tailOrder.setOrderNo(application.getDiscardNo());
|
||||||
|
tailOrder.setCustomerId(customerId);
|
||||||
|
tailOrder.setOrderType("CARGO_DISCARD");
|
||||||
|
tailOrder.setInboundCode(application.getInboundCodes());
|
||||||
|
tailOrder.setInboundOrderNo(application.getDiscardNo());
|
||||||
|
tailOrder.setSourceRegion("LASTLEG_DISCARD");
|
||||||
|
tailOrder.setSourceInboundId("CARGO_DISCARD:" + application.getId());
|
||||||
|
tailOrder.setSourceOrderId(String.valueOf(application.getId()));
|
||||||
|
tailOrder.setSourceOrderNo(application.getDiscardNo());
|
||||||
|
tailOrder.setOverseasWarehouseId(warehouseId);
|
||||||
|
tailOrder.setOverseasWarehouseName(warehouseName);
|
||||||
|
tailOrder.setPackageCount(total);
|
||||||
|
tailOrder.setCurrentStatus(PENDING_REVIEW);
|
||||||
|
tailOrder.setVersion(0);
|
||||||
|
tailOrder.setCreatedAt(now);
|
||||||
|
tailOrder.setUpdatedAt(now);
|
||||||
|
tailOrder.setSourceEventId("CARGO_DISCARD:" + application.getId());
|
||||||
|
tailOrder.setSourceLoadingPlanId("CARGO_DISCARD");
|
||||||
|
tailOrder.setSourcePayloadHash(requestHash);
|
||||||
|
orders.insert(tailOrder);
|
||||||
|
LastlegOrderStatusHistory initialOrderHistory = new LastlegOrderStatusHistory();
|
||||||
|
initialOrderHistory.setId(IdWorker.getId()); initialOrderHistory.setCustomerId(customerId); initialOrderHistory.setOrderId(tailOrder.getId());
|
||||||
|
initialOrderHistory.setFromStatus(null); initialOrderHistory.setToStatus(PENDING_REVIEW);
|
||||||
|
initialOrderHistory.setEventId("CARGO_DISCARD_CREATE:" + application.getId()); initialOrderHistory.setRemark(trim(request.remark())); initialOrderHistory.setCreatedAt(now);
|
||||||
|
orderHistories.insert(initialOrderHistory);
|
||||||
|
|
||||||
for (CargoDiscardRequests.Item input : request.items()) {
|
for (CargoDiscardRequests.Item input : request.items()) {
|
||||||
reserve(customerId, application, orderCache.get(input.orderId()), input, now);
|
reserve(customerId, application, orderCache.get(input.orderId()), input, now);
|
||||||
}
|
}
|
||||||
@@ -175,11 +212,7 @@ public class CargoDiscardService {
|
|||||||
Map<Long, List<CargoDiscardCarton>> linksByItem = links.stream()
|
Map<Long, List<CargoDiscardCarton>> linksByItem = links.stream()
|
||||||
.collect(java.util.stream.Collectors.groupingBy(CargoDiscardCarton::getItemId));
|
.collect(java.util.stream.Collectors.groupingBy(CargoDiscardCarton::getItemId));
|
||||||
String historyRemark = request.remark();
|
String historyRemark = request.remark();
|
||||||
if (APPROVED.equals(next)) {
|
if (REJECTED.equals(next)) {
|
||||||
CargoDiscardInventoryAdjustmentService.AdjustmentResult result = adjustments.discard(
|
|
||||||
customerId, application, applicationItems, linksByItem, operatorId, now());
|
|
||||||
historyRemark = result.auditRemark(request.remark());
|
|
||||||
} else {
|
|
||||||
release(customerId, links);
|
release(customerId, links);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,10 +220,74 @@ public class CargoDiscardService {
|
|||||||
application.setStatus(next); application.setReviewedBy(operatorId);
|
application.setStatus(next); application.setReviewedBy(operatorId);
|
||||||
application.setReviewRemark(trim(request.remark())); application.setReviewedAt(reviewedAt);
|
application.setReviewRemark(trim(request.remark())); application.setReviewedAt(reviewedAt);
|
||||||
application.setUpdatedAt(reviewedAt); applications.updateById(application);
|
application.setUpdatedAt(reviewedAt); applications.updateById(application);
|
||||||
|
LastlegOrder linked = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId());
|
||||||
|
if (linked != null) {
|
||||||
|
linked.setCurrentStatus(next); linked.setVersion((linked.getVersion() == null ? 0 : linked.getVersion()) + 1); linked.setUpdatedAt(reviewedAt);
|
||||||
|
orders.updateById(linked);
|
||||||
|
LastlegOrderStatusHistory orderHistory = new LastlegOrderStatusHistory(); orderHistory.setId(IdWorker.getId());
|
||||||
|
orderHistory.setCustomerId(customerId); orderHistory.setOrderId(linked.getId()); orderHistory.setFromStatus(PENDING_REVIEW);
|
||||||
|
orderHistory.setToStatus(next); orderHistory.setEventId("CARGO_DISCARD_REVIEW:" + application.getId()); orderHistory.setRemark(historyRemark); orderHistory.setCreatedAt(reviewedAt);
|
||||||
|
orderHistories.insert(orderHistory);
|
||||||
|
}
|
||||||
history(customerId, application.getId(), PENDING_REVIEW, next, operatorId, historyRemark, reviewedAt);
|
history(customerId, application.getId(), PENDING_REVIEW, next, operatorId, historyRemark, reviewedAt);
|
||||||
return detail(customerId, discardNo);
|
return detail(customerId, discardNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CargoDiscardResponses.Detail complete(String customerId, String discardNo, String operatorId) {
|
||||||
|
CargoDiscardApplication application = applications.lockNo(customerId, discardNo);
|
||||||
|
if (application == null) throw notFound("Cargo discard application not found");
|
||||||
|
if (COMPLETED.equals(application.getStatus())) return detail(customerId, discardNo);
|
||||||
|
if (!PDA_COMPLETED.equals(application.getStatus())) throw conflict("全部托盘 PDA 作业完成后才能确认废弃完成");
|
||||||
|
updateLinkedStatus(customerId, application, PDA_COMPLETED, COMPLETED, operatorId, "仓库确认废弃完成");
|
||||||
|
return detail(customerId, discardNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLinkedStatus(String customerId, CargoDiscardApplication application, String from, String to,
|
||||||
|
String operatorId, String remark) {
|
||||||
|
LocalDateTime at = now(); application.setStatus(to); application.setUpdatedAt(at); applications.updateById(application);
|
||||||
|
LastlegOrder linked = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId());
|
||||||
|
if (linked != null) {
|
||||||
|
linked.setCurrentStatus(to); linked.setPackageCount(application.getTotalPackages());
|
||||||
|
linked.setVersion((linked.getVersion() == null ? 0 : linked.getVersion()) + 1); linked.setUpdatedAt(at); orders.updateById(linked);
|
||||||
|
LastlegOrderStatusHistory h = new LastlegOrderStatusHistory(); h.setId(IdWorker.getId()); h.setCustomerId(customerId); h.setOrderId(linked.getId());
|
||||||
|
h.setFromStatus(from); h.setToStatus(to); h.setEventId("CARGO_DISCARD_STATUS:" + application.getId() + ":" + to); h.setRemark(remark); h.setCreatedAt(at); orderHistories.insert(h);
|
||||||
|
}
|
||||||
|
history(customerId, application.getId(), from, to, operatorId, remark, at);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CargoDiscardResponses.Detail cancel(String customerId, String discardNo, String operatorId, boolean platform) {
|
||||||
|
CargoDiscardApplication application = applications.lockNo(customerId, discardNo);
|
||||||
|
if (application == null) throw notFound("Cargo discard application not found");
|
||||||
|
if (CANCELLED.equals(application.getStatus())) return detail(customerId, discardNo);
|
||||||
|
if (REJECTED.equals(application.getStatus())) throw conflict("已驳回的废弃申请不能取消");
|
||||||
|
if (PDA_COMPLETED.equals(application.getStatus()) || COMPLETED.equals(application.getStatus())) throw conflict("PDA 已完成库存扣减,废弃申请不能取消");
|
||||||
|
if (items.listApplication(customerId, application.getId()).stream().anyMatch(i -> "COMPLETED".equals(i.getPdaStatus()))) {
|
||||||
|
throw conflict("已有托盘完成废弃作业,不能取消申请");
|
||||||
|
}
|
||||||
|
if (!platform && !PENDING_REVIEW.equals(application.getStatus())) {
|
||||||
|
throw conflict("客户只能取消待审核的废弃申请");
|
||||||
|
}
|
||||||
|
List<CargoDiscardCarton> links = discardCartons.listApplication(customerId, application.getId());
|
||||||
|
release(customerId, links);
|
||||||
|
LocalDateTime at = now();
|
||||||
|
String from = application.getStatus();
|
||||||
|
application.setStatus(CANCELLED); application.setReviewedBy(platform ? operatorId : application.getReviewedBy());
|
||||||
|
application.setReviewRemark(platform ? "平台取消" : application.getReviewRemark());
|
||||||
|
application.setUpdatedAt(at); applications.updateById(application);
|
||||||
|
LastlegOrder order = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId());
|
||||||
|
if (order != null) {
|
||||||
|
order.setCurrentStatus(CANCELLED); order.setVersion((order.getVersion() == null ? 0 : order.getVersion()) + 1); order.setUpdatedAt(at);
|
||||||
|
orders.updateById(order);
|
||||||
|
LastlegOrderStatusHistory h = new LastlegOrderStatusHistory(); h.setId(IdWorker.getId()); h.setCustomerId(customerId); h.setOrderId(order.getId());
|
||||||
|
h.setFromStatus(from); h.setToStatus(CANCELLED); h.setEventId("CARGO_DISCARD_CANCEL:" + application.getId()); h.setRemark("废弃申请已取消"); h.setCreatedAt(at);
|
||||||
|
orderHistories.insert(h);
|
||||||
|
}
|
||||||
|
history(customerId, application.getId(), from, CANCELLED, operatorId, "废弃申请已取消", at);
|
||||||
|
return detail(customerId, discardNo);
|
||||||
|
}
|
||||||
|
|
||||||
private void reserve(String customerId, CargoDiscardApplication application, LastlegOrder order,
|
private void reserve(String customerId, CargoDiscardApplication application, LastlegOrder order,
|
||||||
CargoDiscardRequests.Item input, LocalDateTime now) {
|
CargoDiscardRequests.Item input, LocalDateTime now) {
|
||||||
String skuCode = input.skuCode().trim();
|
String skuCode = input.skuCode().trim();
|
||||||
|
|||||||
+3
-2
@@ -54,7 +54,8 @@ public record DispatchPalletCandidate(
|
|||||||
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null, null);
|
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null, null);
|
||||||
}
|
}
|
||||||
public String sourcePalletKey() {
|
public String sourcePalletKey() {
|
||||||
// A plan pallet keeps the same lock identity before and after the physical pallet is received.
|
// Once received, the physical pallet is the shared resource even when it contains
|
||||||
return planPalletId != null ? "PLAN:" + planPalletId : "RECEIVED:" + warehousePalletId;
|
// allocations from multiple inbound orders/plan-pallet rows.
|
||||||
|
return warehousePalletId != null ? "RECEIVED:" + warehousePalletId : "PLAN:" + planPalletId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -637,8 +637,9 @@ public class DispatchService {
|
|||||||
|
|
||||||
private DispatchPalletLock activeLock(String customerId, DispatchPalletCandidate candidate) {
|
private DispatchPalletLock activeLock(String customerId, DispatchPalletCandidate candidate) {
|
||||||
DispatchPalletLock active = locks.lockActive(customerId, candidate.sourcePalletKey());
|
DispatchPalletLock active = locks.lockActive(customerId, candidate.sourcePalletKey());
|
||||||
if (active == null && candidate.warehousePalletId() != null) {
|
if (active == null && candidate.warehousePalletId() != null && candidate.planPalletId() != null) {
|
||||||
active = locks.lockActive(customerId, "RECEIVED:" + candidate.warehousePalletId());
|
// Read legacy plan-level locks created before physical-pallet locking was introduced.
|
||||||
|
active = locks.lockActive(customerId, "PLAN:" + candidate.planPalletId());
|
||||||
}
|
}
|
||||||
return active;
|
return active;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -64,4 +64,9 @@ public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
|
|||||||
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if></script>")
|
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if></script>")
|
||||||
InventorySummary summarize(@Param("customerId") String customerId,
|
InventorySummary summarize(@Param("customerId") String customerId,
|
||||||
@Param("overseasWarehouseId") String overseasWarehouseId);
|
@Param("overseasWarehouseId") String overseasWarehouseId);
|
||||||
|
|
||||||
|
@Update("UPDATE ll_inventory_balance SET available_packages=available_packages-#{packages},version=version+1,updated_at=NOW(3) " +
|
||||||
|
"WHERE customer_id=#{customerId} AND id=#{balanceId} AND available_packages>=#{packages}")
|
||||||
|
int deductAvailable(@Param("customerId") String customerId, @Param("balanceId") Long balanceId,
|
||||||
|
@Param("packages") int packages);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -6,6 +6,7 @@ import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
|||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -31,4 +32,10 @@ public interface InventoryPositionMapper extends BaseMapper<InventoryPosition> {
|
|||||||
@Param("balanceId") Long balanceId,
|
@Param("balanceId") Long balanceId,
|
||||||
@Param("palletId") Long palletId,
|
@Param("palletId") Long palletId,
|
||||||
@Param("batchNo") String batchNo);
|
@Param("batchNo") String batchNo);
|
||||||
|
|
||||||
|
@Update("UPDATE ll_inventory_position SET available_packages=available_packages-#{packages}," +
|
||||||
|
"status=CASE WHEN available_packages-#{packages}=0 THEN 'EMPTY' ELSE status END,version=version+1,updated_at=NOW(3) " +
|
||||||
|
"WHERE customer_id=#{customerId} AND id=#{positionId} AND available_packages>=#{packages}")
|
||||||
|
int deductAvailable(@Param("customerId") String customerId, @Param("positionId") Long positionId,
|
||||||
|
@Param("packages") int packages);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -51,6 +51,14 @@ public class MerchantRelabelApplicationController {
|
|||||||
@PathVariable String relabelNo, @RequestPart("file") MultipartFile file) {
|
@PathVariable String relabelNo, @RequestPart("file") MultipartFile file) {
|
||||||
return ApiResponse.success(service.uploadShippingLabel(customer(user), relabelNo, operatorLong(user), file));
|
return ApiResponse.success(service.uploadShippingLabel(customer(user), relabelNo, operatorLong(user), file));
|
||||||
}
|
}
|
||||||
|
@PostMapping(value = "/{relabelNo}/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public ApiResponse<RelabelResponses.Detail> uploadFile(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@PathVariable String relabelNo, @RequestParam String scope, @RequestParam(required = false) Long itemId,
|
||||||
|
@RequestPart("file") MultipartFile file) {
|
||||||
|
String kind = "SKU_TOTAL".equalsIgnoreCase(scope) ? "SKU_TOTAL" : "CARTON_TOTAL";
|
||||||
|
if ("ITEM".equalsIgnoreCase(scope)) return ApiResponse.success(service.uploadItemFile(customer(user), relabelNo, itemId == null ? 0L : itemId, operatorLong(user), file));
|
||||||
|
return ApiResponse.success(service.uploadTotalFile(customer(user), relabelNo, operatorLong(user), file, kind));
|
||||||
|
}
|
||||||
@GetMapping("/{relabelNo}/shipping-label/{fileId}/download")
|
@GetMapping("/{relabelNo}/shipping-label/{fileId}/download")
|
||||||
public ResponseEntity<byte[]> downloadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
public ResponseEntity<byte[]> downloadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable String relabelNo, @PathVariable long fileId) {
|
@PathVariable String relabelNo, @PathVariable long fileId) {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public final class RelabelResponses {
|
|||||||
String contactName, String contactPhone, String status, int itemCount,
|
String contactName, String contactPhone, String status, int itemCount,
|
||||||
int totalPackageCount, int notifiedPackageCount, String skuCodes,
|
int totalPackageCount, int notifiedPackageCount, String skuCodes,
|
||||||
String totalFileId, String totalFileName, String totalFileUrl,
|
String totalFileId, String totalFileName, String totalFileUrl,
|
||||||
|
String cartonTotalFileId, String cartonTotalFileName, String cartonTotalFileUrl,
|
||||||
|
String skuTotalFileId, String skuTotalFileName, String skuTotalFileUrl,
|
||||||
BigDecimal chargeAmount, String chargeCurrency, String walletStatus,
|
BigDecimal chargeAmount, String chargeCurrency, String walletStatus,
|
||||||
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
|
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
|
||||||
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
||||||
|
|||||||
+6
@@ -33,6 +33,12 @@ public class RelabelApplication {
|
|||||||
private String totalFileId;
|
private String totalFileId;
|
||||||
private String totalFileName;
|
private String totalFileName;
|
||||||
private String totalFileUrl;
|
private String totalFileUrl;
|
||||||
|
private String cartonTotalFileId;
|
||||||
|
private String cartonTotalFileName;
|
||||||
|
private String cartonTotalFileUrl;
|
||||||
|
private String skuTotalFileId;
|
||||||
|
private String skuTotalFileName;
|
||||||
|
private String skuTotalFileUrl;
|
||||||
private String status;
|
private String status;
|
||||||
private Integer itemCount;
|
private Integer itemCount;
|
||||||
private Integer totalPackageCount;
|
private Integer totalPackageCount;
|
||||||
|
|||||||
+13
-3
@@ -98,11 +98,19 @@ public class RelabelApplicationService implements WalletResultHandler {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public RelabelResponses.Detail uploadTotalFile(String customerId, String relabelNo, long operatorId, MultipartFile file) {
|
public RelabelResponses.Detail uploadTotalFile(String customerId, String relabelNo, long operatorId, MultipartFile file) {
|
||||||
|
return uploadTotalFile(customerId, relabelNo, operatorId, file, "TOTAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RelabelResponses.Detail uploadTotalFile(String customerId, String relabelNo, long operatorId, MultipartFile file, String kind) {
|
||||||
RelabelApplication application = lockForDocument(customerId, relabelNo);
|
RelabelApplication application = lockForDocument(customerId, relabelNo);
|
||||||
requirePdf(file);
|
requirePdf(file);
|
||||||
var uploaded = documentFiles.upload(application.getCustomerId(), "OVERSEAS_RELABEL_TOTAL_DOCUMENT", String.valueOf(application.getId()), operatorId, file);
|
String businessType = "SKU_TOTAL".equals(kind) ? "OVERSEAS_RELABEL_SKU_TOTAL_DOCUMENT" : "OVERSEAS_RELABEL_CARTON_TOTAL_DOCUMENT";
|
||||||
application.setTotalFileId(String.valueOf(uploaded.fileId())); application.setTotalFileName(uploaded.fileName());
|
var uploaded = documentFiles.upload(application.getCustomerId(), businessType, String.valueOf(application.getId()), operatorId, file);
|
||||||
application.setTotalFileUrl(documentUrl(relabelNo, uploaded.fileId(), null)); application.setUpdatedAt(LocalDateTime.now());
|
String fileId = String.valueOf(uploaded.fileId()); String url = documentUrl(relabelNo, uploaded.fileId(), null);
|
||||||
|
if ("SKU_TOTAL".equals(kind)) { application.setSkuTotalFileId(fileId); application.setSkuTotalFileName(uploaded.fileName()); application.setSkuTotalFileUrl(url); }
|
||||||
|
else { application.setCartonTotalFileId(fileId); application.setCartonTotalFileName(uploaded.fileName()); application.setCartonTotalFileUrl(url); }
|
||||||
|
application.setTotalFileId(fileId); application.setTotalFileName(uploaded.fileName()); application.setTotalFileUrl(url); application.setUpdatedAt(LocalDateTime.now());
|
||||||
applications.updateById(application);
|
applications.updateById(application);
|
||||||
return detail(application.getCustomerId(), relabelNo);
|
return detail(application.getCustomerId(), relabelNo);
|
||||||
}
|
}
|
||||||
@@ -708,6 +716,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
|||||||
Objects.requireNonNullElse(value.getTotalPackageCount(), 0),
|
Objects.requireNonNullElse(value.getTotalPackageCount(), 0),
|
||||||
Objects.requireNonNullElse(value.getNotifiedPackageCount(), 0), skuCodes,
|
Objects.requireNonNullElse(value.getNotifiedPackageCount(), 0), skuCodes,
|
||||||
value.getTotalFileId(), value.getTotalFileName(), value.getTotalFileUrl(),
|
value.getTotalFileId(), value.getTotalFileName(), value.getTotalFileUrl(),
|
||||||
|
value.getCartonTotalFileId(), value.getCartonTotalFileName(), value.getCartonTotalFileUrl(),
|
||||||
|
value.getSkuTotalFileId(), value.getSkuTotalFileName(), value.getSkuTotalFileUrl(),
|
||||||
value.getChargeAmount(), value.getChargeCurrency(), value.getWalletStatus(), value.getWalletErrorMessage(),
|
value.getChargeAmount(), value.getChargeCurrency(), value.getWalletStatus(), value.getWalletErrorMessage(),
|
||||||
value.getDeliveryMethod(), Boolean.TRUE.equals(value.getPrintShippingLabel()),
|
value.getDeliveryMethod(), Boolean.TRUE.equals(value.getPrintShippingLabel()),
|
||||||
value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(),
|
value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(),
|
||||||
|
|||||||
@@ -534,7 +534,10 @@ public class LastlegOrderService {
|
|||||||
String warehouse = StringUtils.hasText(query.warehouse()) ? query.warehouse().trim() : null;
|
String warehouse = StringUtils.hasText(query.warehouse()) ? query.warehouse().trim() : null;
|
||||||
return new LambdaQueryWrapper<LastlegOrder>()
|
return new LambdaQueryWrapper<LastlegOrder>()
|
||||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||||
.like(StringUtils.hasText(query.orderNo()), LastlegOrder::getOrderNo, query.orderNo())
|
.and(StringUtils.hasText(query.orderNo()), nested -> nested
|
||||||
|
.like(LastlegOrder::getOrderNo, query.orderNo())
|
||||||
|
.or()
|
||||||
|
.like(LastlegOrder::getSourceOrderNo, query.orderNo()))
|
||||||
.like(StringUtils.hasText(relabelNo), LastlegOrder::getRelabelOrderNo, relabelNo)
|
.like(StringUtils.hasText(relabelNo), LastlegOrder::getRelabelOrderNo, relabelNo)
|
||||||
.eq(StringUtils.hasText(query.orderType()), LastlegOrder::getOrderType, query.orderType())
|
.eq(StringUtils.hasText(query.orderType()), LastlegOrder::getOrderType, query.orderType())
|
||||||
.like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode())
|
.like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode())
|
||||||
@@ -590,7 +593,7 @@ public class LastlegOrderService {
|
|||||||
private LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations,
|
private LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations,
|
||||||
String containerNode, String dispatchStatus) {
|
String containerNode, String dispatchStatus) {
|
||||||
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
|
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
|
||||||
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(),
|
return new LastlegOrderSummary(o.getId(), displayOrderNo(o), o.getOrderType(),
|
||||||
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
|
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
|
||||||
displayInboundCode(o), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
|
displayInboundCode(o), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
|
||||||
o.getOverseasWarehouseCode(), o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
|
o.getOverseasWarehouseCode(), o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
|
||||||
@@ -606,6 +609,11 @@ public class LastlegOrderService {
|
|||||||
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
|
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String displayOrderNo(LastlegOrder order) {
|
||||||
|
return "WAREHOUSE_TRANSFER".equals(order.getOrderType()) && StringUtils.hasText(order.getSourceOrderNo())
|
||||||
|
? order.getSourceOrderNo() : order.getOrderNo();
|
||||||
|
}
|
||||||
|
|
||||||
private String containerNode(LastlegOrder order) {
|
private String containerNode(LastlegOrder order) {
|
||||||
if (!StringUtils.hasText(order.getSourceContainerBookingId())) return null;
|
if (!StringUtils.hasText(order.getSourceContainerBookingId())) return null;
|
||||||
ContainerShippingGate gate = containerShippingGates.selectById(order.getSourceContainerBookingId());
|
ContainerShippingGate gate = containerShippingGates.selectById(order.getSourceContainerBookingId());
|
||||||
|
|||||||
@@ -1530,6 +1530,8 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
|
|||||||
PREPARE transfer_estimated_ship_stmt FROM @ddl; EXECUTE transfer_estimated_ship_stmt; DEALLOCATE PREPARE transfer_estimated_ship_stmt;
|
PREPARE transfer_estimated_ship_stmt FROM @ddl; EXECUTE transfer_estimated_ship_stmt; DEALLOCATE PREPARE transfer_estimated_ship_stmt;
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='label_deadline'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN label_deadline DATETIME(3) NULL COMMENT ''换标截止时间'', ADD COLUMN relabel_contact_name VARCHAR(128) NULL COMMENT ''换标联系人'', ADD COLUMN relabel_contact_phone VARCHAR(64) NULL COMMENT ''换标联系电话'', ADD COLUMN relabel_charge_amount DECIMAL(20,2) NULL COMMENT ''换标费用CNY'', ADD COLUMN relabel_delivery_method VARCHAR(32) NULL COMMENT ''换标后派送方式'''));
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='label_deadline'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN label_deadline DATETIME(3) NULL COMMENT ''换标截止时间'', ADD COLUMN relabel_contact_name VARCHAR(128) NULL COMMENT ''换标联系人'', ADD COLUMN relabel_contact_phone VARCHAR(64) NULL COMMENT ''换标联系电话'', ADD COLUMN relabel_charge_amount DECIMAL(20,2) NULL COMMENT ''换标费用CNY'', ADD COLUMN relabel_delivery_method VARCHAR(32) NULL COMMENT ''换标后派送方式'''));
|
||||||
PREPARE transfer_relabel_config_stmt FROM @ddl; EXECUTE transfer_relabel_config_stmt; DEALLOCATE PREPARE transfer_relabel_config_stmt;
|
PREPARE transfer_relabel_config_stmt FROM @ddl; EXECUTE transfer_relabel_config_stmt; DEALLOCATE PREPARE transfer_relabel_config_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='relabel_completed_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN relabel_completed_at DATETIME(3) NULL COMMENT ''关联贴标完成时间'' AFTER relabel_no'));
|
||||||
|
PREPARE transfer_relabel_completed_stmt FROM @ddl; EXECUTE transfer_relabel_completed_stmt; DEALLOCATE PREPARE transfer_relabel_completed_stmt;
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='palletize_idempotency_key'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN palletize_idempotency_key VARCHAR(255) NULL COMMENT ''PDA打托幂等键'', ADD COLUMN palletize_request_hash CHAR(64) NULL COMMENT ''PDA打托请求哈希'', ADD COLUMN confirmation_idempotency_key VARCHAR(255) NULL COMMENT ''客户测量确认幂等键'', ADD COLUMN confirmation_request_hash CHAR(64) NULL COMMENT ''客户测量确认请求哈希'', ADD COLUMN customer_confirmed_at DATETIME(3) NULL COMMENT ''客户确认测量时间'''));
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='palletize_idempotency_key'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN palletize_idempotency_key VARCHAR(255) NULL COMMENT ''PDA打托幂等键'', ADD COLUMN palletize_request_hash CHAR(64) NULL COMMENT ''PDA打托请求哈希'', ADD COLUMN confirmation_idempotency_key VARCHAR(255) NULL COMMENT ''客户测量确认幂等键'', ADD COLUMN confirmation_request_hash CHAR(64) NULL COMMENT ''客户测量确认请求哈希'', ADD COLUMN customer_confirmed_at DATETIME(3) NULL COMMENT ''客户确认测量时间'''));
|
||||||
PREPARE transfer_fulfillment_stmt FROM @ddl; EXECUTE transfer_fulfillment_stmt; DEALLOCATE PREPARE transfer_fulfillment_stmt;
|
PREPARE transfer_fulfillment_stmt FROM @ddl; EXECUTE transfer_fulfillment_stmt; DEALLOCATE PREPARE transfer_fulfillment_stmt;
|
||||||
|
|
||||||
@@ -1920,7 +1922,9 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_item (
|
|||||||
id BIGINT NOT NULL COMMENT '主键ID', application_id BIGINT NOT NULL COMMENT '申请单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
id BIGINT NOT NULL COMMENT '主键ID', application_id BIGINT NOT NULL COMMENT '申请单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||||
order_id BIGINT NOT NULL COMMENT '订单ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓单号快照', profile_id BIGINT NOT NULL COMMENT '托盘档案ID',
|
order_id BIGINT NOT NULL COMMENT '订单ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓单号快照', profile_id BIGINT NOT NULL COMMENT '托盘档案ID',
|
||||||
sku_inventory_id BIGINT NOT NULL COMMENT 'SKU库存记录ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称',
|
sku_inventory_id BIGINT NOT NULL COMMENT 'SKU库存记录ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称',
|
||||||
available_packages_snapshot INT NOT NULL COMMENT '可用箱数快照', discard_packages INT NOT NULL COMMENT '废弃箱数',
|
available_packages_snapshot INT NOT NULL COMMENT '可用箱数快照', discard_packages INT NOT NULL COMMENT '申请废弃箱数',
|
||||||
|
actual_discard_packages INT NULL COMMENT 'PDA确认实际废弃箱数', pda_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/COMPLETED',
|
||||||
|
pda_operator_id VARCHAR(128) NULL COMMENT 'PDA操作人', pda_completed_at DATETIME(3) NULL COMMENT 'PDA完成时间',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id),
|
||||||
UNIQUE KEY uk_ll_cargo_discard_item(application_id,profile_id,sku_code),
|
UNIQUE KEY uk_ll_cargo_discard_item(application_id,profile_id,sku_code),
|
||||||
KEY idx_ll_cargo_discard_item_list(customer_id,application_id),
|
KEY idx_ll_cargo_discard_item_list(customer_id,application_id),
|
||||||
@@ -1943,6 +1947,20 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_status_history (
|
|||||||
KEY idx_ll_cargo_discard_history(customer_id,application_id,created_at)
|
KEY idx_ll_cargo_discard_history(customer_id,application_id,created_at)
|
||||||
) ENGINE=InnoDB COMMENT='货物废弃审批历史';
|
) ENGINE=InnoDB COMMENT='货物废弃审批历史';
|
||||||
|
|
||||||
|
-- Backfill applications created before cargo discard became a first-class tail-leg order.
|
||||||
|
INSERT IGNORE INTO ll_order (
|
||||||
|
id,order_no,customer_id,order_type,inbound_code,inbound_order_no,source_region,source_inbound_id,
|
||||||
|
source_order_id,source_order_no,overseas_warehouse_id,overseas_warehouse_name,package_count,
|
||||||
|
current_status,version,source_event_id,source_loading_plan_id,source_payload_hash,created_at,updated_at
|
||||||
|
)
|
||||||
|
SELECT a.id,a.discard_no,a.customer_id,'CARGO_DISCARD',a.inbound_codes,a.discard_no,'LASTLEG_DISCARD',
|
||||||
|
CONCAT('CARGO_DISCARD:',a.id),CAST(a.id AS CHAR),a.discard_no,a.overseas_warehouse_id,a.overseas_warehouse_name,
|
||||||
|
a.total_packages,a.status,0,CONCAT('CARGO_DISCARD:',a.id),'CARGO_DISCARD',a.request_hash,a.created_at,a.updated_at
|
||||||
|
FROM ll_cargo_discard_application a
|
||||||
|
LEFT JOIN ll_order o ON o.customer_id=a.customer_id AND o.source_region='LASTLEG_DISCARD'
|
||||||
|
AND o.source_inbound_id=CONCAT('CARGO_DISCARD:',a.id)
|
||||||
|
WHERE o.id IS NULL;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order (
|
CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order (
|
||||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||||
customer_name VARCHAR(255) NULL COMMENT '客户名称快照', order_no VARCHAR(64) NOT NULL COMMENT '订单号',
|
customer_name VARCHAR(255) NULL COMMENT '客户名称快照', order_no VARCHAR(64) NOT NULL COMMENT '订单号',
|
||||||
|
|||||||
+7
-7
@@ -14,6 +14,7 @@ import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
|||||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||||
|
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -48,6 +49,7 @@ class CargoDiscardServiceTest {
|
|||||||
@Mock CargoDiscardInventoryMapper inventory;
|
@Mock CargoDiscardInventoryMapper inventory;
|
||||||
@Mock CargoDiscardInventoryAdjustmentService adjustments;
|
@Mock CargoDiscardInventoryAdjustmentService adjustments;
|
||||||
@Mock LastlegOrderMapper orders;
|
@Mock LastlegOrderMapper orders;
|
||||||
|
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||||
|
|
||||||
private CargoDiscardService service;
|
private CargoDiscardService service;
|
||||||
private final AtomicReference<CargoDiscardApplication> storedApplication = new AtomicReference<>();
|
private final AtomicReference<CargoDiscardApplication> storedApplication = new AtomicReference<>();
|
||||||
@@ -66,6 +68,7 @@ class CargoDiscardServiceTest {
|
|||||||
ReflectionTestUtils.setField(service, "adjustments", adjustments);
|
ReflectionTestUtils.setField(service, "adjustments", adjustments);
|
||||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||||
ReflectionTestUtils.setField(service, "orders", orders);
|
ReflectionTestUtils.setField(service, "orders", orders);
|
||||||
|
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
|
||||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||||
ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00"));
|
ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00"));
|
||||||
lenient().when(applications.insert(any(CargoDiscardApplication.class))).thenAnswer(invocation -> { storedApplication.set(invocation.getArgument(0)); return 1; });
|
lenient().when(applications.insert(any(CargoDiscardApplication.class))).thenAnswer(invocation -> { storedApplication.set(invocation.getArgument(0)); return 1; });
|
||||||
@@ -100,7 +103,7 @@ class CargoDiscardServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void approvalDeductsSkuInventoryAndWritesOneLedgerPerCarton() {
|
void approvalPushesToPdaWithoutDeductingInventory() {
|
||||||
CargoDiscardApplication application = pendingApplication();
|
CargoDiscardApplication application = pendingApplication();
|
||||||
storedApplication.set(application);
|
storedApplication.set(application);
|
||||||
CargoDiscardItem item = item();
|
CargoDiscardItem item = item();
|
||||||
@@ -108,15 +111,12 @@ class CargoDiscardServiceTest {
|
|||||||
storedLinks.add(link(item.getId(), 1L));
|
storedLinks.add(link(item.getId(), 1L));
|
||||||
storedLinks.add(link(item.getId(), 2L));
|
storedLinks.add(link(item.getId(), 2L));
|
||||||
when(applications.lockNo("100", "FH-1")).thenReturn(application);
|
when(applications.lockNo("100", "FH-1")).thenReturn(application);
|
||||||
when(adjustments.discard(eq("100"), eq(application), any(), any(), eq("9"), any()))
|
|
||||||
.thenReturn(new CargoDiscardInventoryAdjustmentService.AdjustmentResult(3, 2, 1));
|
|
||||||
|
|
||||||
var detail = service.review("100", "FH-1", "9", new CargoDiscardRequests.Review("APPROVE", "ok"));
|
var detail = service.review("100", "FH-1", "9", new CargoDiscardRequests.Review("APPROVE", "ok"));
|
||||||
|
|
||||||
assertEquals(CargoDiscardService.APPROVED, detail.summary().status());
|
assertEquals(CargoDiscardService.APPROVED, detail.summary().status());
|
||||||
verify(adjustments).discard(eq("100"), eq(application), any(), any(), eq("9"), any());
|
verify(adjustments, never()).discard(any(), any(), any(), any(), any(), any());
|
||||||
assertEquals("ok; inventoryBefore=3, discarded=2, inventoryAfter=1",
|
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
|
||||||
storedHistories.get(storedHistories.size() - 1).getRemark());
|
assertEquals("ok", storedHistories.get(storedHistories.size() - 1).getRemark());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+9
@@ -412,6 +412,15 @@ class DispatchServiceTest {
|
|||||||
verify(locks, org.mockito.Mockito.times(1)).selectList(any());
|
verify(locks, org.mockito.Mockito.times(1)).selectList(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void receivedPalletUsesPhysicalPalletAsDispatchLockIdentity() {
|
||||||
|
DispatchPalletCandidate mergedInboundOrder = candidate(10L, 900L, "ONT8");
|
||||||
|
DispatchPalletCandidate plannedOnly = candidate(11L, null, "ONT8");
|
||||||
|
|
||||||
|
assertEquals("RECEIVED:900", mergedInboundOrder.sourcePalletKey());
|
||||||
|
assertEquals("PLAN:11", plannedOnly.sourcePalletKey());
|
||||||
|
}
|
||||||
|
|
||||||
private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) {
|
private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) {
|
||||||
return candidate(planPalletId, warehousePalletId, destination, 12);
|
return candidate(planPalletId, warehousePalletId, destination, 12);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,16 @@ class LastlegOrderServiceTest {
|
|||||||
@Mock ContainerShippingGateMapper containerShippingGates;
|
@Mock ContainerShippingGateMapper containerShippingGates;
|
||||||
LastlegOrderService service;
|
LastlegOrderService service;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesWarehouseTransferNumberAsTailOrderNumber() {
|
||||||
|
LastlegOrder order = new LastlegOrder();
|
||||||
|
order.setOrderNo("WT-WH-20260911-020226");
|
||||||
|
order.setOrderType("WAREHOUSE_TRANSFER");
|
||||||
|
order.setSourceOrderNo("WT-WH-20260911-020225");
|
||||||
|
|
||||||
|
assertEquals("WT-WH-20260911-020225", LastlegOrderService.displayOrderNo(order));
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
service = new LastlegOrderService();
|
service = new LastlegOrderService();
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
USE baiyue_lastleg;
|
||||||
|
|
||||||
|
ALTER TABLE ll_cargo_discard_item
|
||||||
|
ADD COLUMN actual_discard_packages INT NULL COMMENT 'PDA确认实际废弃箱数' AFTER discard_packages,
|
||||||
|
ADD COLUMN pda_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/COMPLETED' AFTER actual_discard_packages,
|
||||||
|
ADD COLUMN pda_operator_id VARCHAR(128) NULL COMMENT 'PDA操作人' AFTER pda_status,
|
||||||
|
ADD COLUMN pda_completed_at DATETIME(3) NULL COMMENT 'PDA完成时间' AFTER pda_operator_id;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO ll_order (
|
||||||
|
id,order_no,customer_id,order_type,inbound_code,inbound_order_no,source_region,source_inbound_id,
|
||||||
|
source_order_id,source_order_no,overseas_warehouse_id,overseas_warehouse_name,package_count,
|
||||||
|
current_status,version,source_event_id,source_loading_plan_id,source_payload_hash,created_at,updated_at
|
||||||
|
)
|
||||||
|
SELECT a.id,a.discard_no,a.customer_id,'CARGO_DISCARD',a.inbound_codes,a.discard_no,'LASTLEG_DISCARD',
|
||||||
|
CONCAT('CARGO_DISCARD:',a.id),CAST(a.id AS CHAR),a.discard_no,a.overseas_warehouse_id,a.overseas_warehouse_name,
|
||||||
|
a.total_packages,a.status,0,CONCAT('CARGO_DISCARD:',a.id),'CARGO_DISCARD',a.request_hash,a.created_at,a.updated_at
|
||||||
|
FROM ll_cargo_discard_application a
|
||||||
|
LEFT JOIN ll_order o ON o.customer_id=a.customer_id AND o.source_region='LASTLEG_DISCARD'
|
||||||
|
AND o.source_inbound_id=CONCAT('CARGO_DISCARD:',a.id)
|
||||||
|
WHERE o.id IS NULL;
|
||||||
@@ -3893,6 +3893,18 @@ PREPARE relabel_inventory_stmt FROM @ddl; EXECUTE relabel_inventory_stmt; DEALLO
|
|||||||
-- 2026-09-09 / 换标PDA完工、客户确认及出库分流
|
-- 2026-09-09 / 换标PDA完工、客户确认及出库分流
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='delivery_method'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN delivery_method VARCHAR(32) NULL COMMENT ''派送方式:TRUCK卡车、PICKUP自提、EXPRESS快递;仅涉及箱唛换标时必填'' AFTER contact_phone'));
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='delivery_method'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN delivery_method VARCHAR(32) NULL COMMENT ''派送方式:TRUCK卡车、PICKUP自提、EXPRESS快递;仅涉及箱唛换标时必填'' AFTER contact_phone'));
|
||||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_id VARCHAR(128) NULL COMMENT ''箱唛总文件ID'' AFTER total_file_url'));
|
||||||
|
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_name VARCHAR(255) NULL COMMENT ''箱唛总文件名'' AFTER carton_total_file_id'));
|
||||||
|
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_url VARCHAR(1000) NULL COMMENT ''箱唛总文件地址'' AFTER carton_total_file_name'));
|
||||||
|
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_id VARCHAR(128) NULL COMMENT ''SKU总文件ID'' AFTER carton_total_file_url'));
|
||||||
|
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_name VARCHAR(255) NULL COMMENT ''SKU总文件名'' AFTER sku_total_file_id'));
|
||||||
|
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_url VARCHAR(1000) NULL COMMENT ''SKU总文件地址'' AFTER sku_total_file_name'));
|
||||||
|
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='print_shipping_label'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN print_shipping_label TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否需要打印面单'' AFTER delivery_method'));
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='print_shipping_label'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN print_shipping_label TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否需要打印面单'' AFTER delivery_method'));
|
||||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='shipping_label_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN shipping_label_file_id VARCHAR(128) NULL COMMENT ''客户上传面单文件ID'' AFTER print_shipping_label'));
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='shipping_label_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN shipping_label_file_id VARCHAR(128) NULL COMMENT ''客户上传面单文件ID'' AFTER print_shipping_label'));
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
USE baiyue_lastleg;
|
||||||
|
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_id VARCHAR(128) NULL COMMENT ''箱唛总文件ID'' AFTER total_file_url'));
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_name VARCHAR(255) NULL COMMENT ''箱唛总文件名'' AFTER carton_total_file_id'));
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_url VARCHAR(1000) NULL COMMENT ''箱唛总文件地址'' AFTER carton_total_file_name'));
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_id VARCHAR(128) NULL COMMENT ''SKU总文件ID'' AFTER carton_total_file_url'));
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_name VARCHAR(255) NULL COMMENT ''SKU总文件名'' AFTER sku_total_file_id'));
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_url VARCHAR(1000) NULL COMMENT ''SKU总文件地址'' AFTER sku_total_file_name'));
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
Reference in New Issue
Block a user