提交
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
-- 对应数据库:baiyue_lastleg;可重复执行,不修改原箱号、SKU预报件数和实测数据。
|
||||
ALTER TABLE ll_warehouse_transfer_carton MODIFY COLUMN transfer_item_id BIGINT NULL COMMENT '实际箱内首项SKU明细ID,预报补箱可为空';
|
||||
+9
@@ -74,6 +74,15 @@ public class MerchantRelabelApplicationController {
|
||||
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}/files/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_LIST + "')")
|
||||
public ResponseEntity<byte[]> downloadFile(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @PathVariable long fileId, @RequestParam String scope,
|
||||
@RequestParam(required = false) Long itemId) {
|
||||
var file = service.downloadFile(customer(user), relabelNo, scope, itemId, fileId);
|
||||
return download(file.content(), file.fileName(), file.contentType());
|
||||
}
|
||||
@GetMapping("/{relabelNo}/shipping-label/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_LIST + "')")
|
||||
public ResponseEntity<byte[]> downloadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+11
@@ -134,6 +134,17 @@ public class RelabelApplicationController {
|
||||
return ResponseEntity.ok().cacheControl(org.springframework.http.CacheControl.noStore()).header("Content-Disposition", ContentDisposition.inline().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(org.springframework.http.MediaType.parseMediaType(file.contentType())).contentLength(file.content().length).body(file.content());
|
||||
}
|
||||
/** 平台按申请绑定关系读取客户上传的面单。 */
|
||||
@GetMapping("/{relabelNo}/shipping-label/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_LIST + "')")
|
||||
public ResponseEntity<byte[]> downloadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @PathVariable long fileId) {
|
||||
requirePlatform(user);
|
||||
var file = service.downloadShippingLabel(service.customerId(relabelNo), relabelNo, fileId);
|
||||
return ResponseEntity.ok().cacheControl(org.springframework.http.CacheControl.noStore())
|
||||
.header("Content-Disposition", ContentDisposition.inline().filename(file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(org.springframework.http.MediaType.parseMediaType(file.contentType())).contentLength(file.content().length).body(file.content());
|
||||
}
|
||||
@GetMapping("/{relabelNo}/completion-photos/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_LIST + "')")
|
||||
public ResponseEntity<byte[]> downloadCompletionPhoto(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+12
@@ -20,6 +20,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
public class WarehouseTransferController {
|
||||
public static final String MANAGE_PERMISSION = "lastleg:warehouse-transfer:manage";
|
||||
@Resource private WarehouseTransferService service;
|
||||
@Resource private com.baiyue.lastleg.transfer.service.WarehouseTransferForecastService forecasts;
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@@ -41,6 +42,17 @@ public class WarehouseTransferController {
|
||||
return ApiResponse.success(service.detail(scope(user, customerId), transferNo));
|
||||
}
|
||||
|
||||
/** 平台人员在测量完成前追加整单预报箱,沿用统一锁单及箱唛生成规则。 */
|
||||
@PostMapping("/{transferNo}/increase-forecast-cartons")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.WAREHOUSE_TRANSFER_APPROVE + "')")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> increaseForecastCartons(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String customerId, @PathVariable String transferNo,
|
||||
@Valid @RequestBody WarehouseTransferRequests.IncreaseForecastCartons request) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode()) || user.tenantId() == null || user.tenantId() != 0L)
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform identity is required");
|
||||
return ApiResponse.success(forecasts.increase(scope(user, customerId), transferNo, operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/{transferNo}/carton-label")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
public ApiResponse<WarehouseTransferResponses.Label> cartonLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+1
-3
@@ -105,11 +105,9 @@ public final class WarehouseTransferRequests {
|
||||
public record Outbound(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
/** 仅增加既有SKU的预报箱数,携带原箱数用于并发核对及重试去重。 */
|
||||
/** 仅增加整单预报箱数,箱内SKU由PDA清点确定,携带原箱数用于并发核对及重试去重。 */
|
||||
public record IncreaseForecastCartons(
|
||||
@Positive @Max(5000) int expectedPackageCount,
|
||||
@NotNull Long transferItemId,
|
||||
@Min(0) @Max(5000) int expectedItemPackageCount,
|
||||
@Positive @Max(5000) int packageCount
|
||||
) {}
|
||||
|
||||
|
||||
+5
-18
@@ -6,7 +6,6 @@ import com.baiyue.lastleg.transfer.entity.*;
|
||||
import com.baiyue.lastleg.transfer.mapper.*;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegInboundOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.mapper.*;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
@@ -20,11 +19,11 @@ import java.util.List;
|
||||
@Repository
|
||||
public class TransferForecastRepository {
|
||||
@Resource private WarehouseTransferOrderMapper transfers;
|
||||
@Resource private WarehouseTransferItemMapper items;
|
||||
|
||||
@Resource private WarehouseTransferCartonMapper cartons;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private LastlegInboundOrderMapper inboundOrders;
|
||||
@Resource private LastlegOrderDetailMapper orderDetails;
|
||||
|
||||
@Resource private LastlegOrderStatusHistoryMapper histories;
|
||||
@Resource private InboundPlanLineMapper planLines;
|
||||
|
||||
@@ -33,26 +32,19 @@ public class TransferForecastRepository {
|
||||
return transfers.lockNo(customerId, transferNo);
|
||||
}
|
||||
|
||||
/** 按创建顺序读取预报明细,保持关联尾程明细的序号稳定。 */
|
||||
public List<WarehouseTransferItem> items(String customerId, Long transferId) {
|
||||
return items.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
|
||||
.eq(WarehouseTransferItem::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferItem::getTransferOrderId, transferId).orderByAsc(WarehouseTransferItem::getId));
|
||||
}
|
||||
|
||||
/** 读取原箱号用于确定新增编号,不删除或重建原箱。 */
|
||||
public List<WarehouseTransferCarton> cartons(String customerId, Long transferId) {
|
||||
return cartons.listOrder(customerId, transferId);
|
||||
}
|
||||
|
||||
/** 追加预报箱并同步各预报口径;清点数据保留,整单清点确认失效。 */
|
||||
public void append(WarehouseTransferOrder transfer, WarehouseTransferItem item, int itemIndex,
|
||||
public void append(WarehouseTransferOrder transfer,
|
||||
List<WarehouseTransferCarton> added, LocalDateTime now, String operatorId) {
|
||||
String customerId = transfer.getCustomerId();
|
||||
var order = orders.selectOne(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getId, transfer.getLastlegOrderId()));
|
||||
if (order == null) throw new IllegalStateException("转运关联尾程订单不存在");
|
||||
items.updateById(item);
|
||||
|
||||
transfers.updateById(transfer);
|
||||
for (var carton : added) cartons.insert(carton);
|
||||
cartons.update(null, new LambdaUpdateWrapper<WarehouseTransferCarton>()
|
||||
@@ -65,18 +57,13 @@ public class TransferForecastRepository {
|
||||
inboundOrders.update(null, new LambdaUpdateWrapper<LastlegInboundOrder>()
|
||||
.eq(LastlegInboundOrder::getCustomerId, customerId).eq(LastlegInboundOrder::getId, transfer.getInboundOrderId())
|
||||
.set(LastlegInboundOrder::getPackageCount, transfer.getPackageCount()).set(LastlegInboundOrder::getUpdatedAt, now));
|
||||
int updated = orderDetails.update(null, new LambdaUpdateWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, customerId).eq(LastlegOrderDetail::getOrderId, transfer.getLastlegOrderId())
|
||||
.eq(LastlegOrderDetail::getSourceInboundDetailId, transfer.getTransferNo() + ":" + itemIndex)
|
||||
.set(LastlegOrderDetail::getPackageCount, item.getPackageCount()));
|
||||
if (updated != 1) throw new IllegalStateException("转运关联尾程货物明细不完整");
|
||||
planLines.update(null, new LambdaUpdateWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getCustomerId, customerId).eq(InboundPlanLine::getOrderId, transfer.getLastlegOrderId())
|
||||
.set(InboundPlanLine::getRequiredPackageCount, transfer.getPackageCount()));
|
||||
var history = new LastlegOrderStatusHistory(); history.setCustomerId(customerId); history.setOrderId(transfer.getLastlegOrderId());
|
||||
history.setFromStatus(order.getCurrentStatus()); history.setToStatus(order.getCurrentStatus());
|
||||
history.setEventId(transfer.getTransferNo()); history.setCreatedAt(now);
|
||||
history.setRemark("客户增加预报箱数,补充箱唛" + added.size() + "个;操作人:" + operatorId);
|
||||
history.setRemark("增加预报箱数,补充箱唛" + added.size() + "个;操作人:" + operatorId);
|
||||
histories.insert(history);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-12
@@ -29,17 +29,13 @@ public class WarehouseTransferForecastService {
|
||||
if (transfer == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "转运单不存在");
|
||||
if (!transferNo.equals(transfer.getInboundCode()) || !Set.of("PENDING_REVIEW", "WAIT_PDA_RECEIVING").contains(transfer.getStatus()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "仅测量完成前的外来转运单可增加预报箱数");
|
||||
var items = repository.items(customerId, transfer.getId());
|
||||
var item = items.stream().filter(row -> row.getId().equals(request.transferItemId())).findFirst()
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "SKU不属于当前转运单"));
|
||||
int addedCount = request.packageCount() - request.expectedItemPackageCount();
|
||||
long total = (long) request.expectedPackageCount() + addedCount;
|
||||
int addedCount = request.packageCount() - request.expectedPackageCount();
|
||||
int total = request.packageCount();
|
||||
if (addedCount <= 0 || total > 5000 || request.expectedPackageCount() <= 0)
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "只能增加预报箱数,整单最多5000箱");
|
||||
// 相同目标值视为已完成的重试;不同原快照则要求刷新,避免多端覆盖。
|
||||
if (item.getPackageCount() == request.packageCount() && transfer.getPackageCount() == total)
|
||||
return transfers.detail(customerId, transferNo);
|
||||
if (transfer.getPackageCount() != request.expectedPackageCount() || item.getPackageCount() != request.expectedItemPackageCount())
|
||||
// 相同目标值重试不再生成箱唛;不同原快照必须刷新,防止并发覆盖。
|
||||
if (transfer.getPackageCount() == total) return transfers.detail(customerId, transferNo);
|
||||
if (transfer.getPackageCount() != request.expectedPackageCount())
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "预报箱数已变化,请刷新后重试");
|
||||
var existing = repository.cartons(customerId, transfer.getId());
|
||||
int index = existing.stream().mapToInt(WarehouseTransferCarton::getCartonIndex).max().orElse(0);
|
||||
@@ -47,13 +43,14 @@ public class WarehouseTransferForecastService {
|
||||
var added = new ArrayList<WarehouseTransferCarton>();
|
||||
for (int i = 0; i < addedCount; i++) {
|
||||
var carton = new WarehouseTransferCarton(); carton.setId(IdWorker.getId()); carton.setCustomerId(customerId);
|
||||
carton.setTransferOrderId(transfer.getId()); carton.setTransferItemId(item.getId());
|
||||
carton.setTransferOrderId(transfer.getId());
|
||||
// 预报箱仅代表一个箱唛,不预先绑定SKU,实收箱内明细由PDA记录。
|
||||
carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId()));
|
||||
carton.setCartonIndex(++index); carton.setTotalCartons((int) total); carton.setStatus("EXPECTED");
|
||||
carton.setCreatedAt(now); carton.setUpdatedAt(now); added.add(carton);
|
||||
}
|
||||
item.setPackageCount(request.packageCount()); transfer.setPackageCount((int) total); transfer.setUpdatedAt(now);
|
||||
repository.append(transfer, item, items.indexOf(item), added, now, operatorId);
|
||||
transfer.setPackageCount(total); transfer.setUpdatedAt(now);
|
||||
repository.append(transfer, added, now, operatorId);
|
||||
return transfers.detail(customerId, transferNo);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-7
@@ -410,9 +410,13 @@ public class WarehouseTransferService {
|
||||
inboundOrders.updateById(inbound);
|
||||
}
|
||||
|
||||
// 草稿可能已打印箱唛,编辑时保留已有序号对应的编号,只追加或移除尾部箱子。
|
||||
Map<Integer, WarehouseTransferCarton> originalCartons = transferCartons.listOrder(customerId, transfer.getId())
|
||||
.stream().collect(Collectors.toMap(WarehouseTransferCarton::getCartonIndex, Function.identity()));
|
||||
transferCartons.delete(new LambdaQueryWrapper<WarehouseTransferCarton>()
|
||||
.eq(WarehouseTransferCarton::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferCarton::getTransferOrderId, transfer.getId()));
|
||||
.eq(WarehouseTransferCarton::getTransferOrderId, transfer.getId())
|
||||
.gt(WarehouseTransferCarton::getCartonIndex, request.packageCount()));
|
||||
transferItems.delete(new LambdaQueryWrapper<WarehouseTransferItem>()
|
||||
.eq(WarehouseTransferItem::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()));
|
||||
@@ -431,8 +435,16 @@ public class WarehouseTransferService {
|
||||
List<String> customerMarks = splitMarks(source.shippingMark());
|
||||
for (int itemCarton = 0; itemCarton < source.packageCount(); itemCarton++) {
|
||||
cartonIndex++;
|
||||
var existingCarton = originalCartons.get(cartonIndex);
|
||||
String customerMark = itemCarton < customerMarks.size() ? customerMarks.get(itemCarton) : null;
|
||||
if (existingCarton == null) {
|
||||
insertTransferCarton(customerId, transfer.getId(), itemId, transferNo, cartonIndex,
|
||||
request.packageCount(), itemCarton < customerMarks.size() ? customerMarks.get(itemCarton) : null, now);
|
||||
request.packageCount(), customerMark, now);
|
||||
} else {
|
||||
existingCarton.setTransferItemId(itemId); existingCarton.setTotalCartons(request.packageCount());
|
||||
existingCarton.setCustomerCartonMark(customerMark); existingCarton.setUpdatedAt(now);
|
||||
transferCartons.updateById(existingCarton);
|
||||
}
|
||||
}
|
||||
insertOrderDetail(customerId, order.getId(), transferNo, index, source, request, now);
|
||||
}
|
||||
@@ -698,12 +710,11 @@ public class WarehouseTransferService {
|
||||
return cartonLabel(transfer.getCustomerId(), transferNo);
|
||||
}
|
||||
|
||||
/** 提交预报后打印整单固定箱唛,测量及未收状态不改变箱号集合。 */
|
||||
/** 保存草稿后即可打印整单固定箱唛,测量及未收状态不改变箱号集合。 */
|
||||
@Transactional(readOnly = true)
|
||||
public WarehouseTransferResponses.Label cartonLabel(String customerId, String transferNo) {
|
||||
WarehouseTransferOrder transfer = transfers.findNo(customerId, transferNo);
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
if (DRAFT.equals(transfer.getStatus())) throw conflict("Submit the warehouse transfer order before printing carton labels");
|
||||
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId()).stream()
|
||||
.map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(), value.getCartonIndex(), value.getTotalCartons(), value.getStatus(), value.getTransferItemId(), value.getTransferPalletId(), value.getLengthCm(), value.getWidthCm(), value.getHeightCm(), value.getGrossWeightKg(), value.getCountedQuantity(), value.getDiscrepancyReason(), value.getCountedBy(), value.getCountedAt(), cartonContents(value))).toList();
|
||||
StringBuilder labels = new StringBuilder();
|
||||
@@ -930,8 +941,6 @@ public class WarehouseTransferService {
|
||||
var carton = byMark.get(input.cartonMark());
|
||||
if (carton == null || !seen.add(input.cartonMark()))
|
||||
throw badRequest("箱号不属于本单或重复填写");
|
||||
var item = byItem.get(carton.getTransferItemId());
|
||||
if (item == null) throw badRequest("箱子申报明细不属于本单");
|
||||
if (input.items() == null || input.items().isEmpty() || input.items().size() > 200)
|
||||
throw badRequest("每箱必须填写 1 至 200 行 SKU");
|
||||
Set<String> skuCodes = new java.util.HashSet<>();
|
||||
@@ -941,7 +950,7 @@ public class WarehouseTransferService {
|
||||
|| !skuCodes.add(line.skuCode())) throw badRequest("箱内 SKU 必须来自本单预报,同箱不能重复");
|
||||
if (line.quantity() <= 0 || line.quantity() > Integer.MAX_VALUE) throw badRequest("每行 SKU 必须填写有效实点件数");
|
||||
}
|
||||
if (!skuCodes.contains(item.getSkuCode())) throw badRequest("请保留箱子原申报 SKU,可添加其他本单 SKU");
|
||||
// 箱内实际SKU以PDA清点为准,允许混箱或调整,仍须属于当前单预报。
|
||||
if (input.length() == null || input.width() == null
|
||||
|| input.height() == null || input.weight() == null || input.length().signum() <= 0 || input.width().signum() <= 0
|
||||
|| input.height().signum() <= 0 || input.weight().signum() <= 0) throw badRequest("每箱必须填写有效尺寸、重量和实点件数");
|
||||
@@ -963,6 +972,7 @@ public class WarehouseTransferService {
|
||||
var declared = byItem.values().stream().filter(value -> value.getSkuCode().equals(line.skuCode())).findFirst().orElseThrow();
|
||||
return new WarehouseTransferResponses.CartonItem(declared.getId(), line.skuCode(), line.quantity());
|
||||
}).toList();
|
||||
carton.setTransferItemId(contents.get(0).transferItemId());
|
||||
try { carton.setSkuItemsJson(objectMapper.writeValueAsString(contents)); }
|
||||
catch (com.fasterxml.jackson.core.JsonProcessingException e) { throw new IllegalStateException("Cannot serialize carton contents", e); }
|
||||
carton.setCountedBy(operatorId); carton.setCountedAt(now); carton.setStatus("COUNTED");
|
||||
|
||||
@@ -2282,7 +2282,7 @@ CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_carton (
|
||||
transfer_pallet_id BIGINT NULL COMMENT '签收箱所属转运托盘事实ID',
|
||||
sku_items_json JSON NULL COMMENT '实收箱内SKU及逐项件数明细',
|
||||
id BIGINT NOT NULL COMMENT '预报箱主键', transfer_order_id BIGINT NOT NULL COMMENT '转运单ID',
|
||||
transfer_item_id BIGINT NOT NULL COMMENT '转运申报明细ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
transfer_item_id BIGINT NULL COMMENT '实际箱内首项SKU明细ID,预报补箱可为空', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
customer_carton_mark VARCHAR(255) NULL COMMENT '客户原箱唛,用于换标现场绑定',
|
||||
carton_mark VARCHAR(255) NOT NULL COMMENT '系统生成的一箱一码箱唛', carton_index INT NOT NULL COMMENT '全单箱序号',
|
||||
total_cartons INT NOT NULL COMMENT '首次生成时全单总箱数', status VARCHAR(32) NOT NULL DEFAULT 'EXPECTED' COMMENT '箱状态:EXPECTED待实收',
|
||||
|
||||
@@ -7,7 +7,7 @@ paths:
|
||||
/api/merchant/v1/last-leg/warehouse-transfers/{transferNo}/increase-forecast-cartons:
|
||||
post:
|
||||
summary: 测量完成前增加预报箱数并补充箱唛
|
||||
description: 客户身份及transfer:create权限;仅外来货物待审核或待PDA测量可用,按原预报快照追加既有SKU的箱数,原箱唛与件数不变。补箱及PDA完工锁定同一订单;成功后PDA刷新继续清点,无新增表字段。
|
||||
description: 客户身份及transfer:create权限;仅外来货物待审核或待PDA测量可用,按原预报快照追加整单箱数,不绑定SKU,实际箱内SKU由PDA清点确定,原箱唛与件数不变。补箱及PDA完工锁定同一订单;成功后PDA刷新继续清点。
|
||||
parameters:
|
||||
- {name: transferNo, in: path, required: true, schema: {type: string}}
|
||||
requestBody:
|
||||
@@ -16,14 +16,33 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [expectedPackageCount, transferItemId, expectedItemPackageCount, packageCount]
|
||||
required: [expectedPackageCount, packageCount]
|
||||
properties:
|
||||
expectedPackageCount: {type: integer, minimum: 1, maximum: 5000, description: 修改前整单预报箱数}
|
||||
transferItemId: {type: integer, format: int64, description: 当前单既有SKU明细}
|
||||
expectedItemPackageCount: {type: integer, minimum: 0, maximum: 5000, description: 修改前该SKU预报箱数}
|
||||
packageCount: {type: integer, minimum: 1, maximum: 5000, description: 增加后该SKU预报箱数}
|
||||
packageCount: {type: integer, minimum: 1, maximum: 5000, description: 增加后整单预报箱数}
|
||||
responses:
|
||||
'200': {description: 返回补箱后的转运详情,相同原快照和目标箱数重试不重复生成箱唛}
|
||||
/api/platform/v1/last-leg/warehouse-transfers/{transferNo}/increase-forecast-cartons:
|
||||
post:
|
||||
summary: 平台在测量完成前增加整单预报箱数
|
||||
description: 平台身份及 platform:overseas-warehouse:warehouse-transfer:approve 权限;指定目标客户,沿用客户补箱的状态、并发及幂等规则,不改变SKU预报件数。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: transferNo, in: path, required: true, schema: { type: string } }
|
||||
- { name: customerId, in: query, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [expectedPackageCount, packageCount]
|
||||
properties:
|
||||
expectedPackageCount: { type: integer, minimum: 1, maximum: 5000 }
|
||||
packageCount: { type: integer, minimum: 1, maximum: 5000 }
|
||||
responses:
|
||||
'200': { description: 补箱后的转运详情 }
|
||||
'409': { description: 已完成测量或原预报箱数过期 }
|
||||
/api/pda/v1/last-leg/warehouse-transfers/{transferNo}/carton-label:
|
||||
get:
|
||||
summary: PDA读取转运箱标签
|
||||
@@ -1796,6 +1815,31 @@ paths:
|
||||
- { name: itemId, in: query, required: false, schema: { type: integer, format: int64 } }
|
||||
requestBody: { required: true, content: { multipart/form-data: { schema: { type: object, required: [file], properties: { file: { type: string, format: binary } } } } } }
|
||||
responses: { '200': { description: 更新后的换标详情 }, '403': { description: 缺少换标文件维护权限 }, '409': { description: 状态或归属冲突 } }
|
||||
/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/files/{fileId}/download:
|
||||
get:
|
||||
summary: 客户查看或下载申请已绑定的贴标文件
|
||||
description: 权限 merchant:overseas-warehouse:relabel:list;按登录客户隔离,并校验总文件或明细文件归属。响应禁止缓存。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: relabelNo, in: path, required: true, schema: { type: string } }
|
||||
- { name: fileId, in: path, required: true, schema: { type: integer, format: int64, minimum: 1 } }
|
||||
- { name: scope, in: query, required: true, schema: { type: string, enum: [TOTAL, CARTON_TOTAL, SKU_TOTAL, ITEM] } }
|
||||
- { name: itemId, in: query, required: false, schema: { type: integer, format: int64 } }
|
||||
responses:
|
||||
'200': { description: 贴标 PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
|
||||
'403': { description: 缺少客户查询权限 }
|
||||
'404': { description: 申请或文件不存在或不属于当前客户 }
|
||||
/api/platform/v1/last-leg/relabel-applications/{relabelNo}/shipping-label/{fileId}/download:
|
||||
get:
|
||||
summary: 平台查看客户上传的换标面单
|
||||
description: 权限 platform:overseas-warehouse:relabel-order:list;校验平台身份及申请的面单绑定关系。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: relabelNo, in: path, required: true, schema: { type: string } }
|
||||
- { name: fileId, in: path, required: true, schema: { type: integer, format: int64, minimum: 1 } }
|
||||
responses:
|
||||
'200': { description: 面单文件, content: { application/pdf: { schema: { type: string, format: binary } }, image/jpeg: { schema: { type: string, format: binary } }, image/png: { schema: { type: string, format: binary } } } }
|
||||
'404': { description: 文件未绑定当前申请 }
|
||||
/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/shipping-label/{fileId}/download:
|
||||
get:
|
||||
summary: 下载换标快递面单
|
||||
|
||||
+14
@@ -34,6 +34,20 @@ class MerchantRelabelApplicationControllerTest {
|
||||
() -> controller.page(user(0L, "PLATFORM"), query));
|
||||
}
|
||||
|
||||
/** 下载必须使用登录客户,且响应禁止缓存。 */
|
||||
@Test
|
||||
void documentDownloadUsesAuthenticatedCustomerAndRejectsPlatform() {
|
||||
org.mockito.Mockito.when(service.downloadFile("100", "RL001", "SKU_TOTAL", null, 99L))
|
||||
.thenReturn(new com.baiyue.lastleg.relabel.gateway.RelabelDocumentFileGateway.DownloadedFile(
|
||||
"%PDF-1.7".getBytes(), "SKU标签.pdf", "application/pdf"));
|
||||
var result = controller.downloadFile(user(100L, "MERCHANT"), "RL001", 99L, "SKU_TOTAL", null);
|
||||
verify(service).downloadFile("100", "RL001", "SKU_TOTAL", null, 99L);
|
||||
org.junit.jupiter.api.Assertions.assertEquals("no-store", result.getHeaders().getCacheControl());
|
||||
org.junit.jupiter.api.Assertions.assertEquals("application/pdf", result.getHeaders().getContentType().toString());
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> controller.downloadFile(user(0L, "PLATFORM"), "RL001", 99L, "SKU_TOTAL", null));
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long tenantId, String appCode) {
|
||||
return new AuthenticatedUser(1L, tenantId, tenantId, 9L, "merchant", appCode, "WEB", Set.of());
|
||||
}
|
||||
|
||||
+12
@@ -61,6 +61,18 @@ class RelabelApplicationControllerTest {
|
||||
org.junit.jupiter.api.Assertions.assertEquals("application/pdf", response.getHeaders().getContentType().toString());
|
||||
}
|
||||
|
||||
/** 平台面单下载按申请解析客户,避免任意文件读取。 */
|
||||
@Test
|
||||
void shippingLabelDownloadUsesApplicationCustomer() {
|
||||
org.mockito.Mockito.when(service.customerId("RL-1")).thenReturn("101");
|
||||
org.mockito.Mockito.when(service.downloadShippingLabel("101", "RL-1", 88L))
|
||||
.thenReturn(new com.baiyue.lastleg.relabel.gateway.RelabelDocumentFileGateway.DownloadedFile(
|
||||
"%PDF-1.7".getBytes(), "shipping.pdf", "application/pdf"));
|
||||
var response = controller.downloadShippingLabel(platform(), "RL-1", 88L);
|
||||
verify(service).downloadShippingLabel("101", "RL-1", 88L);
|
||||
org.junit.jupiter.api.Assertions.assertEquals("no-store", response.getHeaders().getCacheControl());
|
||||
}
|
||||
|
||||
private RelabelRequests.Create request() {
|
||||
return new RelabelRequests.Create("CUSTOMER-1", "WH-1",
|
||||
LocalDateTime.of(2026, 8, 20, 10, 0), "Li", "13800000000", null, null, "key-1",
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ class MerchantWarehouseTransferControllerTest {
|
||||
/** 补预报只使用登录客户身份,平台身份不可越权操作。 */
|
||||
@Test
|
||||
void forecastIncreaseUsesAuthenticatedCustomerAndRejectsPlatform() {
|
||||
var request = new WarehouseTransferRequests.IncreaseForecastCartons(2,9L,2,3);
|
||||
var request = new WarehouseTransferRequests.IncreaseForecastCartons(2,3);
|
||||
controller.increaseForecastCartons(user(100L,"MERCHANT"),"TR001",request);
|
||||
verify(forecastService).increase("100","TR001","1",request);
|
||||
assertThrows(ResponseStatusException.class,()->controller.increaseForecastCartons(user(0L,"PLATFORM"),"TR001",request));
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.transfer.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import com.baiyue.lastleg.transfer.service.WarehouseTransferForecastService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.Set;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class WarehouseTransferForecastControllerTest {
|
||||
/** 平台补箱明确使用选中客户和操作者,拒绝客户身份调用平台入口。 */
|
||||
@Test void platformIncreaseRequiresPlatformAndExplicitCustomer() {
|
||||
var service = mock(WarehouseTransferForecastService.class);
|
||||
var controller = new WarehouseTransferController();
|
||||
ReflectionTestUtils.setField(controller, "forecasts", service);
|
||||
var platform = new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB", Set.of());
|
||||
var customer = new AuthenticatedUser(8L, 101L, 101L, 9L, "customer", "MERCHANT", "WEB", Set.of());
|
||||
var request = new WarehouseTransferRequests.IncreaseForecastCartons(4, 6);
|
||||
controller.increaseForecastCartons(platform, "101", "WT-1", request);
|
||||
verify(service).increase("101", "WT-1", "7", request);
|
||||
assertThrows(ResponseStatusException.class, () -> controller.increaseForecastCartons(customer, "101", "WT-1", request));
|
||||
assertThrows(ResponseStatusException.class, () -> controller.increaseForecastCartons(platform, "", "WT-1", request));
|
||||
verifyNoMoreInteractions(service);
|
||||
}
|
||||
}
|
||||
+9
@@ -55,6 +55,15 @@ class WarehouseTransferCartonCountTest {
|
||||
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
|
||||
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request("K1",input("BOX1","A",13),input("BOX2","B",7))));
|
||||
}
|
||||
/** 补充预报箱可无SKU归属,箱内装货按PDA实点记录。 */
|
||||
@Test void acceptsUnassignedForecastAndDifferentOriginalSku() {
|
||||
a.setTransferItemId(null);
|
||||
service.countCartons("OP1",77L,"WT1",request("UNBOUND",input("BOX1","B",3),input("BOX2","A",4)));
|
||||
assertNotNull(a.getTransferItemId());
|
||||
assertTrue(a.getSkuItemsJson().contains("B"));
|
||||
assertTrue(b.getSkuItemsJson().contains("A"));
|
||||
}
|
||||
|
||||
/** 整托汇总后再保留三位,不能把每箱先舍入后累加。 */
|
||||
@Test void volumeRoundsOnceAfterSummingBoxes() {
|
||||
var first = new WarehouseTransferRequests.CountCarton("BOX1", null, 200L,
|
||||
|
||||
+8
-8
@@ -26,31 +26,31 @@ class WarehouseTransferForecastServiceTest {
|
||||
order = new WarehouseTransferOrder(); order.setId(1L); order.setCustomerId("C"); order.setTransferNo("WT1"); order.setInboundCode("WT1"); order.setStatus("WAIT_PDA_RECEIVING"); order.setPackageCount(2);
|
||||
item = new WarehouseTransferItem(); item.setId(2L); item.setPackageCount(2); item.setDeclaredQuantity(10L);
|
||||
carton = new WarehouseTransferCarton(); carton.setCartonIndex(2); carton.setCartonMark("ORIGINAL"); carton.setStatus("COUNTED");
|
||||
when(repository.lock("C","WT1")).thenReturn(order); when(repository.items("C",1L)).thenReturn(List.of(item));
|
||||
when(repository.lock("C","WT1")).thenReturn(order);
|
||||
}
|
||||
|
||||
/** 补箱追加编号,预报件数与原箱号保持不变,相同快照重放不重复增加。 */
|
||||
@Test void appendPreservesMarksAndQuantityAndReplays() {
|
||||
when(repository.cartons("C",1L)).thenReturn(List.of(carton));
|
||||
var request = new WarehouseTransferRequests.IncreaseForecastCartons(2,2L,2,4);
|
||||
var request = new WarehouseTransferRequests.IncreaseForecastCartons(2,4);
|
||||
service.increase("C","WT1","OP",request);
|
||||
var added = org.mockito.ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).append(eq(order),eq(item),eq(0),added.capture(),any(),eq("OP"));
|
||||
verify(repository).append(eq(order),added.capture(),any(),eq("OP"));
|
||||
var rows = (List<WarehouseTransferCarton>) added.getValue();
|
||||
assertEquals(2,rows.size()); assertEquals(3,rows.get(0).getCartonIndex()); assertEquals(4,rows.get(1).getCartonIndex());
|
||||
assertNotEquals(rows.get(0).getCartonMark(),rows.get(1).getCartonMark()); assertEquals("ORIGINAL",carton.getCartonMark());
|
||||
assertEquals(10L,item.getDeclaredQuantity()); assertEquals(4,order.getPackageCount());
|
||||
assertNull(rows.get(0).getTransferItemId()); assertEquals(2,item.getPackageCount()); assertEquals(10L,item.getDeclaredQuantity()); assertEquals(4,order.getPackageCount());
|
||||
clearInvocations(repository); service.increase("C","WT1","OP",request);
|
||||
verify(repository,never()).append(any(),any(),anyInt(),anyList(),any(),anyString());
|
||||
verify(repository,never()).append(any(),anyList(),any(),anyString());
|
||||
}
|
||||
|
||||
/** 不允许完工后、跨单SKU、旧快照或减少箱数。 */
|
||||
@Test void rejectsCompletedForeignStaleAndDecreasing() {
|
||||
order.setStatus("WAIT_CUSTOMER_CONFIRMATION");
|
||||
assertThrows(ResponseStatusException.class,()->service.increase("C","WT1","OP",new WarehouseTransferRequests.IncreaseForecastCartons(2,2L,2,3)));
|
||||
assertThrows(ResponseStatusException.class,()->service.increase("C","WT1","OP",new WarehouseTransferRequests.IncreaseForecastCartons(2,3)));
|
||||
order.setStatus("WAIT_PDA_RECEIVING");
|
||||
for(var request:List.of(new WarehouseTransferRequests.IncreaseForecastCartons(2,99L,2,3),new WarehouseTransferRequests.IncreaseForecastCartons(1,2L,2,3),new WarehouseTransferRequests.IncreaseForecastCartons(2,2L,2,1)))
|
||||
for(var request:List.of(new WarehouseTransferRequests.IncreaseForecastCartons(1,3),new WarehouseTransferRequests.IncreaseForecastCartons(2,1)))
|
||||
assertThrows(ResponseStatusException.class,()->service.increase("C","WT1","OP",request));
|
||||
verify(repository,never()).append(any(),any(),anyInt(),anyList(),any(),anyString());
|
||||
verify(repository,never()).append(any(),anyList(),any(),anyString());
|
||||
}
|
||||
}
|
||||
|
||||
+30
-9
@@ -767,6 +767,29 @@ class WarehouseTransferServiceTest {
|
||||
verify(orderDetails).insert(any(LastlegOrderDetail.class));
|
||||
}
|
||||
|
||||
/** 已打印草稿再次保存时保留箱唛,并保存SKU预报件数。 */
|
||||
@Test
|
||||
void draftUpdatePreservesPrintedCartonMarksAndDeclaredQuantity() throws Exception {
|
||||
var transfer = persistedTransfer(); transfer.setStatus(WarehouseTransferService.DRAFT);
|
||||
var tailOrder = historicalOrder(); tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setCurrentStatus(WarehouseTransferService.DRAFT);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder);
|
||||
var box = new WarehouseTransferCarton(); box.setId(88L); box.setCartonIndex(1); box.setCartonMark("BX-KEEP"); box.setTotalCartons(5); box.setStatus("EXPECTED");
|
||||
when(transferCartons.listOrder("TENANT-A", transfer.getId())).thenReturn(List.of(box));
|
||||
var jsonMapper = new ObjectMapper().findAndRegisterModules();
|
||||
com.fasterxml.jackson.databind.node.ObjectNode requestJson = jsonMapper.valueToTree(request(false));
|
||||
((com.fasterxml.jackson.databind.node.ObjectNode) requestJson.get("items").get(0)).put("quantity", 120L);
|
||||
var withQuantity = jsonMapper.treeToValue(requestJson, WarehouseTransferRequests.Create.class);
|
||||
var result = service.updateDraft("TENANT-A", "WTR-1", "USER-1", withQuantity);
|
||||
var savedItem = ArgumentCaptor.forClass(WarehouseTransferItem.class);
|
||||
verify(transferItems).insert(savedItem.capture());
|
||||
assertEquals(120L, savedItem.getValue().getDeclaredQuantity());
|
||||
assertEquals("BX-KEEP", box.getCartonMark());
|
||||
verify(transferCartons).updateById(box);
|
||||
verify(transferCartons, times(4)).insert(any(WarehouseTransferCarton.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantCannotUpdateSubmittedTransfer() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
@@ -790,17 +813,15 @@ class WarehouseTransferServiceTest {
|
||||
verify(transfers, never()).updateById(any(WarehouseTransferOrder.class));
|
||||
}
|
||||
|
||||
/** 草稿预先打印与后续正式提交使用同一服务端箱唛。 */
|
||||
@Test
|
||||
void draftCannotPrintCartonLabelsBeforeSubmission() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
transfer.setStatus(WarehouseTransferService.DRAFT);
|
||||
void draftCanPrintPersistedCartonLabels() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer(); transfer.setStatus(WarehouseTransferService.DRAFT);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.cartonLabel("TENANT-A", "WTR-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(transferCartons, never()).listOrder(anyString(), anyLong());
|
||||
var carton = new WarehouseTransferCarton(); carton.setCartonMark("BX-PRESERVED"); carton.setCartonIndex(1); carton.setTotalCartons(1); carton.setStatus("EXPECTED");
|
||||
when(transferCartons.listOrder("TENANT-A", transfer.getId())).thenReturn(List.of(carton));
|
||||
var result = service.cartonLabel("TENANT-A", "WTR-1");
|
||||
assertTrue(result.printableHtml().contains("BX-PRESERVED"));
|
||||
}
|
||||
|
||||
/** 提交预报后即可打印全部预报箱唛,不依赖测量或照片完成。 */
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 对应数据库:baiyue_lastleg;可重复执行,不修改原箱号、SKU预报件数和实测数据。
|
||||
USE baiyue_lastleg;
|
||||
ALTER TABLE ll_warehouse_transfer_carton MODIFY COLUMN transfer_item_id BIGINT NULL COMMENT '实际箱内首项SKU明细ID,预报补箱可为空';
|
||||
Reference in New Issue
Block a user