fix(lastleg): route all relabel pickups directly to outbound and verify final schemas
This commit is contained in:
+2
-2
@@ -244,7 +244,7 @@ public interface DispatchCandidateMapper {
|
||||
"LEFT JOIN ll_warehouse_transfer_order wto ON wto.relabel_no=a.relabel_no AND wto.customer_id=a.customer_id " +
|
||||
"LEFT JOIN ll_warehouse_transfer_pallet wtp ON wtp.transfer_order_id=wto.id AND wtp.customer_id=a.customer_id " +
|
||||
"AND wtp.warehouse_pallet_id=pos.pallet_id " +
|
||||
"WHERE a.delivery_method!='EXPRESS' AND a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' " +
|
||||
"WHERE a.delivery_method='TRUCK' AND a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND a.customer_id=#{customerId} </if>" +
|
||||
"<if test='relabelNo != null and relabelNo != \"\"'>AND a.relabel_no=#{relabelNo} </if>" +
|
||||
"<if test='orderNo != null and orderNo != \"\"'>AND (a.relabel_no LIKE CONCAT('%',#{orderNo},'%') OR ri.source_inbound_code LIKE CONCAT('%',#{orderNo},'%') OR ri.source_inbound_order_no LIKE CONCAT('%',#{orderNo},'%')) </if>" +
|
||||
@@ -271,7 +271,7 @@ public interface DispatchCandidateMapper {
|
||||
"FROM ll_relabel_application a JOIN ll_relabel_item ri ON ri.application_id=a.id AND ri.customer_id=a.customer_id " +
|
||||
"JOIN ll_return_inventory_carton c ON c.id=ri.return_carton_id AND c.customer_id=a.customer_id " +
|
||||
"AND c.relabel_application_id=a.id AND c.overseas_warehouse_id=a.overseas_warehouse_id " +
|
||||
"WHERE a.delivery_method!='EXPRESS' AND a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' " +
|
||||
"WHERE a.delivery_method='TRUCK' AND a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' " +
|
||||
"AND c.status='RELABEL_PENDING' AND c.pallet_code IS NOT NULL " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND a.customer_id=#{customerId} </if>" +
|
||||
"<if test='relabelNo != null and relabelNo != \"\"'>AND a.relabel_no=#{relabelNo} </if>" +
|
||||
|
||||
+5
-4
@@ -116,8 +116,8 @@ public class DispatchService {
|
||||
if (!RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标尚未完成或客户尚未确认,无法进行排车");
|
||||
}
|
||||
if ("EXPRESS".equals(application.getDeliveryMethod())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "该换标单为快递派送,无需进行排车");
|
||||
if (!"TRUCK".equals(application.getDeliveryMethod())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "只有平台卡车派送的换标单可以排车");
|
||||
}
|
||||
return application.getCustomerId();
|
||||
}
|
||||
@@ -607,10 +607,11 @@ public class DispatchService {
|
||||
if (application == null || !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标尚未完成或客户尚未确认,无法进行排车");
|
||||
}
|
||||
if (!"TRUCK".equals(application.getDeliveryMethod())
|
||||
|| !"WAIT_PRE_DISPATCH".equals(application.getFulfillmentStatus()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标单不是待排车状态");
|
||||
relabelBindingGuard.requireBound(application);
|
||||
relabelMeasurements.requireMeasured(application);
|
||||
if ("EXPRESS".equals(application.getDeliveryMethod()) || !"WAIT_PRE_DISPATCH".equals(application.getFulfillmentStatus()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标单不是待排车状态");
|
||||
List<DispatchPalletCandidate> selected = java.util.stream.Stream.concat(
|
||||
measuredRelabelCandidates(candidates.relabelAvailable(customerId, null, request.relabelNo())).stream(),
|
||||
returnRelabelStock.candidates(customerId, null, request.relabelNo()).stream()).toList();
|
||||
|
||||
+10
-2
@@ -512,7 +512,7 @@ public class PdaRelabelService {
|
||||
updateTask(task, request.expectedVersion());
|
||||
insertHistory(task, "COMPLETE", before, WAIT_REVIEW, operatorKey, operatorName,
|
||||
Map.of("photoFileIds", photoIds, "remark", Objects.toString(trim(request.remark()), ""),
|
||||
"manualCompletion", request.manualCompletion()));
|
||||
"manualCompletion", request.manualCompletion(), "roundKey", mixedRound(task)));
|
||||
applicationService.changeStatus(application.getCustomerId(), relabelNo, operatorKey,
|
||||
new RelabelRequests.ChangeStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, request.remark()));
|
||||
applicationService.settlePdaCompletion(application.getCustomerId(), relabelNo, operatorKey);
|
||||
@@ -816,7 +816,15 @@ public class PdaRelabelService {
|
||||
task == null ? null : task.getCompletionRemark(), task == null ? null : task.getReviewDecision(),
|
||||
task == null ? null : task.getReviewRemark(), task == null ? null : task.getClaimedAt(),
|
||||
task == null ? null : task.getSubmittedAt(), task == null ? null : task.getReviewedAt(),
|
||||
itemResponses, factResponses, historyResponses, application.getDeliveryMethod(), !"EXPRESS".equals(application.getDeliveryMethod()));
|
||||
itemResponses, factResponses, historyResponses, application.getDeliveryMethod(),
|
||||
detailItems.stream().anyMatch(item -> {
|
||||
String operation = operationType(item);
|
||||
if (item.getReturnCartonId() == null)
|
||||
return Set.of("CARTON_LABEL", "CARTON_AND_SKU", "RETURN_BULK_SKU_LABEL").contains(operation);
|
||||
if ("SKU_LABEL".equals(operation) && application.getDeliveryMethod() == null) return false;
|
||||
return !"EXPRESS".equals(application.getDeliveryMethod()) && !"PICKUP".equals(application.getDeliveryMethod())
|
||||
&& !"AMAZON_PICKUP".equals(application.getDeliveryMethod()) && !"CUSTOMER_PICKUP".equals(application.getDeliveryMethod());
|
||||
}));
|
||||
}
|
||||
|
||||
private String effectiveTaskStatus(RelabelApplication application, PdaRelabelTask task) {
|
||||
|
||||
+15
-5
@@ -94,6 +94,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.ReturnSkuConversionService skuConversions;
|
||||
@Resource private RelabelBindingGuard bindingGuard;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper skuConversionRows;
|
||||
@Resource private ReturnOrderMapper returnOrders;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper returnPackingTraces;
|
||||
@@ -368,7 +369,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|| request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
String deliveryMethod = trim(request.deliveryMethod());
|
||||
if (requiresOutbound && (!StringUtils.hasText(deliveryMethod) || !DELIVERY_METHODS.contains(deliveryMethod))) {
|
||||
throw badRequest("Delivery method must be TRUCK, PICKUP or EXPRESS when carton labels are changed");
|
||||
throw badRequest("箱唛换标请选择平台卡车、快递、亚马逊自提、客户自提或自提");
|
||||
}
|
||||
if (!requiresOutbound && StringUtils.hasText(deliveryMethod)) {
|
||||
throw badRequest("SKU-only relabel does not create an outbound fulfillment");
|
||||
@@ -933,6 +934,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
throw conflict("Upload the shipping label before confirming relabel completion");
|
||||
}
|
||||
List<RelabelItem> relabelItems = itemList(application.getId());
|
||||
// Manual warehouse completion does not waive physical binding at customer confirmation.
|
||||
bindingGuard.requireBound(application);
|
||||
skuConversions.confirm(application);
|
||||
WarehouseTransferOrder linkedTransfer = warehouseTransfers == null ? null
|
||||
: warehouseTransfers.lockByRelabelNo(customerId, relabelNo);
|
||||
@@ -940,6 +943,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
&& StringUtils.hasText(linkedTransfer.getAllocationIdempotencyKey());
|
||||
boolean requiresOutbound = relabelItems.stream()
|
||||
.anyMatch(item -> Set.of("CARTON_LABEL", "CARTON_AND_SKU").contains(operationType(item)));
|
||||
boolean directOutbound = "EXPRESS".equals(application.getDeliveryMethod())
|
||||
|| "PICKUP".equals(application.getDeliveryMethod())
|
||||
|| "AMAZON_PICKUP".equals(application.getDeliveryMethod())
|
||||
|| "CUSTOMER_PICKUP".equals(application.getDeliveryMethod());
|
||||
reviewTask.setStatus("COMPLETED"); reviewTask.setReviewDecision("CONFIRMED");
|
||||
reviewTask.setReviewerKey(operatorId); reviewTask.setReviewerName("客户确认");
|
||||
reviewTask.setReviewRemark(request.remark()); reviewTask.setReviewedAt(LocalDateTime.now());
|
||||
@@ -951,9 +958,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setCustomerConfirmationKey(key);
|
||||
application.setCustomerConfirmedAt(now);
|
||||
application.setFulfillmentStatus(requiresOutbound
|
||||
? ("EXPRESS".equals(application.getDeliveryMethod()) ? "WAIT_OUTBOUND_CREATION" : "WAIT_PRE_DISPATCH")
|
||||
? (directOutbound ? "WAIT_OUTBOUND_CREATION" : "WAIT_PRE_DISPATCH")
|
||||
: "NOT_REQUIRED");
|
||||
if (requiresOutbound && !existingStockTransfer && "EXPRESS".equals(application.getDeliveryMethod())) {
|
||||
if (requiresOutbound && !existingStockTransfer && directOutbound) {
|
||||
createDirectOutbound(application, operatorId, now);
|
||||
application.setFulfillmentStatus("OUTBOUND_CREATED");
|
||||
}
|
||||
@@ -993,7 +1000,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
carton.setRelabelStatus("COMPLETED"); carton.setRelabelCompletedAt(now);
|
||||
// 换标已完成后释放“当前申请占用”关系,但保留 COMPLETED 作为历史状态。
|
||||
// 否则退货箱虽显示 AVAILABLE,下一次贴标申请仍会被旧 relabel_application_id 拦截。
|
||||
boolean awaitingTruck = requiresOutbound && !"EXPRESS".equals(application.getDeliveryMethod());
|
||||
boolean awaitingTruck = requiresOutbound && !directOutbound;
|
||||
if (!StringUtils.hasText(application.getOutboundOrderNo()) && !awaitingTruck) {
|
||||
carton.setRelabelApplicationId(null); carton.setRelabelNo(null);
|
||||
}
|
||||
@@ -1050,6 +1057,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
boolean relabelHasDestination = !relabelItems.isEmpty() && relabelItems.stream()
|
||||
.allMatch(item -> StringUtils.hasText(item.getDestinationFbaWarehouseCode()));
|
||||
application.setFulfillmentStatus(existingStockTransfer ? "WAIT_TRANSFER_OUTBOUND"
|
||||
: StringUtils.hasText(application.getOutboundOrderNo()) ? "OUTBOUND_CREATED"
|
||||
: "READY_FOR_DISPATCH".equals(transferStatus) || relabelHasDestination
|
||||
? "WAIT_PRE_DISPATCH" : transferStatus);
|
||||
applications.updateById(application);
|
||||
@@ -1085,6 +1093,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (source.getReturnCartonId() == null) continue;
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(source.getReturnCartonId());
|
||||
if (carton == null || !application.getCustomerId().equals(carton.getCustomerId())
|
||||
|| !Objects.equals(application.getOverseasWarehouseId(), carton.getOverseasWarehouseId())
|
||||
|| !application.getId().equals(carton.getRelabelApplicationId())
|
||||
|| !"RELABEL_PENDING".equals(carton.getStatus())
|
||||
|| intValue(source.getNotifiedPackageCount()) != 1)
|
||||
@@ -1103,7 +1112,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
this::outboundSourceKey, LinkedHashMap::new, Collectors.toList()));
|
||||
for (Map.Entry<Long, List<RelabelItem>> entry : byPosition.entrySet()) {
|
||||
InventoryPosition position = entry.getKey() > 0 ? inventoryPositions.selectById(entry.getKey()) : null;
|
||||
if (entry.getKey() > 0 && (position == null || !application.getCustomerId().equals(position.getCustomerId()))) {
|
||||
if (entry.getKey() > 0 && (position == null || !application.getCustomerId().equals(position.getCustomerId())
|
||||
|| !Objects.equals(application.getOverseasWarehouseId(), position.getOverseasWarehouseId()))) {
|
||||
throw conflict("Relabel source inventory position no longer exists");
|
||||
}
|
||||
List<RelabelItem> group = entry.getValue();
|
||||
|
||||
+54
-8
@@ -22,6 +22,7 @@ public class RelabelBindingGuard {
|
||||
@Resource private PdaRelabelHistoryMapper histories;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper conversions;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper returnLines;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@@ -32,10 +33,15 @@ public class RelabelBindingGuard {
|
||||
.contains(item.getOperationType())).toList();
|
||||
if (cartons.isEmpty()) return;
|
||||
Map<String, Set<String>> bindings = new HashMap<>();
|
||||
String currentRound = null;
|
||||
for (PdaRelabelHistory history : histories.selectList(new QueryWrapper<PdaRelabelHistory>()
|
||||
.eq("application_id", app.getId()).eq("customer_id", app.getCustomerId())
|
||||
.orderByAsc("created_at", "id"))) {
|
||||
if ("REWORK_ARCHIVE".equals(history.getAction())) bindings.clear();
|
||||
if ("REWORK_ARCHIVE".equals(history.getAction())) { bindings.clear(); currentRound = null; }
|
||||
if ("COMPLETE".equals(history.getAction())) {
|
||||
try { currentRound = objectMapper.readTree(history.getDetailJson()).path("roundKey").asText(null); }
|
||||
catch (Exception failure) { throw conflict("换标完工轮次记录无法读取"); }
|
||||
}
|
||||
if (!"PALLET_BIND".equals(history.getAction())) continue;
|
||||
try {
|
||||
var data = objectMapper.readTree(history.getDetailJson());
|
||||
@@ -45,20 +51,26 @@ public class RelabelBindingGuard {
|
||||
} catch (Exception e) { throw conflict("组托记录无法读取,请核对后重试"); }
|
||||
}
|
||||
for (RelabelItem item : cartons) {
|
||||
var sourceCarton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
|
||||
if (item.getReturnCartonId() != null && (sourceCarton == null
|
||||
|| !Objects.equals(app.getCustomerId(), sourceCarton.getCustomerId())
|
||||
|| !Objects.equals(app.getOverseasWarehouseId(), sourceCarton.getOverseasWarehouseId())))
|
||||
throw conflict("退货箱来源缺失或仓库不一致");
|
||||
if (item.getReturnCartonId() != null && !Set.of("RETURN_BULK_SKU_LABEL", "SKU_LABEL").contains(item.getOperationType())) {
|
||||
var carton = returnCartons.selectById(item.getReturnCartonId());
|
||||
if (carton == null || !app.getCustomerId().equals(carton.getCustomerId())) throw conflict("退货箱来源缺失");
|
||||
var carton = sourceCarton;
|
||||
if ("CARTON_AND_SKU".equals(item.getOperationType())) {
|
||||
var expected = new HashMap<String, Long>();
|
||||
try {
|
||||
for (var sku : objectMapper.readTree(item.getSkuRelabelItemsJson()))
|
||||
expected.merge(sku.path("skuCode").asText().toUpperCase(Locale.ROOT), sku.path("quantity").asLong(), Long::sum);
|
||||
} catch (Exception failure) { throw conflict("混贴 SKU 明细无法读取"); }
|
||||
projectCurrentContents(app, List.of(item), expected, currentRound);
|
||||
var actual = new HashMap<String, Long>();
|
||||
returnLines.lockCarton(item.getReturnCartonId()).forEach(line -> actual.merge(line.getSkuCode().toUpperCase(Locale.ROOT), line.getQuantity(), Long::sum));
|
||||
if (!expected.equals(actual)) throw conflict("包裹/箱混贴必须包含实物全部 SKU 和件数;部分数量请先隔离为独立新箱");
|
||||
}
|
||||
if ("EXPRESS".equals(app.getDeliveryMethod())) continue;
|
||||
if ("EXPRESS".equals(app.getDeliveryMethod()) || "PICKUP".equals(app.getDeliveryMethod())
|
||||
|| "AMAZON_PICKUP".equals(app.getDeliveryMethod()) || "CUSTOMER_PICKUP".equals(app.getDeliveryMethod())) continue;
|
||||
if (carton.getPalletCode() == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode()))
|
||||
throw conflict("退货箱尚未完成排车托盘绑定");
|
||||
continue;
|
||||
@@ -75,20 +87,54 @@ public class RelabelBindingGuard {
|
||||
}
|
||||
var actual = new HashMap<String, Long>();
|
||||
stock.forEach(i -> actual.merge(i.getSkuCode().toUpperCase(Locale.ROOT), i.getQuantity(), Long::sum));
|
||||
projectCurrentContents(app, sameCarton, selected, currentRound);
|
||||
if (!selected.equals(actual)) throw conflict("部分换标货物必须先合箱生成独立新箱,再提交完工");
|
||||
if ("EXPRESS".equals(app.getDeliveryMethod())) continue;
|
||||
var carton = item.getReturnCartonId() == null ? null : returnCartons.selectById(item.getReturnCartonId());
|
||||
if (carton == null || carton.getPalletCode() == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode())
|
||||
// SKU-only work has no outbound fulfillment, but must still cover the isolated carton.
|
||||
if ("SKU_LABEL".equals(item.getOperationType()) && app.getDeliveryMethod() == null) continue;
|
||||
if ("EXPRESS".equals(app.getDeliveryMethod()) || "PICKUP".equals(app.getDeliveryMethod())
|
||||
|| "AMAZON_PICKUP".equals(app.getDeliveryMethod()) || "CUSTOMER_PICKUP".equals(app.getDeliveryMethod())) continue;
|
||||
var carton = sourceCarton;
|
||||
if (carton.getPalletCode() == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(carton.getPalletCode())
|
||||
|| !"PROCESSING".equals(carton.getRelabelStatus()))
|
||||
throw conflict("退货箱 " + item.getCartonMark() + " 尚未绑定带目的仓的新托盘,不能完成换标");
|
||||
continue;
|
||||
}
|
||||
var position = item.getSourceInventoryPositionId() == null ? null
|
||||
: positions.findTenantPosition(app.getCustomerId(), item.getSourceInventoryPositionId());
|
||||
if (position == null || position.getPalletNo() == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(position.getPalletNo())
|
||||
if (position == null || !Objects.equals(app.getOverseasWarehouseId(), position.getOverseasWarehouseId())
|
||||
|| position.getPalletNo() == null || !bindings.getOrDefault(item.getCartonMark(), Set.of()).contains(position.getPalletNo())
|
||||
|| position.getHoldPackages() == null || position.getHoldPackages() < item.getNotifiedPackageCount())
|
||||
throw conflict("箱唛 " + item.getCartonMark() + " 尚未完成托盘绑定,请先在 PDA 确认组托,不能完成或排车出库");
|
||||
}
|
||||
}
|
||||
|
||||
private void projectCurrentContents(RelabelApplication app, List<RelabelItem> selectedItems,
|
||||
Map<String, Long> expected, String currentRound) {
|
||||
if (!"CUSTOMER_CONFIRM_PENDING".equals(app.getStatus()) && !"READY_FOR_OUTBOUND".equals(app.getStatus())
|
||||
&& !"COMPLETED".equals(app.getStatus())) return;
|
||||
if (currentRound == null || currentRound.isBlank()) throw conflict("换标完工轮次缺失,不能核对当前 SKU 库存");
|
||||
// Warehouse completion has already applied SKU conversions. Confirmation and dispatch
|
||||
// compare against that round's persisted changes, never assume that every new SKU exists.
|
||||
Map<String, Long> outgoing = new HashMap<>(), incoming = new HashMap<>();
|
||||
for (var row : conversions.lockApplication(app.getCustomerId(), app.getId())) {
|
||||
if (!Objects.equals(row.getCustomerId(), app.getCustomerId()) || !Objects.equals(row.getApplicationId(), app.getId())
|
||||
|| !Objects.equals(row.getRoundKey(), currentRound)
|
||||
|| !("APPLIED".equals(row.getStatus()) || "CONFIRMED".equals(row.getStatus()))
|
||||
|| selectedItems.stream().noneMatch(item -> Objects.equals(item.getId(), row.getItemId())
|
||||
&& Objects.equals(item.getReturnCartonId(), row.getCartonId()))) continue;
|
||||
String oldSku = row.getOldSkuCode().toUpperCase(Locale.ROOT);
|
||||
String newSku = row.getNewSkuCode().toUpperCase(Locale.ROOT);
|
||||
long quantity = Objects.requireNonNullElse(row.getQuantity(), 0L);
|
||||
if (quantity <= 0) throw conflict("换标转换数量与批准明细不一致");
|
||||
outgoing.merge(oldSku, quantity, Math::addExact);
|
||||
incoming.merge(newSku, quantity, Math::addExact);
|
||||
}
|
||||
for (var source : outgoing.entrySet()) {
|
||||
long remaining = expected.getOrDefault(source.getKey(), 0L) - source.getValue();
|
||||
if (remaining < 0) throw conflict("换标转换数量与批准明细不一致");
|
||||
if (remaining == 0) expected.remove(source.getKey()); else expected.put(source.getKey(), remaining);
|
||||
}
|
||||
incoming.forEach((sku, quantity) -> expected.merge(sku, quantity, Math::addExact));
|
||||
}
|
||||
private ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user