贴标申请,仓储转运
This commit is contained in:
@@ -2378,7 +2378,7 @@ CREATE TABLE ll_return_inventory_outbound_movement (
|
||||
carton_id BIGINT NOT NULL COMMENT '真实退货库存箱ID',
|
||||
carton_mark VARCHAR(128) NOT NULL COMMENT '冻结的真实退货箱业务码',
|
||||
outbound_no VARCHAR(64) NOT NULL COMMENT '本次退货出库业务单号',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT '本次出库SKU编码快照',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT '本次出库SKU编码快照', return_sku_quantity BIGINT NULL COMMENT '退货单SKU本来源计划出库件数快照,NULL为历史整箱出库',
|
||||
product_name_zh VARCHAR(255) NULL COMMENT '中文品名快照',
|
||||
quantity BIGINT NOT NULL COMMENT '本箱该SKU实际出库件数,始终为正',
|
||||
total_before BIGINT NOT NULL COMMENT '客户仓SKU出库前实物总数量',
|
||||
@@ -2390,11 +2390,11 @@ CREATE TABLE ll_return_inventory_outbound_movement (
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '出库记账时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_return_outbound_pick_sku(outbound_pick_id,sku_code),
|
||||
UNIQUE KEY uk_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code),
|
||||
KEY idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code),
|
||||
KEY idx_ll_return_outbound_movement_query(customer_id,overseas_warehouse_id,sku_code,occurred_at),
|
||||
CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0
|
||||
AND total_before-total_after=quantity AND available_before-available_after=quantity
|
||||
AND cartons_after>=0 AND cartons_before-cartons_after=1 AND total_after>=available_after)
|
||||
AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)
|
||||
) ENGINE=InnoDB COMMENT='不可变退货出库SKU库存流水';
|
||||
|
||||
CREATE TABLE ll_return_inventory_balance (
|
||||
|
||||
+14
-2
@@ -80,7 +80,13 @@ public final class DeliveryRequests {
|
||||
@Size(max=1000) String remark,
|
||||
@NotBlank @Size(max=255) String idempotencyKey,
|
||||
@Size(max=128) String skuCode,
|
||||
@jakarta.validation.constraints.Pattern(regexp = "BOX|PALLET") String unitType) {
|
||||
@jakarta.validation.constraints.Pattern(regexp = "BOX|PALLET") String unitType,
|
||||
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = WholePieceQuantityDeserializer.class)
|
||||
@jakarta.validation.constraints.Positive Long quantity) {
|
||||
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
|
||||
List<String> cartonMarks, String remark, String idempotencyKey, String skuCode, String unitType) {
|
||||
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, skuCode, unitType, null);
|
||||
}
|
||||
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
|
||||
List<String> cartonMarks, String remark, String idempotencyKey) {
|
||||
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, null, null);
|
||||
@@ -114,7 +120,13 @@ public final class DeliveryRequests {
|
||||
public record PdaPick(@NotNull Long outboundPalletItemId, Long inventoryPositionId,
|
||||
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey,
|
||||
@Size(max=500) List<@NotBlank @Size(max=255) String> cartonMarks,
|
||||
@Size(max=128) String skuCode) {
|
||||
@Size(max=128) String skuCode,
|
||||
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = WholePieceQuantityDeserializer.class)
|
||||
@jakarta.validation.constraints.Positive Long productQuantity) {
|
||||
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey,
|
||||
List<String> cartonMarks, String skuCode) {
|
||||
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, cartonMarks, skuCode, null);
|
||||
}
|
||||
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey) {
|
||||
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, List.of(), null);
|
||||
}
|
||||
|
||||
+2
-2
@@ -92,7 +92,7 @@ public final class DeliveryResponses {
|
||||
String skuCode, String inboundCode, long availablePackages,
|
||||
long pickedPackages) {}
|
||||
public record PdaTaskItem(Long outboundPalletItemId, String palletNo, String skuCode, String inboundCode,
|
||||
int packageCount, int pickedPackageCount, String status,
|
||||
List<PdaPickPosition> positions) {}
|
||||
int packageCount, int pickedPackageCount, String status,
|
||||
List<PdaPickPosition> positions, Long returnSkuQuantity) {}
|
||||
public record PdaTaskDetail(PdaTask task, List<PdaTaskItem> items) {}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.delivery.dto;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Piece quantities must not silently truncate a fractional JSON number. */
|
||||
public final class WholePieceQuantityDeserializer extends JsonDeserializer<Long> {
|
||||
@Override public Long deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
||||
if (parser.currentToken() != JsonToken.VALUE_NUMBER_INT)
|
||||
return (Long) context.handleUnexpectedToken(Long.class, parser);
|
||||
return parser.getLongValue();
|
||||
}
|
||||
}
|
||||
+2
@@ -28,6 +28,8 @@ public class OutboundPalletItem {
|
||||
private Integer pickedPackageCount;
|
||||
private String status;
|
||||
private String skuCode;
|
||||
/** Immutable piece quantity for a selected single-SKU return source. Null means legacy whole-carton work. */
|
||||
private Long returnSkuQuantity;
|
||||
private String cartonMarks;
|
||||
private String orderRefs;
|
||||
private String inboundCode;
|
||||
|
||||
+29
-8
@@ -499,6 +499,8 @@ public class DeliveryTransferService {
|
||||
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
|
||||
String warehouseId = request.overseasWarehouseId().trim();
|
||||
if (request.quantity() != null && (!StringUtils.hasText(request.skuCode()) || request.quantity() <= 0
|
||||
|| "PALLET".equals(request.unitType()))) throw conflict("按件数出库必须选择单SKU,不能同时整托出库");
|
||||
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(StringUtils::hasText).distinct().toList();
|
||||
if (marks.size() != request.cartonMarks().size()) throw conflict("箱唛不能为空且不可重复");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -514,6 +516,8 @@ public class DeliveryTransferService {
|
||||
existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
|
||||
for (ReturnInventoryCarton carton : locked) {
|
||||
if ("PARCEL".equals(carton.getSourceUnitType()) && !StringUtils.hasText(request.skuCode()))
|
||||
throw conflict("包裹散件只能按 SKU 出库,不能按箱号或托盘出库");
|
||||
com.baiyue.lastleg.returnreceipt.service.ReturnCartonCountGuard.requireConfirmed(carton);
|
||||
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("退货箱已被其他作业占用:" + carton.getCartonMark());
|
||||
var contents = returnCartonItems.lockCarton(carton.getId());
|
||||
@@ -521,12 +525,22 @@ public class DeliveryTransferService {
|
||||
throw conflict("退货箱缺少明细或已被合箱计划锁定:" + carton.getCartonMark());
|
||||
if (StringUtils.hasText(request.skuCode()) && (contents.size() != 1
|
||||
|| !request.skuCode().trim().equals(contents.get(0).getSkuCode())))
|
||||
throw conflict("单SKU出库只支持指定SKU的散件库存");
|
||||
if (StringUtils.hasText(request.skuCode()) && !"PARCEL".equals(carton.getSourceUnitType()))
|
||||
throw conflict("已装箱库存请按箱唛出库");
|
||||
throw conflict("单SKU出库只支持箱内仅有指定SKU的库存;混合箱请先拆箱");
|
||||
if ("PALLET".equals(request.unitType()) && !StringUtils.hasText(carton.getPalletCode()))
|
||||
throw conflict("整托出库必须存在托盘绑定");
|
||||
}
|
||||
Map<String, Long> skuQuantities = new LinkedHashMap<>();
|
||||
if (StringUtils.hasText(request.skuCode())) {
|
||||
long available = locked.stream().mapToLong(c -> value(c.getTotalQuantity())).sum();
|
||||
long remaining = request.quantity() == null ? available : request.quantity();
|
||||
if (remaining > available) throw conflict("出库件数超过所选来源库存数量");
|
||||
for (ReturnInventoryCarton carton : locked) {
|
||||
long selected = Math.min(remaining, value(carton.getTotalQuantity()));
|
||||
if (selected > 0) skuQuantities.put(carton.getCartonMark(), selected);
|
||||
remaining -= selected;
|
||||
}
|
||||
locked = locked.stream().filter(c -> skuQuantities.containsKey(c.getCartonMark())).toList();
|
||||
}
|
||||
if ("PALLET".equals(request.unitType())) {
|
||||
String pallet = locked.get(0).getPalletCode();
|
||||
if (locked.stream().anyMatch(c -> !Objects.equals(pallet, c.getPalletCode()))) throw conflict("整托出库只能选择同一托盘");
|
||||
@@ -539,7 +553,7 @@ public class DeliveryTransferService {
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RETURN_OUTBOUND", warehouseId, outbound.getId()));
|
||||
outbound.setOutboundType("RETURN_OUTBOUND"); outbound.setStatus("WAIT_PICK");
|
||||
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(marks.size()); outbound.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(locked.size()); outbound.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
outbound.setOverseasWarehouseId(warehouseId); outbound.setIdempotencyKey(request.idempotencyKey().trim());
|
||||
outbound.setRequestHash(requestHash);
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
|
||||
@@ -547,7 +561,9 @@ public class DeliveryTransferService {
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId);
|
||||
item.setOutboundOrderId(outbound.getId()); item.setPackageCount(1); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setCartonMarks(carton.getCartonMark()); item.setInboundCode(carton.getSourceReturnNo());
|
||||
item.setSkuCode(trim(request.skuCode()));
|
||||
// Packed stock is still picked by its physical carton mark; loose parcels use SKU scanning.
|
||||
item.setSkuCode("PARCEL".equals(carton.getSourceUnitType()) ? trim(request.skuCode()) : null);
|
||||
item.setReturnSkuQuantity(skuQuantities.get(carton.getCartonMark()));
|
||||
item.setPalletNo("PALLET".equals(request.unitType()) ? carton.getPalletCode() : null);
|
||||
item.setCreatedAt(now); outboundPallets.insert(item);
|
||||
carton.setStatus("OUTBOUND_PENDING"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
|
||||
@@ -557,7 +573,7 @@ public class DeliveryTransferService {
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(warehouseId); delivery.setDeliveryGroupKey("RETURN:" + request.returnNo().trim());
|
||||
delivery.setStatus("WAIT_DELIVERY"); delivery.setDeliveryAddress(request.recipient().trim());
|
||||
delivery.setPackageCount(marks.size()); delivery.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
delivery.setPackageCount(locked.size()); delivery.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
delivery.setRemark(trim(request.remark())); delivery.setIdempotencyKey(childKey(request.idempotencyKey(), "RETURN-DELIVERY"));
|
||||
delivery.setVersion(0L); delivery.setCreatedBy(operatorId); delivery.setCreatedAt(now); delivery.setUpdatedAt(now);
|
||||
deliveryOrders.insert(delivery);
|
||||
@@ -822,7 +838,10 @@ public class DeliveryTransferService {
|
||||
requireReturnScan(outbound, item, request);
|
||||
if (value(item.getPickedPackageCount()) > 0) throw conflict("退货箱不可重复出库");
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(null); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(), outbound.getOutboundNo(), pick.getId());
|
||||
if (item.getReturnSkuQuantity() != null)
|
||||
returnStock.shipCartonQuantity(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(),
|
||||
outbound.getOutboundNo(), pick.getId(), item.getReturnSkuQuantity());
|
||||
else returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(), outbound.getOutboundNo(), pick.getId());
|
||||
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
|
||||
updateOutboundPickingStatus(customerId, outbound);
|
||||
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
|
||||
@@ -1287,7 +1306,7 @@ public class DeliveryTransferService {
|
||||
value(item.getPickedPackageCount()), item.getStatus(),
|
||||
("RETURN_OUTBOUND".equals(outbound.getOutboundType())
|
||||
|| ("RELABEL_OUTBOUND".equals(outbound.getOutboundType()) && item.getWarehousePalletId() == null)) ? returnPickPositions(outbound, task, item)
|
||||
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item))).toList();
|
||||
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item), item.getReturnSkuQuantity())).toList();
|
||||
return new DeliveryResponses.PdaTaskDetail(taskResult(outbound, task), items);
|
||||
}
|
||||
|
||||
@@ -1326,6 +1345,8 @@ public class DeliveryTransferService {
|
||||
|| !outbound.getCustomerId().equals(item.getCustomerId())) throw notFound("退货出库明细不存在");
|
||||
if (request.inventoryPositionId() != null || !Long.valueOf(1).equals(request.packages()) || value(item.getPackageCount()) != 1)
|
||||
throw conflict("退货出库须逐箱或逐SKU扫描,无需库存仓位");
|
||||
if (item.getReturnSkuQuantity() != null && !item.getReturnSkuQuantity().equals(request.productQuantity()))
|
||||
throw conflict("实际出库件数必须等于本明细计划件数,请核对数量");
|
||||
if (StringUtils.hasText(item.getSkuCode())) {
|
||||
if (!Objects.equals(item.getSkuCode(), trim(request.skuCode()))) throw conflict("请扫描指定的退货SKU");
|
||||
} else if (request.cartonMarks() == null || request.cartonMarks().size() != 1
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@ public final class PdaRelabelResponses {
|
||||
String applicationStatus, String taskStatus, String operationType, int itemCount,
|
||||
int expectedPackages, int relabelledPackages, int skippedPackages,
|
||||
int exceptionPackages, int version, boolean claimedByCurrentOperator,
|
||||
String reviewDecision, String reviewRemark, LocalDateTime reviewedAt) { }
|
||||
String reviewDecision, String reviewRemark, LocalDateTime reviewedAt,
|
||||
String customerName) { }
|
||||
|
||||
public record Detail(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline,
|
||||
String contactName, String contactPhone, String applicationStatus, String operationType,
|
||||
|
||||
+21
-6
@@ -72,6 +72,7 @@ public class PdaRelabelService {
|
||||
@Resource private RelabelPhotoFileGateway photoFiles;
|
||||
@Resource private ObjectMapper sourceObjectMapper;
|
||||
@Resource private com.baiyue.lastleg.mapper.LastlegOrderMapper tailOrders;
|
||||
@Resource private com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory;
|
||||
@Resource private com.baiyue.lastleg.exceptionorder.service.ExceptionOrderService exceptionOrders;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@@ -84,14 +85,27 @@ public class PdaRelabelService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PdaRelabelResponses.Candidate> candidates(String operatorKey, long overseasWarehouseId) {
|
||||
return applications.selectList(new LambdaQueryWrapper<RelabelApplication>()
|
||||
List<RelabelApplication> rows = applications.selectList(new LambdaQueryWrapper<RelabelApplication>()
|
||||
.eq(RelabelApplication::getOverseasWarehouseId, String.valueOf(overseasWarehouseId))
|
||||
.in(RelabelApplication::getStatus, RelabelApplicationService.PENDING_PROCESSING, PROCESSING,
|
||||
RelabelApplicationService.CUSTOMER_CONFIRM_PENDING)
|
||||
.orderByAsc(RelabelApplication::getLabelDeadline)
|
||||
.orderByAsc(RelabelApplication::getCreatedAt))
|
||||
.stream().map(application -> candidate(application,
|
||||
tasks.find(application.getCustomerId(), application.getId()), operatorKey)).toList();
|
||||
.orderByAsc(RelabelApplication::getCreatedAt));
|
||||
List<Long> orderIds = rows.stream().map(RelabelApplication::getLastlegOrderId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
Map<Long, com.baiyue.lastleg.entity.LastlegOrder> linkedOrders = orderIds.isEmpty() ? Map.of()
|
||||
: tailOrders.selectBatchIds(orderIds).stream().collect(Collectors.toMap(
|
||||
com.baiyue.lastleg.entity.LastlegOrder::getId, order -> order));
|
||||
Map<String, String> companyNames = new HashMap<>();
|
||||
return rows.stream().map(application -> {
|
||||
var order = linkedOrders.get(application.getLastlegOrderId());
|
||||
String customerName = order != null && Objects.equals(application.getCustomerId(), order.getCustomerId())
|
||||
? order.getCustomerName() : null;
|
||||
if (!StringUtils.hasText(customerName)) {
|
||||
customerName = companyNames.computeIfAbsent(application.getCustomerId(), customerId -> customerDirectory.find(customerId).name());
|
||||
}
|
||||
return candidate(application, tasks.find(application.getCustomerId(), application.getId()), operatorKey, customerName);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -458,6 +472,7 @@ public class PdaRelabelService {
|
||||
"manualCompletion", request.manualCompletion()));
|
||||
applicationService.changeStatus(application.getCustomerId(), relabelNo, operatorKey,
|
||||
new RelabelRequests.ChangeStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, request.remark()));
|
||||
applicationService.settlePdaCompletion(application.getCustomerId(), relabelNo, operatorKey);
|
||||
application.setStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING);
|
||||
PdaRelabelResponses.Detail result = detail(application, task);
|
||||
saveOperation(task, operatorKey, "COMPLETE", request.idempotencyKey(), command, result);
|
||||
@@ -704,7 +719,7 @@ public class PdaRelabelService {
|
||||
}
|
||||
|
||||
private PdaRelabelResponses.Candidate candidate(RelabelApplication application, PdaRelabelTask task,
|
||||
String operatorKey) {
|
||||
String operatorKey, String customerName) {
|
||||
return new PdaRelabelResponses.Candidate(application.getRelabelNo(), application.getCustomerOrderNo(),
|
||||
application.getLabelDeadline(), application.getStatus(), effectiveTaskStatus(application, task),
|
||||
operationType(application), value(application.getItemCount()),
|
||||
@@ -713,7 +728,7 @@ public class PdaRelabelService {
|
||||
task == null ? 0 : value(task.getExceptionPackages()), task == null ? 0 : value(task.getVersion()),
|
||||
task != null && PROCESSING.equals(task.getStatus()) && operatorKey.equals(task.getOperatorKey()),
|
||||
task == null ? null : task.getReviewDecision(), task == null ? null : task.getReviewRemark(),
|
||||
task == null ? null : task.getReviewedAt());
|
||||
task == null ? null : task.getReviewedAt(), customerName);
|
||||
}
|
||||
|
||||
private PdaRelabelResponses.Detail detail(RelabelApplication application, PdaRelabelTask task) {
|
||||
|
||||
+9
-1
@@ -850,7 +850,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
private void publishCompletionFeesAndSettlement(RelabelApplication application, String operatorId,
|
||||
List<LastlegOrder> linkedOrders) {
|
||||
if ("NOT_REQUIRED".equals(application.getWalletStatus())) return;
|
||||
if (Set.of("NOT_REQUIRED", "SETTLE_PENDING", "SETTLED").contains(Objects.toString(application.getWalletStatus(), ""))) return;
|
||||
BigDecimal quantity = BigDecimal.valueOf(Objects.requireNonNullElse(application.getTotalPackageCount(), 0));
|
||||
String customerId = application.getCustomerId(), relabelNo = application.getRelabelNo();
|
||||
feeSources.publishApproved(operatorId, "RELABEL:" + relabelNo,
|
||||
@@ -868,6 +868,14 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setWalletStatus("SETTLE_PENDING"); applications.updateById(application);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void settlePdaCompletion(String customerId, String relabelNo, String operatorId) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
if (!CUSTOMER_CONFIRM_PENDING.equals(application.getStatus())) throw conflict("换标尚未完成,不能扣款");
|
||||
publishCompletionFeesAndSettlement(application, operatorId, tailOrders(application));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail confirmCompletion(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.CustomerConfirm request) {
|
||||
|
||||
+6
@@ -12,6 +12,12 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventoryBalance> {
|
||||
@Update("UPDATE ll_return_inventory_balance SET total_quantity=total_quantity-#{quantity}, " +
|
||||
"available_quantity=available_quantity-#{quantity},updated_at=NOW(3) " +
|
||||
"WHERE id=#{id} AND customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND total_quantity>=#{quantity} AND available_quantity>=#{quantity} AND carton_quantity>=1")
|
||||
int deductOutboundPieces(@Param("id") long id, @Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId, @Param("quantity") long quantity);
|
||||
String FREE_STOCK_COLUMNS = "b.id,b.customer_id,b.overseas_warehouse_id,b.sku_code,b.product_name_zh,b.total_quantity,b.carton_quantity,b.created_at,b.updated_at," +
|
||||
"LEAST(b.available_quantity,COALESCE((SELECT SUM(GREATEST(0,i.quantity-COALESCE(i.packing_reserved_quantity,0))) " +
|
||||
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id=c.customer_id " +
|
||||
|
||||
+34
-9
@@ -382,6 +382,17 @@ public class ReturnInventoryService {
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
public void shipCarton(String customerId, String warehouseId, String returnNo, String cartonMark,
|
||||
String outboundNo, long outboundPickId) {
|
||||
shipCartonInternal(customerId, warehouseId, returnNo, cartonMark, outboundNo, outboundPickId, null);
|
||||
}
|
||||
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
public void shipCartonQuantity(String customerId, String warehouseId, String returnNo, String cartonMark,
|
||||
String outboundNo, long outboundPickId, long quantity) {
|
||||
shipCartonInternal(customerId, warehouseId, returnNo, cartonMark, outboundNo, outboundPickId, quantity);
|
||||
}
|
||||
|
||||
private void shipCartonInternal(String customerId, String warehouseId, String returnNo, String cartonMark,
|
||||
String outboundNo, long outboundPickId, Long requestedQuantity) {
|
||||
ReturnInventoryCarton carton = cartons.lockMark(required(cartonMark, "退货箱码不能为空"));
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !warehouseId.equals(carton.getOverseasWarehouseId()) || !matchesReturnSource(carton, returnNo)
|
||||
@@ -400,22 +411,27 @@ public class ReturnInventoryService {
|
||||
}
|
||||
if (contents.isEmpty() || total != value(carton.getTotalQuantity())
|
||||
|| carton.getSkuTypes() == null || contents.size() != carton.getSkuTypes()) throw conflict("退货箱内容与实物数量不一致");
|
||||
if (requestedQuantity != null && (contents.size() != 1 || requestedQuantity <= 0 || requestedQuantity > total))
|
||||
throw conflict("单SKU出库件数或来源明细不匹配");
|
||||
boolean partial = requestedQuantity != null && requestedQuantity < total;
|
||||
boolean parcel = "PARCEL".equals(carton.getSourceUnitType());
|
||||
long cartonDelta = partial || parcel ? 0 : 1;
|
||||
contents = contents.stream().sorted(Comparator.comparing(ReturnInventoryCartonItem::getSkuCode)).toList();
|
||||
String scopedCustomer = carton.getCustomerId(), scopedWarehouse = carton.getOverseasWarehouseId();
|
||||
Map<String, com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance> lockedBalances = new LinkedHashMap<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (ReturnInventoryCartonItem content : contents) {
|
||||
var balance = balances.lockSku(scopedCustomer, scopedWarehouse, content.getSkuCode());
|
||||
long quantity = content.getQuantity();
|
||||
long quantity = requestedQuantity == null ? content.getQuantity() : requestedQuantity;
|
||||
if (balance == null || value(balance.getTotalQuantity()) < quantity
|
||||
|| value(balance.getAvailableQuantity()) < quantity || value(balance.getCartonQuantity()) < 1) {
|
||||
|| value(balance.getAvailableQuantity()) < quantity || (!parcel && value(balance.getCartonQuantity()) < 1)) {
|
||||
throw conflict("退货库存不足,不能完成该箱出库:" + content.getSkuCode());
|
||||
}
|
||||
lockedBalances.put(content.getSkuCode(), balance);
|
||||
}
|
||||
for (ReturnInventoryCartonItem content : contents) {
|
||||
var balance = lockedBalances.get(content.getSkuCode());
|
||||
long quantity = content.getQuantity();
|
||||
long quantity = requestedQuantity == null ? content.getQuantity() : requestedQuantity;
|
||||
var movement = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOutboundMovement();
|
||||
movement.setId(IdWorker.getId()); movement.setOutboundPickId(outboundPickId);
|
||||
movement.setCustomerId(scopedCustomer); movement.setOverseasWarehouseId(scopedWarehouse);
|
||||
@@ -424,12 +440,19 @@ public class ReturnInventoryService {
|
||||
movement.setSkuCode(content.getSkuCode()); movement.setProductNameZh(content.getProductNameZh()); movement.setQuantity(quantity);
|
||||
movement.setTotalBefore(balance.getTotalQuantity()); movement.setTotalAfter(balance.getTotalQuantity() - quantity);
|
||||
movement.setAvailableBefore(balance.getAvailableQuantity()); movement.setAvailableAfter(balance.getAvailableQuantity() - quantity);
|
||||
movement.setCartonsBefore(balance.getCartonQuantity()); movement.setCartonsAfter(balance.getCartonQuantity() - 1);
|
||||
movement.setCartonsBefore(balance.getCartonQuantity()); movement.setCartonsAfter(balance.getCartonQuantity() - cartonDelta);
|
||||
movement.setOccurredAt(now);
|
||||
if (balances.deductOutbound(balance.getId(), scopedCustomer, scopedWarehouse, quantity) != 1) throw conflict("退货库存余额已变化,请重新核对");
|
||||
int changed = partial || parcel ? balances.deductOutboundPieces(balance.getId(), scopedCustomer, scopedWarehouse, quantity)
|
||||
: balances.deductOutbound(balance.getId(), scopedCustomer, scopedWarehouse, quantity);
|
||||
if (changed != 1) throw conflict("退货库存余额已变化,请重新核对");
|
||||
if (movements.insert(movement) != 1) throw new IllegalStateException("退货出库流水写入失败");
|
||||
if (partial) {
|
||||
content.setQuantity(content.getQuantity() - quantity);
|
||||
if (cartonItems.updateById(content) != 1) throw conflict("退货箱内数量已变化,请重新核对");
|
||||
}
|
||||
}
|
||||
carton.setStatus("OUTBOUNDED"); carton.setUpdatedAt(now);
|
||||
if (partial) carton.setTotalQuantity(total - requestedQuantity);
|
||||
carton.setStatus(partial ? "AVAILABLE" : "OUTBOUNDED"); carton.setUpdatedAt(now);
|
||||
if (cartons.updateById(carton) != 1) throw conflict("退货箱状态已变化,请重新核对");
|
||||
}
|
||||
|
||||
@@ -441,7 +464,9 @@ public class ReturnInventoryService {
|
||||
required(lines.get(0).skuCode(), "SKU 不能为空");
|
||||
String cartonMark = text(requestedMark);
|
||||
long cartonId = IdWorker.getId();
|
||||
if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
|
||||
// A parcel row tracks loose SKU provenance; it is never a physical carton or printable box label.
|
||||
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = "SP" + hash(List.of(receipt.getId(), lines.get(0).skuCode())).substring(0, 24).toUpperCase(java.util.Locale.ROOT);
|
||||
else if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
|
||||
if (cartons.findMark(cartonMark) != null) throw conflict("箱号已存在: " + cartonMark);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long total = lines.stream().mapToLong(ReceiptLine::quantity).sum();
|
||||
@@ -465,7 +490,7 @@ public class ReturnInventoryService {
|
||||
item.setSkuCode(line.skuCode()); item.setProductNameZh(text(line.productNameZh())); item.setQuantity(line.quantity());
|
||||
cartonItems.insert(item);
|
||||
balances.adjust(IdWorker.getId(), order.getCustomerId(), order.getOverseasWarehouseId(), line.skuCode(),
|
||||
text(line.productNameZh()), line.quantity(), line.quantity(), 1);
|
||||
text(line.productNameZh()), line.quantity(), line.quantity(), "PARCEL".equals(receipt.getUnitType()) ? 0 : 1);
|
||||
}
|
||||
if ("BOX".equals(receipt.getUnitType())) {
|
||||
var contents = lines.stream().map(line -> new com.baiyue.lastleg.returnreceipt.dto.ReturnCartonCountDtos.Sku(line.skuCode(), line.productNameZh(), line.quantity())).toList();
|
||||
@@ -968,7 +993,7 @@ public class ReturnInventoryService {
|
||||
productNames.putIfAbsent(sku, item.getProductNameZh());
|
||||
item.setQuantity(item.getQuantity() - take);
|
||||
if (item.getQuantity() == 0) cartonItems.deleteById(item.getId()); else cartonItems.updateById(item);
|
||||
if (item.getQuantity() == 0) emptiedBySku.merge(sku, 1, Integer::sum);
|
||||
if (item.getQuantity() == 0 && !"PARCEL".equals(source.getSourceUnitType())) emptiedBySku.merge(sku, 1, Integer::sum);
|
||||
}
|
||||
if (item.getQuantity() > 0) { sourceTotal = Math.addExact(sourceTotal, item.getQuantity()); sourceTypes++; }
|
||||
}
|
||||
|
||||
+7
-4
@@ -255,7 +255,7 @@ public class ReturnReceiptService {
|
||||
.toList();
|
||||
long boxes = value(receipt.getTotalBoxCount());
|
||||
if (boxes == 0) boxes = lines.stream().mapToLong(ReturnOrderResponses.ReceiptItem::boxCount).sum();
|
||||
String cartonMark = text(receipt.getCartonMark());
|
||||
String cartonMark = "PARCEL".equals(receipt.getUnitType()) ? null : text(receipt.getCartonMark());
|
||||
List<ReturnInventoryService.ReturnOrderLine> labelLines = lines.stream()
|
||||
.map(line -> new ReturnInventoryService.ReturnOrderLine(line.skuCode(), line.productNameZh(), line.quantity(), line.boxCount()))
|
||||
.toList();
|
||||
@@ -579,6 +579,9 @@ public class ReturnReceiptService {
|
||||
if (unitCode == null && "PALLET".equals(unitType)) unitCode = text(request.palletCode());
|
||||
if (unitCode == null && "BOX".equals(unitType))
|
||||
unitCode = "RC" + hash(List.of(order.getReturnNo(), IdWorker.getId())).substring(0, 16).toUpperCase(java.util.Locale.ROOT) + "#1-1";
|
||||
if (unitCode == null && "PARCEL".equals(unitType)) {
|
||||
unitCode = "PK" + hash(List.of(order.getReturnNo(), IdWorker.getId())).substring(0, 16).toUpperCase(java.util.Locale.ROOT);
|
||||
}
|
||||
if (unitCode == null) throw badRequest(("PALLET".equals(unitType) ? "托盘码" : "包裹码") + "不能为空");
|
||||
if ("PALLET".equals(unitType)
|
||||
&& (request.length() == null || request.width() == null || request.height() == null || request.weight() == null
|
||||
@@ -633,7 +636,7 @@ public class ReturnReceiptService {
|
||||
} else {
|
||||
inventoryCartons = List.of(inventory.createReceiptCarton(order, receipt, unitCode, new ArrayList<>(inventoryLines.values())));
|
||||
}
|
||||
receipt.setCartonMark(inventoryCartons.get(0).getCartonMark());
|
||||
receipt.setCartonMark("PARCEL".equals(unitType) ? null : inventoryCartons.get(0).getCartonMark());
|
||||
receipts.updateById(receipt);
|
||||
syncTailCargo(order, receipt, allocations, now);
|
||||
List<ReturnOrderItem> finalItems = orderItems.listOrder(order.getId());
|
||||
@@ -777,10 +780,10 @@ public class ReturnReceiptService {
|
||||
return confirmed(order, receipt, COMPLETED.equals(order.getStatus()));
|
||||
}
|
||||
private PdaReturnReceiptResponses.Confirmed confirmed(ReturnOrder order, ReturnReceipt receipt, boolean completed) {
|
||||
String cartonMark = text(receipt.getCartonMark());
|
||||
String cartonMark = "PARCEL".equals(receipt.getUnitType()) ? null : text(receipt.getCartonMark());
|
||||
String printableHtml = null;
|
||||
List<ReturnOrderResponses.CartonLabel> labels = new java.util.ArrayList<>();
|
||||
if (receipt.getId() != null) {
|
||||
if (receipt.getId() != null && !"PARCEL".equals(receipt.getUnitType())) {
|
||||
for (ReturnInventoryCarton carton : cartons.listReceipt(receipt.getId())) {
|
||||
var label = inventory.label(order.getCustomerId(), carton.getCartonMark());
|
||||
if (label != null) labels.add(new ReturnOrderResponses.CartonLabel(carton.getCartonMark(), label.printableHtml()));
|
||||
|
||||
@@ -2043,7 +2043,7 @@ CREATE TABLE IF NOT EXISTS ll_outbound_pallet_item (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', outbound_order_id BIGINT NOT NULL COMMENT '出库单ID',
|
||||
warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID', pallet_no VARCHAR(128) NULL COMMENT '托盘号',
|
||||
destination_warehouse_id VARCHAR(128) NULL COMMENT '目的仓ID', destination_warehouse_code VARCHAR(128) NULL COMMENT '目的仓编码快照', destination_warehouse_name VARCHAR(255) NULL COMMENT '目的仓名称快照',
|
||||
package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', gross_weight_kg DECIMAL(18,3) NULL COMMENT '毛重(千克),来源未确认时为空', volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)', volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '体积(立方米)', picked_package_count INT NOT NULL DEFAULT 0 COMMENT '已拣箱数', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_PICK' COMMENT '拣货状态', sku_code VARCHAR(128) NULL COMMENT '一件代发SKU', order_refs VARCHAR(2000) NULL COMMENT '关联订单信息', inbound_code VARCHAR(128) NULL COMMENT '入仓单号快照',
|
||||
package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', gross_weight_kg DECIMAL(18,3) NULL COMMENT '毛重(千克),来源未确认时为空', volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)', volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '体积(立方米)', picked_package_count INT NOT NULL DEFAULT 0 COMMENT '已拣箱数', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_PICK' COMMENT '拣货状态', sku_code VARCHAR(128) NULL COMMENT '一件代发SKU', return_sku_quantity BIGINT NULL COMMENT '退货单SKU本来源计划出库件数快照,NULL为历史整箱出库', order_refs VARCHAR(2000) NULL COMMENT '关联订单信息', inbound_code VARCHAR(128) NULL COMMENT '入仓单号快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ll_outbound_pallet (customer_id,outbound_order_id,warehouse_pallet_id,inbound_code,sku_code),
|
||||
KEY idx_ll_outbound_pallet_list (customer_id,outbound_order_id), KEY idx_ll_outbound_pallet_destination (customer_id,destination_warehouse_code)
|
||||
@@ -3079,11 +3079,11 @@ CREATE TABLE IF NOT EXISTS ll_return_inventory_outbound_movement (
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '出库记账时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_return_outbound_pick_sku(outbound_pick_id,sku_code),
|
||||
UNIQUE KEY uk_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code),
|
||||
KEY idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code),
|
||||
KEY idx_ll_return_outbound_movement_query(customer_id,overseas_warehouse_id,sku_code,occurred_at),
|
||||
CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0
|
||||
AND total_before-total_after=quantity AND available_before-available_after=quantity
|
||||
AND cartons_after>=0 AND cartons_before-cartons_after=1 AND total_after>=available_after)
|
||||
AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)
|
||||
) ENGINE=InnoDB COMMENT='不可变退货出库SKU库存流水';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_return_inventory_balance (
|
||||
@@ -3264,6 +3264,29 @@ CREATE TABLE IF NOT EXISTS ll_return_carton_count_session (
|
||||
UNIQUE KEY uk_ll_return_count_session_receipt(receipt_id)
|
||||
) ENGINE=InnoDB COMMENT='退货箱内清点单人单任务持久会话';
|
||||
|
||||
-- 退货单 SKU 按件数出库快照。目标库:baiyue_lastleg。可重复执行。
|
||||
|
||||
SET @return_sku_quantity_ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_outbound_pallet_item' AND column_name='return_sku_quantity'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE ll_outbound_pallet_item ADD COLUMN return_sku_quantity BIGINT NULL COMMENT ''退货单SKU本来源计划出库件数快照,NULL为历史整箱出库'' AFTER sku_code'
|
||||
);
|
||||
PREPARE return_sku_quantity_stmt FROM @return_sku_quantity_ddl;
|
||||
EXECUTE return_sku_quantity_stmt;
|
||||
DEALLOCATE PREPARE return_sku_quantity_stmt;
|
||||
|
||||
-- 部分出库仅扣件数;整箱扣一箱,保留守恒和非负约束。
|
||||
SET @return_sku_check_ddl = IF(EXISTS(SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND constraint_name='chk_ll_return_outbound_movement_delta'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP CHECK chk_ll_return_outbound_movement_delta, ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)', 'ALTER TABLE ll_return_inventory_outbound_movement ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)');
|
||||
PREPARE return_sku_check_stmt FROM @return_sku_check_ddl;
|
||||
EXECUTE return_sku_check_stmt;
|
||||
DEALLOCATE PREPARE return_sku_check_stmt;
|
||||
-- 同箱余数允许再次出库,防重仍由拣货ID+SKU唯一索引保证。
|
||||
SET @return_sku_index_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND index_name='uk_ll_return_outbound_carton_sku'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP INDEX uk_ll_return_outbound_carton_sku, ADD INDEX idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code)', 'SELECT 1');
|
||||
PREPARE return_sku_index_stmt FROM @return_sku_index_ddl;
|
||||
EXECUTE return_sku_index_stmt;
|
||||
DEALLOCATE PREPARE return_sku_index_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_procurement_wallet_attempt (
|
||||
id BIGINT NOT NULL COMMENT '请求尝试ID',
|
||||
order_id BIGINT NOT NULL COMMENT '采购订单ID',
|
||||
|
||||
@@ -822,6 +822,30 @@ paths:
|
||||
- { name: warehouseId, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { type: string } }
|
||||
responses: { '200': { description: 出库单分页 } }
|
||||
/api/platform/v1/last-leg/outbound-orders/return:
|
||||
post:
|
||||
summary: 创建退货出库单,支持单SKU全部或部分件数
|
||||
description: 权限 platform:overseas-warehouse:outbound:create;单SKU来源须为纯SKU可用箱或散件。混合SKU箱须先拆箱。按箱唛排序分配件数,未拣货前占用整个使用来源;拣货后原箱余数恢复可用。相同幂等键改数量拒绝。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: customerId, in: query, required: true, schema: { type: string } }]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [recipient, overseasWarehouseId, returnNo, cartonMarks, idempotencyKey]
|
||||
properties:
|
||||
recipient: { type: string, maxLength: 1000 }
|
||||
overseasWarehouseId: { type: string, maxLength: 128 }
|
||||
returnNo: { type: string, maxLength: 128 }
|
||||
cartonMarks: { type: array, minItems: 1, maxItems: 200, uniqueItems: true, items: { type: string, maxLength: 128 } }
|
||||
skuCode: { type: string, maxLength: 128, nullable: true }
|
||||
quantity: { type: integer, format: int64, minimum: 1, nullable: true, description: 单SKU计划件数,不得超过所选来源总数;省略出全部,只允许skuCode已填且非PALLET维度,不接受小数 }
|
||||
unitType: { type: string, enum: [BOX, PALLET], nullable: true }
|
||||
remark: { type: string, maxLength: 1000, nullable: true }
|
||||
idempotencyKey: { type: string, maxLength: 255 }
|
||||
responses: { '200': { description: 退货出库PDA任务 }, '400': { description: 数量或请求格式错误 }, '409': { description: 来源占用、库存或幂等冲突 } }
|
||||
/api/platform/v1/last-leg/outbound-orders/one-click-dropship:
|
||||
post:
|
||||
summary: 创建一件代发出库单
|
||||
|
||||
@@ -875,6 +875,8 @@ components:
|
||||
outboundPalletItemId: { type: string, pattern: '^[0-9]+$' }
|
||||
inventoryPositionId: { type: string, nullable: true, pattern: '^[0-9]+$', description: 普通库存使用真实仓位键;退货箱库存没有该仓位,提交 null,不提交空字符串或退货库存内部ID }
|
||||
packages: { type: integer, format: int64, minimum: 1 }
|
||||
productQuantity: { type: integer, format: int64, minimum: 1, nullable: true, description: 单SKU退货明细存在returnSkuQuantity时必填且必须等于计划件数;不接受小数。历史无快照明细可省略 }
|
||||
skuCode: { type: string, maxLength: 128, nullable: true, description: PARCEL散件单SKU扫码值;已装箱来源必须扫描箱唛 }
|
||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||
cartonMarks:
|
||||
type: array
|
||||
@@ -911,6 +913,7 @@ components:
|
||||
outboundPalletItemId: { type: string, description: pick请求提交键,不作为用户展示编号 }
|
||||
palletNo: { type: string, nullable: true, description: Warehouse 权威实物托盘码;退货箱明细不伪造托盘码 }
|
||||
skuCode: { type: string, nullable: true }
|
||||
returnSkuQuantity: { type: integer, format: int64, nullable: true, minimum: 1, description: 本来源计划出库件数快照;部分拣货后余数保留原箱,PDA不得改变计划件数 }
|
||||
inboundCode: { type: string }
|
||||
packageCount: { type: integer }
|
||||
pickedPackageCount: { type: integer }
|
||||
|
||||
+32
@@ -236,6 +236,38 @@ class DeliveryTransferServiceTest {
|
||||
assertEquals(1, delivery.getValue().getPackageCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialSkuOutboundDistributesSixPiecesAcrossTwoFivePieceSources() {
|
||||
var first = returnCarton("RC-1", null); first.setId(901L); first.setTotalQuantity(5L);
|
||||
var second = returnCarton("RC-2", null); second.setId(902L); second.setTotalQuantity(5L);
|
||||
when(returnCartons.lockMark("RC-1")).thenReturn(first); when(returnCartons.lockMark("RC-2")).thenReturn(second);
|
||||
var line = new ReturnInventoryCartonItem(); line.setSkuCode("SKU-A"); line.setQuantity(5L);
|
||||
when(returnCartonItems.lockCarton(901L)).thenReturn(List.of(line));
|
||||
when(returnCartonItems.lockCarton(902L)).thenReturn(List.of(line));
|
||||
service.createReturnOutbound("TENANT-A", "U-1", new DeliveryRequests.ReturnOutbound("receiver", "WH-1", "RT-1",
|
||||
List.of("RC-1", "RC-2"), null, "partial-six", "SKU-A", "BOX", 6L));
|
||||
var capture = ArgumentCaptor.forClass(OutboundPalletItem.class);
|
||||
verify(outboundPallets, org.mockito.Mockito.times(2)).insert(capture.capture());
|
||||
assertEquals(List.of(5L, 1L), capture.getAllValues().stream().map(OutboundPalletItem::getReturnSkuQuantity).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleSkuReturnOutboundAcceptsPackedSingleSkuButKeepsCartonScanning() {
|
||||
ReturnInventoryCarton carton = returnCarton("RC-BOX", "PAL-1");
|
||||
carton.setSourceUnitType("BOX"); carton.setTotalQuantity(5L);
|
||||
ReturnInventoryCartonItem line = new ReturnInventoryCartonItem();
|
||||
line.setSkuCode("SKU-A"); line.setQuantity(5L);
|
||||
when(returnCartons.lockMark("RC-BOX")).thenReturn(carton);
|
||||
when(returnCartonItems.lockCarton(carton.getId())).thenReturn(List.of(line));
|
||||
service.createReturnOutbound("TENANT-A", "U-1",
|
||||
new DeliveryRequests.ReturnOutbound("receiver", "WH-1", "RT-1", List.of("RC-BOX"), null, "packed-sku", "SKU-A"));
|
||||
ArgumentCaptor<OutboundPalletItem> item = ArgumentCaptor.forClass(OutboundPalletItem.class);
|
||||
verify(outboundPallets).insert(item.capture());
|
||||
assertNull(item.getValue().getSkuCode());
|
||||
assertEquals("RC-BOX", item.getValue().getCartonMarks());
|
||||
assertEquals("OUTBOUND_PENDING", carton.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleSkuReturnOutboundRejectsMixedCarton() {
|
||||
ReturnInventoryCarton mixed = returnCarton("RC-MIXED", null);
|
||||
|
||||
+11
-2
@@ -71,9 +71,18 @@ class ReturnOutboundClosureTest {
|
||||
verify(outbounds, times(1)).insert(any(OutboundOrder.class));
|
||||
}
|
||||
|
||||
@Test void preventsSingleSkuBypassingExistingBox() {
|
||||
@Test void singleSkuPackedBoxKeepsCartonScanAndPieceSnapshot() {
|
||||
service.createReturnOutbound("C1", "USER", command("SKU1"));
|
||||
verify(items).insert(argThat((OutboundPalletItem i) -> i.getSkuCode() == null && Long.valueOf(6L).equals(i.getReturnSkuQuantity())));
|
||||
assertThat(carton.getStatus()).isEqualTo("OUTBOUND_PENDING");
|
||||
}
|
||||
|
||||
@Test void singleSkuCannotConsumeMixedSkuCarton() {
|
||||
ReturnInventoryCartonItem other = new ReturnInventoryCartonItem(); other.setSkuCode("SKU2"); other.setQuantity(1L);
|
||||
var first = contents.lockCarton(10L).get(0);
|
||||
when(contents.lockCarton(10L)).thenReturn(List.of(first, other));
|
||||
assertThatThrownBy(() -> service.createReturnOutbound("C1", "USER", command("SKU1")))
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("按箱唛");
|
||||
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("SKU");
|
||||
verify(outbounds, never()).insert(any(OutboundOrder.class));
|
||||
}
|
||||
|
||||
|
||||
+18
@@ -82,6 +82,24 @@ class PdaRelabelServiceTest {
|
||||
lenient().when(tasks.updateWithVersion(any(), anyInt())).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidatesUseCustomerNameOnlyFromMatchingCustomerOrder() {
|
||||
var orders = org.mockito.Mockito.mock(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);
|
||||
var directory = org.mockito.Mockito.mock(com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory.class);
|
||||
ReflectionTestUtils.setField(service, "customerDirectory", directory);
|
||||
ReflectionTestUtils.setField(service, "tailOrders", orders);
|
||||
application.setLastlegOrderId(20L);
|
||||
var order = new com.baiyue.lastleg.entity.LastlegOrder();
|
||||
order.setId(20L); order.setCustomerId("100"); order.setCustomerName("测试客户");
|
||||
when(applications.selectList(any())).thenReturn(List.of(application));
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(order));
|
||||
assertEquals("测试客户", service.candidates("7", 900L).get(0).customerName());
|
||||
order.setCustomerId("OTHER");
|
||||
when(directory.find("100")).thenReturn(new com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory.Customer("100", "C100", "所属公司", null, null));
|
||||
assertEquals("所属公司", service.candidates("7", 900L).get(0).customerName());
|
||||
verify(orders, org.mockito.Mockito.times(2)).selectBatchIds(List.of(20L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimUsesFrozenWarehouseAndAdvancesExistingApplicationStateMachine() {
|
||||
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
|
||||
|
||||
+15
@@ -54,6 +54,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
@@ -643,6 +644,18 @@ class RelabelApplicationServiceTest {
|
||||
assertEquals(6L, move.getValue().packages());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaCompletionSettlesBeforeCustomerConfirmationAndDoesNotSettleTwice() {
|
||||
var app = confirmationApplication("TRUCK", false);
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(app);
|
||||
when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-1", "WAIT_RELABEL"));
|
||||
service.settlePdaCompletion("10001", "FBR-1", "picker");
|
||||
service.settlePdaCompletion("10001", "FBR-1", "picker");
|
||||
verify(walletCommands).publish(eq("SETTLE"), eq(10001L), eq("RELABEL_ORDER"), eq("FBR-1"), isNull(), anyString(), eq(2L));
|
||||
assertEquals("SETTLE_PENDING", app.getWalletStatus());
|
||||
assertNull(app.getCustomerConfirmedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void truckCustomerConfirmationIsIdempotentAndWaitsForPreDispatch() {
|
||||
RelabelApplication application = confirmationApplication("TRUCK", false);
|
||||
@@ -662,6 +675,7 @@ class RelabelApplicationServiceTest {
|
||||
assertEquals("CONFIRMED", review.getValue().getReviewDecision());
|
||||
assertEquals("WAIT_OUTBOUND", linked.getCurrentStatus());
|
||||
assertNull(application.getOutboundOrderNo());
|
||||
verify(walletCommands, never()).publish(eq("FREEZE"), anyLong(), anyString(), anyString(), any(), anyString(), anyLong());
|
||||
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
|
||||
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1",
|
||||
@@ -838,6 +852,7 @@ class RelabelApplicationServiceTest {
|
||||
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash",
|
||||
RelabelApplicationService.CUSTOMER_CONFIRM_PENDING);
|
||||
application.setLastlegOrderId(21L);
|
||||
application.setOverseasWarehouseId("900");
|
||||
application.setDeliveryMethod(deliveryMethod);
|
||||
application.setPrintShippingLabel(printShippingLabel);
|
||||
return application;
|
||||
|
||||
+38
-2
@@ -30,6 +30,7 @@ import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -75,6 +76,30 @@ class ReturnInventoryServiceTest {
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialSkuShipmentKeepsOriginalCartonAndDoesNotSubtractCartonCount() {
|
||||
var carton = new ReturnInventoryCarton(); carton.setId(31L); carton.setCartonMark("RC-1");
|
||||
carton.setCustomerId("10001"); carton.setOverseasWarehouseId("42"); carton.setSourceReturnNo("RT-1");
|
||||
carton.setStatus("OUTBOUND_PENDING"); carton.setCountStatus("CONFIRMED"); carton.setSkuTypes(1); carton.setTotalQuantity(5L);
|
||||
var line = item(32L, "SKU-A", 5);
|
||||
var balance = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance();
|
||||
balance.setId(41L); balance.setTotalQuantity(5L); balance.setAvailableQuantity(5L); balance.setCartonQuantity(1L);
|
||||
when(cartons.lockMark("RC-1")).thenReturn(carton);
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(line));
|
||||
when(balances.lockSku("10001", "42", "SKU-A")).thenReturn(balance);
|
||||
when(balances.deductOutboundPieces(41L, "10001", "42", 2L)).thenReturn(1);
|
||||
when(movements.insert(any(com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOutboundMovement.class))).thenReturn(1);
|
||||
when(cartonItems.updateById(line)).thenReturn(1); when(cartons.updateById(carton)).thenReturn(1);
|
||||
service.shipCartonQuantity("10001", "42", "RT-1", "RC-1", "OB-1", 51L, 2L);
|
||||
assertThat(carton.getStatus()).isEqualTo("AVAILABLE"); assertThat(carton.getTotalQuantity()).isEqualTo(3L);
|
||||
assertThat(line.getQuantity()).isEqualTo(3L);
|
||||
var capture = org.mockito.ArgumentCaptor.forClass(com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOutboundMovement.class);
|
||||
verify(movements).insert(capture.capture());
|
||||
assertThat(capture.getValue().getQuantity()).isEqualTo(2L);
|
||||
assertThat(capture.getValue().getCartonsAfter()).isEqualTo(1L);
|
||||
verify(balances, never()).deductOutbound(anyLong(), anyString(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformPlanNumberFilterIsTrimmedAndUsedForCountAndPage() {
|
||||
when(packingPlans.countPlatform(null, "PLAN-1", "C1", "W1", "SKU1")).thenReturn(1L);
|
||||
@@ -124,6 +149,17 @@ class ReturnInventoryServiceTest {
|
||||
assertThat(capture.getValue().getResultJson()).contains("RC-BOX", "RR-BOX", "商品A", "\"quantity\":3", "\"quantity\":7");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parcelSigningStoresLooseSkuSourceWithoutGeneratingCartonNumberOrIncreasingBoxCount() {
|
||||
var receipt = new ReturnReceipt(); receipt.setId(21L); receipt.setUnitType("PARCEL"); receipt.setUnitCode("PK-1");
|
||||
var source = service.createReceiptCarton(completedOrder(), receipt, null,
|
||||
List.of(new ReturnInventoryService.ReceiptLine("A", "商品A", 3)));
|
||||
assertThat(source.getCartonMark()).startsWith("SP");
|
||||
assertThat(source.getSourceUnitType()).isEqualTo("PARCEL");
|
||||
verifyNoInteractions(numbers, operations);
|
||||
verify(balances).adjust(anyLong(), anyString(), anyString(), eq("A"), eq("商品A"), eq(3L), eq(3L), eq(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonDetailsExposeImmutableMergeHistoryWithinCustomerScope() {
|
||||
ReturnInventoryCarton carton = new ReturnInventoryCarton();
|
||||
@@ -320,7 +356,7 @@ class ReturnInventoryServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullyConsumedSourceIsMergedAndSkuCartonCountRemainsOne() {
|
||||
void fullyConsumedParcelSourceIsMergedAndCreatesOnePhysicalCarton() {
|
||||
ReturnInventoryCarton locked = prepareConcurrentSource();
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 2L)));
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-TARGET");
|
||||
@@ -330,7 +366,7 @@ class ReturnInventoryServiceTest {
|
||||
assertThat(locked.getSkuTypes()).isZero();
|
||||
assertThat(locked.getStatus()).isEqualTo(ReturnInventoryService.MERGED);
|
||||
verify(cartonItems).deleteById(41L);
|
||||
verify(balances).adjustCartonQuantity("10001", "42", "SKU-A", 0L);
|
||||
verify(balances).adjustCartonQuantity("10001", "42", "SKU-A", 1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+24
@@ -433,6 +433,30 @@ class ReturnReceiptServiceTest {
|
||||
verify(receipts, times(1)).insert(any(ReturnReceipt.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parcelWithoutTrackingGeneratesParcelCodeAndNeverReturnsBoxLabelsOnReplay() {
|
||||
ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PENDING_RECEIPT, 3, 0);
|
||||
ReturnOrderItem line = item(11L, "SKU-A", 3, 0);
|
||||
stubNewReceipt(order, List.of(line), "SP-SOURCE");
|
||||
var request = new PdaReturnReceiptRequests.Confirm("RT-1", null, "SKU", null,
|
||||
List.of(new PdaReturnReceiptRequests.Item("SKU-A", 1)), "parcel-key",
|
||||
"PARCEL", null, null, null, null, null, null);
|
||||
var result = service.confirm(identity, request);
|
||||
var capture = ArgumentCaptor.forClass(ReturnReceipt.class);
|
||||
verify(receipts).insert(capture.capture());
|
||||
assertThat(result.unitCode()).startsWith("PK");
|
||||
assertThat(result.cartonMark()).isNull();
|
||||
assertThat(result.printableHtml()).isNull();
|
||||
assertThat(result.labels()).isEmpty();
|
||||
assertThat(capture.getValue().getTrackingNo()).isNull();
|
||||
when(receipts.lockKey("operator-1", "parcel-key")).thenReturn(capture.getValue());
|
||||
var replay = service.confirm(identity, request);
|
||||
assertThat(replay.unitCode()).isEqualTo(result.unitCode());
|
||||
assertThat(replay.labels()).isEmpty();
|
||||
verify(inventory, never()).label(anyString(), anyString());
|
||||
verify(inventory, times(1)).createReceiptCarton(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOverReceiptBeforeCreatingReceiptInventoryOrBatch() {
|
||||
ReturnOrder order = order("RT-1", "10", ReturnReceiptService.PARTIALLY_RECEIVED, 2, 1);
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
`procurement-sample-test-env.mjs` 是采购样品正常全链路 TEST 专项入口,默认或 `--plan` 零网络。真实执行使用 `node --env-file="<授权目录>/test-acceptance.env" scripts/procurement-sample-test-env.mjs --execute --environment TEST --run-id <唯一短批次> --warehouse-id <已核对有效海外收货仓>`。只允许当前 TEST 工作区及指定配置;由正式 API 创建独立客户并完成资质,人工充值10元,上传凭证/面单,调用内部 `procurement-sample-fulfillment.mjs` 完成两箱正常履约。充值、文件和采购请求前落盘,结果未知停写;报告独占 `reports/procurement-sample-<批次>/report.json`,不覆盖旧报告,不自动重跑,不执行 SQL/部署/删除。结束退出自身平台与客户会话,保留样本。通过只表示该正常路径,不替代异常、退款、权限、真实页面和请求量验收。离线入口合同为 `procurement-sample-test-env.test.mjs`;内部模块安全合同见模块索引。
|
||||
|
||||
|
||||
`db/return-single-sku-quantity-incremental.sql`:MySQL 8已有尾程库的单SKU按件数出库增量,显式 `USE baiyue_lastleg`。新增明细件数快照,流水箱差允许0或1,同箱索引改为普通索引,保留拣货ID+SKU唯一防重和库存守恒约束;可重复执行,无凭据,不修改业务行。使用外部明确数据库连接,失败停止,保留DDL与错误,修复后重跑;执行后核对字段、CHECK和索引。
|
||||
|
||||
`db/overseas-business-acceptance-incremental-20261009.sql`:本轮海外业务本地验收已有库增量汇总,显式指向 `baiyue_lastleg` 和 `baiyue_iam`。合并四流程结构、退货合箱/合托、换标排车、箱内清点及会话、单SKU部分出库;来源文件列在每段头部,无SOURCE依赖,可重复执行。包含正式平台管理员作业权限补齐,以及条件限定的历史待清点和关联单号快照修正;不改库存件数、不强制推进履约、不删除业务数据、不生成测试资源。数据库实例和凭据由外部连接明确提供,MySQL8工具执行整份文件;遇错立即停止并保留DDL与原错误,核对后重跑,不使用--force。执行记录和结构回查为报告,未自动在TEST或生产执行。
|
||||
|
||||
|
||||
`db/local-four-flows-schema-incremental-20261009.sql`:本地退货换标、仓储转运、贴标出库及退货出库结构比对所得的 MySQL 8 定向增量。显式 `USE baiyue_lastleg`,补齐预约结束时间/版本、转运换标来源绑定、分配 SKU 快照及转运收货幂等命令表;字段按存在性判断,表使用 `CREATE TABLE IF NOT EXISTS`,可重复执行,不删除或回填业务数据。使用操作者显式选择的本地数据库连接,无内置凭据;完整执行输出 `LOCAL_FLOW_SCHEMA_READY`。失败保留已完成的 DDL 与原错误,核对后重跑;不自动清理业务记录,不是全库迁移或生产验收入口。
|
||||
|
||||
`db/return-tail-order-number-incremental-20261009.sql` 将已验证关联的退货尾程单号及退货单尾程号快照统一为退货单号。显式 `USE baiyue_lastleg`,可重复执行,只更新同客户、关联订单 ID 和来源退货号均一致的 `RETURN_RELABEL` 单据;不改变业务 ID、库存或履约状态。执行前核对目标数据库,执行后核验查询应为空,异常关系保留供核实。已同步海外业务统一增量脚本。
|
||||
|
||||
@@ -979,7 +979,7 @@ SET b.inbound_order_id=COALESCE(b.inbound_order_id,b.id),
|
||||
b.inbound_code=COALESCE(NULLIF(b.inbound_code,''),NULLIF(l.inbound_code,''),CONCAT('LEGACY-',b.id))
|
||||
WHERE b.inbound_order_id IS NULL OR b.inbound_code IS NULL OR b.inbound_code='';
|
||||
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inventory_balance' AND column_name='sku_code'), 'ALTER TABLE ll_inventory_balance MODIFY COLUMN sku_code VARCHAR(128) NULL', 'SELECT 1'));
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inventory_balance' AND column_name='sku_code'), 'ALTER TABLE ll_inventory_balance MODIFY COLUMN sku_code VARCHAR(128) NULL', return_sku_quantity BIGINT NULL COMMENT '退货单SKU本来源计划出库件数快照,NULL为历史整箱出库', 'SELECT 1'));
|
||||
PREPARE inventory_v2_stmt FROM @ddl; EXECUTE inventory_v2_stmt; DEALLOCATE PREPARE inventory_v2_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inventory_balance' AND column_name='inbound_order_id'), 'ALTER TABLE ll_inventory_balance MODIFY COLUMN inbound_order_id BIGINT NOT NULL', 'SELECT 1'));
|
||||
PREPARE inventory_v2_stmt FROM @ddl; EXECUTE inventory_v2_stmt; DEALLOCATE PREPARE inventory_v2_stmt;
|
||||
@@ -4439,11 +4439,11 @@ CREATE TABLE IF NOT EXISTS ll_return_inventory_outbound_movement (
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '出库记账时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_return_outbound_pick_sku(outbound_pick_id,sku_code),
|
||||
UNIQUE KEY uk_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code),
|
||||
KEY idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code),
|
||||
KEY idx_ll_return_outbound_movement_query(customer_id,overseas_warehouse_id,sku_code,occurred_at),
|
||||
CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0
|
||||
AND total_before-total_after=quantity AND available_before-available_after=quantity
|
||||
AND cartons_after>=0 AND cartons_before-cartons_after=1 AND total_after>=available_after)
|
||||
AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)
|
||||
) ENGINE=InnoDB COMMENT='不可变退货出库SKU库存流水';
|
||||
|
||||
-- 原生退货换标排车:保留独立退货箱来源,不伪造普通库存仓位。
|
||||
@@ -5031,3 +5031,26 @@ CREATE TABLE IF NOT EXISTS ll_fleet_trip_cost_history (
|
||||
UNIQUE KEY uk_fleet_trip_cost_history(cost_id,record_version),
|
||||
CONSTRAINT fk_fleet_trip_cost_history FOREIGN KEY(cost_id) REFERENCES ll_fleet_trip_cost(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车次成本不可变更正历史';
|
||||
|
||||
-- 退货单 SKU 按件数出库快照。目标库:baiyue_lastleg。可重复执行。
|
||||
|
||||
SET @return_sku_quantity_ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_outbound_pallet_item' AND column_name='return_sku_quantity'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE ll_outbound_pallet_item ADD COLUMN return_sku_quantity BIGINT NULL COMMENT ''退货单SKU本来源计划出库件数快照,NULL为历史整箱出库'' AFTER sku_code'
|
||||
);
|
||||
PREPARE return_sku_quantity_stmt FROM @return_sku_quantity_ddl;
|
||||
EXECUTE return_sku_quantity_stmt;
|
||||
DEALLOCATE PREPARE return_sku_quantity_stmt;
|
||||
|
||||
-- 部分出库仅扣件数;整箱扣一箱,保留守恒和非负约束。
|
||||
SET @return_sku_check_ddl = IF(EXISTS(SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND constraint_name='chk_ll_return_outbound_movement_delta'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP CHECK chk_ll_return_outbound_movement_delta, ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)', 'ALTER TABLE ll_return_inventory_outbound_movement ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)');
|
||||
PREPARE return_sku_check_stmt FROM @return_sku_check_ddl;
|
||||
EXECUTE return_sku_check_stmt;
|
||||
DEALLOCATE PREPARE return_sku_check_stmt;
|
||||
-- 同箱余数允许再次出库,防重仍由拣货ID+SKU唯一索引保证。
|
||||
SET @return_sku_index_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND index_name='uk_ll_return_outbound_carton_sku'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP INDEX uk_ll_return_outbound_carton_sku, ADD INDEX idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code)', 'SELECT 1');
|
||||
PREPARE return_sku_index_stmt FROM @return_sku_index_ddl;
|
||||
EXECUTE return_sku_index_stmt;
|
||||
DEALLOCATE PREPARE return_sku_index_stmt;
|
||||
|
||||
@@ -0,0 +1,956 @@
|
||||
-- 2026-10-09 海外业务本地验收增量汇总;MySQL 8.0.16+;已有业务库升级。
|
||||
-- 显式目标:baiyue_lastleg(业务结构)及 baiyue_iam(平台管理员正式作业权限)。
|
||||
-- 独立文件,无 SOURCE 依赖;在对应实例的数据库工具中执行整个文件,出错停止,不使用 --force。
|
||||
-- 包含四流程缺列/表、合箱合托及退货换标排车、清点会话、单SKU全部/部分出库。
|
||||
-- 不强制完结单据,不修改库存数量,不生成验收单据,不删除业务数据。
|
||||
-- 包含两项限定历史修正:符合条件的历史托盘批次标记待清点;关联一致的退货尾程单号对齐。
|
||||
-- IAM只补正式平台管理员权限;数据库凭据从外部连接提供。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
|
||||
-- 来源:test-lastleg-relabel-return-transfer-columns.sql(复用结构定义,目标连接由本汇总脚本操作者指定)
|
||||
-- 本地与 test 结构比对(2026-09-20):仅补 test 缺失的贴标/退货换标字段。
|
||||
-- 本段只作用于已选择实例的尾程业务库。
|
||||
-- MySQL 8;可重复执行。DDL 会隐式提交,遇错应停止执行,不使用 mysql --force。
|
||||
USE `baiyue_lastleg`;
|
||||
|
||||
-- 贴标 PDA:一次 SKU 扫描可登记多件;旧记录保持默认数量 1。
|
||||
SET @ddl = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_relabel_pda_unit_fact' AND column_name = 'quantity'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `ll_relabel_pda_unit_fact` ADD COLUMN `quantity` INT NOT NULL DEFAULT 1 COMMENT ''本次事实处理件数,箱级作业固定为1'' AFTER `reason`'
|
||||
);
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 退货签收和库存箱记录来源托盘。
|
||||
SET @ddl = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_return_receipt' AND column_name = 'pallet_code'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `ll_return_receipt` ADD COLUMN `pallet_code` VARCHAR(128) NULL COMMENT ''本次签收绑定的托盘码'' AFTER `receipt_batch_no`'
|
||||
);
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_return_inventory_carton' AND column_name = 'pallet_code'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `ll_return_inventory_carton` ADD COLUMN `pallet_code` VARCHAR(128) NULL COMMENT ''签收时绑定的托盘码'' AFTER `source_return_no`'
|
||||
);
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 退货单手动差异完结的审计字段。
|
||||
SET @ddl = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_return_order' AND column_name = 'discrepancy_reason'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `ll_return_order` ADD COLUMN `discrepancy_reason` VARCHAR(500) NULL COMMENT ''手动完结差异原因'' AFTER `remark`'
|
||||
);
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_return_order' AND column_name = 'manually_completed_at'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `ll_return_order` ADD COLUMN `manually_completed_at` DATETIME(3) NULL COMMENT ''手动差异完结时间'' AFTER `discrepancy_reason`'
|
||||
);
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 预期返回 5;低于 5 表示本次补齐不完整。
|
||||
SELECT DATABASE() AS target_schema, COUNT(*) AS aligned_column_count
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND (table_name = 'll_relabel_pda_unit_fact' AND column_name = 'quantity'
|
||||
OR table_name = 'll_return_receipt' AND column_name = 'pallet_code'
|
||||
OR table_name = 'll_return_inventory_carton' AND column_name = 'pallet_code'
|
||||
OR table_name = 'll_return_order' AND column_name IN ('discrepancy_reason', 'manually_completed_at'));
|
||||
|
||||
|
||||
-- 来源:return-receipt-box-count-incremental-20260929.sql(复用结构定义,目标连接由本汇总脚本操作者指定)
|
||||
-- 2026-09-29 海外退货签收 SKU 箱数增量
|
||||
-- MySQL 8.0+;目标数据库:baiyue_lastleg
|
||||
-- 可重复执行,不修改既有业务数据。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_lastleg;
|
||||
|
||||
SET @ddl = IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'll_return_receipt_item'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_return_receipt_item'
|
||||
AND column_name = 'box_count'
|
||||
),
|
||||
'ALTER TABLE ll_return_receipt_item ADD COLUMN box_count BIGINT NULL COMMENT ''本次 SKU 对应箱数'' AFTER received_quantity',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT DATABASE() AS target_database, 'll_return_receipt_item.box_count' AS updated_column;
|
||||
|
||||
|
||||
-- 来源:return-outbound-box-nullable-20260929.sql(复用结构定义,目标连接由本汇总脚本操作者指定)
|
||||
-- MySQL 8.0+; target database: baiyue_lastleg.
|
||||
-- Re-runnable: allow pallet-less carton outbound records.
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_lastleg;
|
||||
|
||||
SET @ddl = (SELECT IF(EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item'
|
||||
AND column_name='pallet_no' AND is_nullable='NO'
|
||||
), 'ALTER TABLE ll_outbound_pallet_item MODIFY COLUMN pallet_no VARCHAR(128) NULL COMMENT ''托盘号,箱维度出库为空''', 'SELECT 1'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT DATABASE() AS target_database, table_name, column_name, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='pallet_no';
|
||||
|
||||
-- 来源:local-four-flows-schema-incremental-20261009.sql
|
||||
-- 本地四流程结构补齐;目标 baiyue_lastleg;MySQL 8;不删除或回填业务数据。
|
||||
USE `baiyue_lastleg`;
|
||||
SET NAMES utf8mb4;
|
||||
DROP PROCEDURE IF EXISTS ensure_local_flow_column;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE ensure_local_flow_column(IN t VARCHAR(64), IN n VARCHAR(64), IN d TEXT)
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=t AND column_name=n) THEN
|
||||
SET @local_flow_ddl=CONCAT('ALTER TABLE `',t,'` ADD COLUMN `',n,'` ',d);
|
||||
PREPARE local_flow_stmt FROM @local_flow_ddl; EXECUTE local_flow_stmt; DEALLOCATE PREPARE local_flow_stmt;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL ensure_local_flow_column('ll_inbound_fba_appointment','appointment_end_at','DATETIME(3) NULL COMMENT ''预约结束时间,未明确时为空''');
|
||||
|
||||
CALL ensure_local_flow_column('ll_inbound_fba_appointment','version','INT NOT NULL DEFAULT 0 COMMENT ''并发版本''');
|
||||
|
||||
CALL ensure_local_flow_column('ll_warehouse_transfer_relabel_binding','source_allocation_id','BIGINT NULL COMMENT ''已有库存转运的原始来源分配ID''');
|
||||
|
||||
CALL ensure_local_flow_column('ll_warehouse_transfer_relabel_binding','relabel_item_id','BIGINT NULL COMMENT ''本票关联换标明细ID''');
|
||||
|
||||
CALL ensure_local_flow_column('ll_warehouse_transfer_inventory_allocation','sku_code','VARCHAR(128) NULL COMMENT ''本次来源分配的实际SKU快照''');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_receipt_command (
|
||||
id BIGINT NOT NULL COMMENT '收货命令主键',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户标识',
|
||||
transfer_order_id BIGINT NOT NULL COMMENT '转运单标识',
|
||||
source_inventory_position_id BIGINT NOT NULL COMMENT '本次收货来源库存位置',
|
||||
idempotency_key VARCHAR(255) NOT NULL COMMENT '原始收货幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '完整原始收货请求摘要',
|
||||
request_json JSON NOT NULL COMMENT '完整原始收货请求快照',
|
||||
target_warehouse_id BIGINT NOT NULL COMMENT '实际目标海外收货仓',
|
||||
target_pallet_id BIGINT NOT NULL COMMENT '目标实物托盘',
|
||||
target_location_id BIGINT NOT NULL COMMENT '目标实际库位',
|
||||
target_hash CHAR(64) NOT NULL COMMENT '共享目标资源绑定摘要',
|
||||
target_json JSON NOT NULL COMMENT '权威目标仓托盘库位及目的仓快照',
|
||||
reservation_key VARCHAR(128) NOT NULL COMMENT '同转运同目标托盘共享资源占用键',
|
||||
resource_lock_id BIGINT NULL COMMENT '已确认有效的海外资源锁标识',
|
||||
status VARCHAR(32) NOT NULL COMMENT '命令状态PREPARED准备完成RESERVED资源占用COMPLETED收货完成',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '命令创建时间',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '命令最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ll_wh_receipt_cmd_source (transfer_order_id, source_inventory_position_id),
|
||||
UNIQUE KEY uk_ll_wh_receipt_cmd_key (customer_id, idempotency_key),
|
||||
KEY idx_ll_wh_receipt_cmd_target (customer_id, transfer_order_id, target_pallet_id),
|
||||
CONSTRAINT ck_ll_wh_receipt_cmd_status CHECK (status IN ('PREPARED','RESERVED','COMPLETED'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转运逐来源收货原请求与共享目标资源占用命令';
|
||||
DROP PROCEDURE IF EXISTS ensure_local_flow_column;
|
||||
SELECT 'LOCAL_FLOW_SCHEMA_READY' AS migration_status, DATABASE() AS target_database;
|
||||
|
||||
|
||||
-- 来源:return-packing-incremental-20261008.sql
|
||||
-- 2026-10-08 退货合箱精确预占、来源追溯与箱级上架;MySQL 8,可重复执行。
|
||||
SET NAMES utf8mb4;
|
||||
-- IAM:平台退货合箱写操作需要正式库存管理权限;仅授予 PLATFORM_ADMIN。
|
||||
USE baiyue_iam;
|
||||
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, deleted, created_at, updated_at)
|
||||
VALUES (5107044, 0, 'PLATFORM', 'BUTTON', 5107040, 'lastleg:inventory:manage', '后台退货库存管理', NULL, 'POST', 744, 'ACTIVE', 1, 0, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3))
|
||||
ON DUPLICATE KEY UPDATE permission_type=VALUES(permission_type), parent_id=VALUES(parent_id), permission_name=VALUES(permission_name), http_method=VALUES(http_method), sort_no=VALUES(sort_no), status='ACTIVE', built_in=1, deleted=0;
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, CURRENT_TIMESTAMP(3)
|
||||
FROM iam_role role JOIN iam_permission permission ON permission.tenant_id=0 AND permission.id=5107044
|
||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='PLATFORM_ADMIN'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0
|
||||
AND NOT EXISTS(SELECT 1 FROM iam_role_permission existing WHERE existing.role_id=role.id AND existing.permission_id=permission.id);
|
||||
USE baiyue_lastleg;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND index_name='uk_ll_return_inventory_carton_receipt'), 'ALTER TABLE ll_return_inventory_carton DROP INDEX uk_ll_return_inventory_carton_receipt', 'SELECT 1'));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='unit_type'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN unit_type VARCHAR(16) NULL COMMENT ''签收类型 PALLET/BOX/PARCEL'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='unit_code'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN unit_code VARCHAR(128) NULL COMMENT ''签收单元业务码'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='total_box_count'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN total_box_count BIGINT NULL COMMENT ''签收SKU箱数汇总'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='length'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN length DECIMAL(12,3) NULL COMMENT ''实测尺寸cm'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='width'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN width DECIMAL(12,3) NULL COMMENT ''实测尺寸cm'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='height'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN height DECIMAL(12,3) NULL COMMENT ''实测尺寸cm'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='weight'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN weight DECIMAL(12,3) NULL COMMENT ''实测重量kg'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt_item' AND column_name='box_count'), 'SELECT 1', 'ALTER TABLE ll_return_receipt_item ADD COLUMN box_count BIGINT NULL COMMENT ''SKU签收箱数'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_unit_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_unit_code VARCHAR(128) NULL COMMENT ''来源单元业务码'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
CREATE TABLE IF NOT EXISTS ll_return_packing_plan (
|
||||
id BIGINT NOT NULL COMMENT '合箱计划ID', plan_no VARCHAR(128) NOT NULL COMMENT '计划业务号',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID',
|
||||
return_nos_json JSON NOT NULL COMMENT '来源退货单范围', boxes_json JSON NOT NULL COMMENT '目标箱明细',
|
||||
mode VARCHAR(32) NOT NULL COMMENT 'SKU散件或BOX箱合并', status VARCHAR(32) NOT NULL COMMENT 'PENDING_REVIEW待审 READY已推送 COMPLETED完成 CANCELLED取消',
|
||||
remark VARCHAR(1000) NULL COMMENT '合箱备注', idempotency_key VARCHAR(255) NOT NULL COMMENT '创建幂等键',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '更新时间',
|
||||
pushed_at DATETIME(3) NULL COMMENT '审核推送时间', completed_at DATETIME(3) NULL COMMENT '作业完成时间',
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_packing_plan_no(plan_no),
|
||||
UNIQUE KEY uk_ll_return_packing_plan_key(customer_id,idempotency_key),
|
||||
KEY idx_ll_return_packing_plan_warehouse(overseas_warehouse_id,status,pushed_at)
|
||||
) ENGINE=InnoDB COMMENT='退货合箱计划';
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton_item' AND column_name='packing_reserved_quantity'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton_item ADD COLUMN packing_reserved_quantity BIGINT NOT NULL DEFAULT 0 COMMENT ''合箱计划预占件数'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_packing_plan' AND column_name='source_items_json'), 'SELECT 1', 'ALTER TABLE ll_return_packing_plan ADD COLUMN source_items_json JSON NULL COMMENT ''精确来源箱和SKU锁定数量'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_packing_plan' AND column_name='result_marks_json'), 'SELECT 1', 'ALTER TABLE ll_return_packing_plan ADD COLUMN result_marks_json JSON NULL COMMENT ''完成生成或并入的目标箱唛'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_packing_plan' AND column_name='request_hash'), 'SELECT 1', 'ALTER TABLE ll_return_packing_plan ADD COLUMN request_hash VARCHAR(64) NULL COMMENT ''创建载荷摘要'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_packing_plan' AND column_name='completion_hash'), 'SELECT 1', 'ALTER TABLE ll_return_packing_plan ADD COLUMN completion_hash VARCHAR(64) NULL COMMENT ''完成载荷摘要'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='location_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN location_code VARCHAR(128) NULL COMMENT ''退货箱当前库位'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='putaway_at'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN putaway_at DATETIME(3) NULL COMMENT ''退货箱上架时间'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_unit_type'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_unit_type VARCHAR(32) NULL COMMENT ''PALLET或BOX实物箱 PARCEL散SKU'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1'));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_status'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_status', 'SELECT 1'));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','PACKING_RESERVED','RELABEL_PENDING','OUTBOUND_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0));
|
||||
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','PACKING_RESERVED','RELABEL_PENDING','OUTBOUND_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED'));
|
||||
CREATE TABLE IF NOT EXISTS ll_return_packing_trace (
|
||||
id BIGINT NOT NULL COMMENT '合箱追溯ID', customer_id VARCHAR(128) NOT NULL COMMENT '原客户隔离键',
|
||||
operation_id BIGINT NOT NULL COMMENT '合箱操作ID', source_carton_mark VARCHAR(128) NOT NULL COMMENT '来源箱唛 空值表示目标箱原有库存快照',
|
||||
target_carton_mark VARCHAR(128) NOT NULL COMMENT '目标箱唛', return_no VARCHAR(128) NOT NULL COMMENT '原始退货单号',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT '商品SKU编码', quantity BIGINT NOT NULL COMMENT '本次移动件数',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '发生时间', PRIMARY KEY(id),
|
||||
KEY idx_return_packing_trace_order(customer_id,return_no,created_at),
|
||||
KEY idx_return_packing_trace_target(customer_id,target_carton_mark,sku_code),
|
||||
KEY idx_return_packing_trace_source(customer_id,source_carton_mark,sku_code),
|
||||
CONSTRAINT chk_return_packing_trace_quantity CHECK(quantity>0)
|
||||
) ENGINE=InnoDB COMMENT='退货合箱逐SKU原始退货单不可变追溯';
|
||||
ALTER TABLE ll_return_packing_trace MODIFY COLUMN sku_code VARCHAR(128) NOT NULL COMMENT '商品SKU编码';
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_operation' AND column_name='result_json'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_operation ADD COLUMN result_json JSON NULL COMMENT ''幂等操作不可变返回快照'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
SET @return_packing_ddl = (SELECT IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_packing_plan' AND column_name='result_labels_json'), 'SELECT 1', 'ALTER TABLE ll_return_packing_plan ADD COLUMN result_labels_json JSON NULL COMMENT ''计划完工标签快照'''));
|
||||
PREPARE return_packing_stmt FROM @return_packing_ddl; EXECUTE return_packing_stmt; DEALLOCATE PREPARE return_packing_stmt;
|
||||
-- Existing outbound tables need this field even when created before the fee snapshot baseline.
|
||||
USE baiyue_lastleg;
|
||||
SET @return_outbound_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='fee_coverage_json'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN fee_coverage_json JSON NULL COMMENT ''发运原费用及服务覆盖确认快照''');
|
||||
PREPARE return_outbound_stmt FROM @return_outbound_ddl;
|
||||
EXECUTE return_outbound_stmt;
|
||||
DEALLOCATE PREPARE return_outbound_stmt;
|
||||
USE baiyue_lastleg;
|
||||
CREATE TABLE IF NOT EXISTS ll_return_inventory_outbound_movement (
|
||||
id BIGINT NOT NULL COMMENT '退货出库SKU流水ID',
|
||||
outbound_pick_id BIGINT NOT NULL COMMENT '同事务PDA出库拣货记录ID',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '库存所属客户ID和隔离键',
|
||||
overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '实际出库海外仓ID',
|
||||
carton_id BIGINT NOT NULL COMMENT '真实退货库存箱ID',
|
||||
carton_mark VARCHAR(128) NOT NULL COMMENT '冻结的真实退货箱业务码',
|
||||
outbound_no VARCHAR(64) NOT NULL COMMENT '本次退货出库业务单号',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT '本次出库SKU编码快照',
|
||||
product_name_zh VARCHAR(255) NULL COMMENT '中文品名快照',
|
||||
quantity BIGINT NOT NULL COMMENT '本箱该SKU实际出库件数,始终为正',
|
||||
total_before BIGINT NOT NULL COMMENT '客户仓SKU出库前实物总数量',
|
||||
total_after BIGINT NOT NULL COMMENT '客户仓SKU出库后实物总数量',
|
||||
available_before BIGINT NOT NULL COMMENT '客户仓SKU出库前可用数量',
|
||||
available_after BIGINT NOT NULL COMMENT '客户仓SKU出库后可用数量',
|
||||
cartons_before BIGINT NOT NULL COMMENT '客户仓包含该SKU的出库前可用箱数',
|
||||
cartons_after BIGINT NOT NULL COMMENT '客户仓包含该SKU的出库后可用箱数',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '出库记账时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_return_outbound_pick_sku(outbound_pick_id,sku_code),
|
||||
UNIQUE KEY uk_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code),
|
||||
KEY idx_ll_return_outbound_movement_query(customer_id,overseas_warehouse_id,sku_code,occurred_at),
|
||||
CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0
|
||||
AND total_before-total_after=quantity AND available_before-available_after=quantity
|
||||
AND cartons_after>=0 AND cartons_before-cartons_after=1 AND total_after>=available_after)
|
||||
) ENGINE=InnoDB COMMENT='不可变退货出库SKU库存流水';
|
||||
|
||||
-- 原生退货换标排车:保留独立退货箱来源,不伪造普通库存仓位。
|
||||
USE baiyue_lastleg;
|
||||
SET @return_dispatch_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pda_pick' AND column_name='return_carton_id')=0,
|
||||
'ALTER TABLE ll_dispatch_pda_pick ADD COLUMN return_carton_id BIGINT NULL COMMENT ''独立退货库存箱ID,与普通仓位互斥''', 'SELECT 1');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
ALTER TABLE ll_dispatch_pda_pick MODIFY COLUMN inventory_position_id BIGINT NULL COMMENT '普通库存仓位记录ID';
|
||||
CREATE TABLE IF NOT EXISTS ll_return_stock_receipt (
|
||||
id BIGINT NOT NULL COMMENT '退回入仓实收凭据ID',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户数据归属',
|
||||
receipt_no VARCHAR(64) NOT NULL COMMENT '退回实收业务编号',
|
||||
inventory_ledger_id BIGINT NOT NULL COMMENT '唯一实际退回入账流水',
|
||||
exception_order_id BIGINT NOT NULL COMMENT '退回异常订单',
|
||||
order_id BIGINT NOT NULL COMMENT '原尾程订单',
|
||||
inbound_order_id BIGINT NOT NULL COMMENT '原入仓单',
|
||||
source_outbound_no VARCHAR(64) NOT NULL COMMENT '原发运出库业务编号',
|
||||
source_outbound_pallet_item_id BIGINT NOT NULL COMMENT '原出库托盘明细',
|
||||
source_inventory_position_id BIGINT NOT NULL COMMENT '原拣货库存位置',
|
||||
inventory_balance_id BIGINT NOT NULL COMMENT '实际退回库存余额',
|
||||
inventory_position_id BIGINT NOT NULL COMMENT '实际退回库存位置',
|
||||
received_packages BIGINT NOT NULL COMMENT '实际退回箱数,不增加原订单箱数',
|
||||
received_at DATETIME(3) NOT NULL COMMENT '实际退回入账时间',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '凭据记录时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_return_stock_receipt_no(customer_id,receipt_no),
|
||||
UNIQUE KEY uk_return_stock_receipt_ledger(customer_id,inventory_ledger_id),
|
||||
KEY idx_return_stock_receipt_order(customer_id,order_id),
|
||||
KEY idx_return_stock_receipt_position(customer_id,inventory_balance_id,inventory_position_id),
|
||||
CONSTRAINT chk_return_stock_receipt_packages CHECK(received_packages>0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='真实退回入仓及再次发运来源凭据';
|
||||
ALTER TABLE ll_dispatch_pda_pick_order MODIFY COLUMN receipt_id BIGINT NULL COMMENT '原入仓实收来源,与退回来源或原生退货箱互斥';
|
||||
-- 真实退回凭据与尾程订单客户键使用同一排序规则,避免相关库存子查询冲突。
|
||||
SET @return_customer_collation = (SELECT collation_name FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='customer_id');
|
||||
SET @return_dispatch_ddl = CONCAT('ALTER TABLE ll_return_stock_receipt MODIFY COLUMN customer_id VARCHAR(128) CHARACTER SET utf8mb4 COLLATE ', @return_customer_collation, ' NOT NULL COMMENT ''客户数据归属''');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
SET @return_dispatch_ddl = CONCAT('ALTER TABLE ll_return_stock_receipt MODIFY COLUMN source_outbound_no VARCHAR(64) CHARACTER SET utf8mb4 COLLATE ', @return_customer_collation, ' NOT NULL COMMENT ''原发运出库业务编号''');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
SET @return_dispatch_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pda_pick_order' AND column_name='return_carton_id')=0,
|
||||
'ALTER TABLE ll_dispatch_pda_pick_order ADD COLUMN return_carton_id BIGINT NULL COMMENT ''原生退货箱来源ID,与实收来源互斥''', 'SELECT 1');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
SET @return_dispatch_ddl = IF((SELECT COUNT(*) FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_dispatch_pda_pick_order' AND constraint_name='chk_dispatch_pick_receipt_source')>0,
|
||||
'ALTER TABLE ll_dispatch_pda_pick_order DROP CHECK chk_dispatch_pick_receipt_source', 'SELECT 1');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
SET @return_dispatch_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pda_pick_order' AND column_name='return_receipt_id')=0,
|
||||
'ALTER TABLE ll_dispatch_pda_pick_order ADD COLUMN return_receipt_id BIGINT NULL COMMENT ''明确选用的真实退回入仓来源''', 'SELECT 1');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
ALTER TABLE ll_dispatch_pda_pick_order ADD CONSTRAINT chk_dispatch_pick_receipt_source CHECK((receipt_id IS NOT NULL) + (return_receipt_id IS NOT NULL) + (return_carton_id IS NOT NULL) = 1);
|
||||
SET @return_dispatch_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='signed_packages')=0,
|
||||
'ALTER TABLE ll_delivery_order ADD COLUMN signed_packages INT NULL COMMENT ''保存签收资料时冻结的实际签收箱数,不含此前已退回量''', 'SELECT 1');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
CREATE TABLE IF NOT EXISTS event_delivery_attempt (
|
||||
attempt_id VARCHAR(36) NOT NULL COMMENT '本次投递租约UUID和审计主键',
|
||||
service_name VARCHAR(32) NOT NULL COMMENT '本库所属生产服务',
|
||||
event_type VARCHAR(96) NOT NULL COMMENT '白名单业务事件类型',
|
||||
entry_id VARCHAR(128) NOT NULL COMMENT '生产者发件箱主键',
|
||||
event_id VARCHAR(128) NULL COMMENT '合法原事件UUID或ULID;格式错误时为空',
|
||||
target_name VARCHAR(64) NOT NULL COMMENT '固定目标业务服务',
|
||||
started_at DATETIME(3) NOT NULL COMMENT '领取及审计事务提交前的开始时间',
|
||||
finished_at DATETIME(3) NULL COMMENT '尝试完成或租约过期恢复时间',
|
||||
duration_ms BIGINT NULL COMMENT '尝试耗时毫秒',
|
||||
outcome VARCHAR(24) NOT NULL COMMENT 'STARTED/APPLIED/DUPLICATE/STALE/RETRY/DEAD/LEASE_EXPIRED',
|
||||
error_code VARCHAR(64) NULL COMMENT '有限错误类别或HTTP状态;不含载荷与令牌',
|
||||
next_retry_at DATETIME(3) NULL COMMENT '本次失败后下次计划投递时间',
|
||||
PRIMARY KEY (attempt_id),
|
||||
KEY idx_delivery_attempt_entry (service_name,entry_id,started_at),
|
||||
KEY idx_delivery_attempt_outcome (outcome,started_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本服务业务事件逐次投递审计';
|
||||
|
||||
-- 补齐排车实体引用的真实退回来源字段;原生退货箱单独使用 return_carton_id。
|
||||
USE baiyue_lastleg;
|
||||
SET @return_dispatch_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='return_receipt_id')=0,
|
||||
'ALTER TABLE ll_dispatch_item ADD COLUMN return_receipt_id BIGINT NULL COMMENT ''明确选用的真实退回入仓来源''', 'SELECT 1');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
SET @return_dispatch_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pallet_lock' AND column_name='return_receipt_id')=0,
|
||||
'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN return_receipt_id BIGINT NULL COMMENT ''明确选用的真实退回入仓来源''', 'SELECT 1');
|
||||
PREPARE return_dispatch_stmt FROM @return_dispatch_ddl;
|
||||
EXECUTE return_dispatch_stmt;
|
||||
DEALLOCATE PREPARE return_dispatch_stmt;
|
||||
|
||||
|
||||
-- 来源:return-relabel-truck-incremental-20261008.sql
|
||||
-- 退货换标排车使用正式车次域,补齐旧本地库缺少的车队结构。
|
||||
-- MySQL 8;显式 baiyue_lastleg;不写业务数据,可重复执行。
|
||||
-- IAM:退货换标真实排车需要平台管理员的正式司机/车队权限;不授予其他角色。
|
||||
USE baiyue_iam;
|
||||
INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, deleted, created_at, updated_at)
|
||||
VALUES
|
||||
(5107300,0,'PLATFORM','MENU',0,'platform:overseas-master-data:driver:list','司机管理','/overseas-master-data/drivers','GET',930,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107301,0,'PLATFORM','BUTTON',5107300,'platform:overseas-master-data:driver:create','新增司机',NULL,'POST',931,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107302,0,'PLATFORM','BUTTON',5107300,'platform:overseas-master-data:driver:edit','编辑与重提司机档案',NULL,'PUT',932,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107304,0,'PLATFORM','BUTTON',5107300,'platform:overseas-master-data:driver:review','审核司机证照',NULL,'POST',934,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107305,0,'PLATFORM','BUTTON',5107300,'platform:overseas-master-data:driver:upload','上传司机证照',NULL,'POST',935,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107310,0,'PLATFORM','MENU',0,'platform:fleet:trip:list','车次管理','/overseas-master-data/trips','GET',940,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107311,0,'PLATFORM','BUTTON',5107310,'platform:fleet:trip:create','创建车次',NULL,'POST',941,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107312,0,'PLATFORM','BUTTON',5107310,'platform:fleet:trip:edit','修改车次计划',NULL,'PUT',942,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107313,0,'PLATFORM','BUTTON',5107310,'platform:fleet:trip:assign','分配车辆和司机',NULL,'POST',943,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107317,0,'PLATFORM','BUTTON',5107310,'platform:fleet:trip:dispatch-review','审核车次客户排车',NULL,'POST',947,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3)),
|
||||
(5107318,0,'PLATFORM','BUTTON',5107310,'platform:fleet:trip:loading-review','审核整车理货',NULL,'POST',948,'ACTIVE',1,0,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
ON DUPLICATE KEY UPDATE permission_type=VALUES(permission_type), parent_id=VALUES(parent_id), permission_name=VALUES(permission_name), route_path=VALUES(route_path), http_method=VALUES(http_method), sort_no=VALUES(sort_no), status='ACTIVE', built_in=1, deleted=0;
|
||||
INSERT INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, CURRENT_TIMESTAMP(3)
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
||||
AND permission.permission_code IN (
|
||||
'platform:overseas-master-data:driver:list',
|
||||
'platform:overseas-master-data:driver:create',
|
||||
'platform:overseas-master-data:driver:edit',
|
||||
'platform:overseas-master-data:driver:upload',
|
||||
'platform:overseas-master-data:driver:review',
|
||||
'platform:fleet:trip:list',
|
||||
'platform:fleet:trip:create',
|
||||
'platform:fleet:trip:assign',
|
||||
'platform:fleet:trip:edit',
|
||||
'platform:fleet:trip:dispatch-review',
|
||||
'platform:fleet:trip:loading-review')
|
||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='PLATFORM_ADMIN'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0
|
||||
AND NOT EXISTS(SELECT 1 FROM iam_role_permission existing WHERE existing.role_id=role.id AND existing.permission_id=permission.id);
|
||||
USE baiyue_lastleg;
|
||||
USE baiyue_lastleg;
|
||||
SET @return_fleet_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_vehicle' AND column_name='max_pallet_spaces')=0, 'ALTER TABLE ll_vehicle ADD COLUMN max_pallet_spaces INT NULL COMMENT ''最大标准托位,未知时为空''', 'SELECT 1');
|
||||
PREPARE return_fleet_stmt FROM @return_fleet_ddl;
|
||||
EXECUTE return_fleet_stmt;
|
||||
DEALLOCATE PREPARE return_fleet_stmt;
|
||||
SET @return_fleet_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_vehicle' AND column_name='max_load_kg')=0, 'ALTER TABLE ll_vehicle ADD COLUMN max_load_kg DECIMAL(12,3) NULL COMMENT ''最大载重,千克''', 'SELECT 1');
|
||||
PREPARE return_fleet_stmt FROM @return_fleet_ddl;
|
||||
EXECUTE return_fleet_stmt;
|
||||
DEALLOCATE PREPARE return_fleet_stmt;
|
||||
SET @return_fleet_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_vehicle' AND column_name='max_volume_cbm')=0, 'ALTER TABLE ll_vehicle ADD COLUMN max_volume_cbm DECIMAL(12,3) NULL COMMENT ''可用容积,立方米''', 'SELECT 1');
|
||||
PREPARE return_fleet_stmt FROM @return_fleet_ddl;
|
||||
EXECUTE return_fleet_stmt;
|
||||
DEALLOCATE PREPARE return_fleet_stmt;
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_driver (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
driver_no VARCHAR(32) NOT NULL COMMENT '司机业务编号',
|
||||
warehouse_id BIGINT NULL COMMENT '所属公共仓库引用',
|
||||
warehouse_code VARCHAR(64) NULL COMMENT '所属仓库编码快照',
|
||||
warehouse_name VARCHAR(256) NULL COMMENT '所属仓库名称快照',
|
||||
vehicle_models VARCHAR(128) NULL COMMENT '可驾驶车型描述',
|
||||
languages_and_area VARCHAR(256) NULL COMMENT '语言及服务区域',
|
||||
medical_expires_on DATE NULL COMMENT '体检到期日,当前不作为必需资质',
|
||||
training_expires_on DATE NULL COMMENT '培训安全到期日,当前不作为必需资质',
|
||||
insurance_expires_on DATE NULL COMMENT '保险到期日,当前不作为必需资质',
|
||||
name VARCHAR(128) NOT NULL COMMENT '姓名',
|
||||
phone VARCHAR(64) NOT NULL COMMENT '联系电话',
|
||||
ownership VARCHAR(16) NOT NULL COMMENT '归属 OWNED自有 DISPATCHED外派',
|
||||
region VARCHAR(16) NOT NULL COMMENT '区域 DOMESTIC国内 OVERSEAS海外',
|
||||
active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '证照审核 PENDING待审 APPROVED通过 REJECTED打回',
|
||||
license_number VARCHAR(128) NULL COMMENT '驾驶证编号',
|
||||
license_type VARCHAR(64) NULL COMMENT '驾驶证类型',
|
||||
license_issued_on DATE NULL COMMENT '驾驶证签发日期',
|
||||
license_expires_on DATE NULL COMMENT '驾驶证有效截止日',
|
||||
license_zone VARCHAR(64) NOT NULL COMMENT '证照有效日期的IANA时区',
|
||||
license_file_id BIGINT NULL COMMENT '平台驾驶证文件引用',
|
||||
remark VARCHAR(1000) NULL COMMENT '备注',
|
||||
record_version BIGINT NOT NULL DEFAULT 0 COMMENT '并发版本',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '创建时间UTC',
|
||||
updated_by VARCHAR(64) NOT NULL COMMENT '修改人',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '修改时间UTC',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_fleet_driver_no (driver_no),
|
||||
KEY idx_fleet_driver_page (active,review_status,updated_at,id),
|
||||
KEY idx_fleet_driver_expiry (license_expires_on),
|
||||
CONSTRAINT chk_fleet_driver_ownership CHECK (ownership IN ('OWNED','DISPATCHED')),
|
||||
CONSTRAINT chk_fleet_driver_region CHECK (region IN ('DOMESTIC','OVERSEAS')),
|
||||
CONSTRAINT chk_fleet_driver_review CHECK (review_status IN ('PENDING','APPROVED','REJECTED')),
|
||||
CONSTRAINT chk_fleet_driver_dates CHECK (license_issued_on IS NULL OR license_expires_on IS NULL OR license_issued_on <= license_expires_on)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台司机档案';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_driver_history (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
driver_id BIGINT NOT NULL COMMENT '司机内部引用',
|
||||
record_version BIGINT NOT NULL COMMENT '操作后的档案版本',
|
||||
action VARCHAR(32) NOT NULL COMMENT '创建编辑启停证照上传及审核动作',
|
||||
reason VARCHAR(1000) NULL COMMENT '操作原因或审核意见',
|
||||
license_file_id BIGINT NULL COMMENT '当时驾驶证文件引用',
|
||||
snapshot_json JSON NOT NULL COMMENT '操作后不可变档案快照',
|
||||
operator_id VARCHAR(64) NOT NULL COMMENT '操作人',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '操作时间UTC',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_fleet_driver_history_version (driver_id,record_version),
|
||||
KEY idx_fleet_driver_history_file (driver_id,license_file_id),
|
||||
CONSTRAINT fk_fleet_driver_history_driver FOREIGN KEY (driver_id) REFERENCES ll_fleet_driver(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='司机档案及审核历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_vehicle_record (
|
||||
id BIGINT NOT NULL COMMENT '记录内部主键',
|
||||
record_no VARCHAR(32) NOT NULL COMMENT '车辆运维业务编号',
|
||||
vehicle_id BIGINT NOT NULL COMMENT '所属车辆主键',
|
||||
vehicle_plate VARCHAR(64) NOT NULL COMMENT '建档时车牌快照',
|
||||
record_type VARCHAR(32) NOT NULL COMMENT '运维类型:保养、加油、维修、故障、救援、异常',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '记录发生时间(UTC)',
|
||||
details_json JSON NOT NULL COMMENT '带明确单位的业务字段快照',
|
||||
files_json JSON NOT NULL COMMENT '所属记录照片与上传幂等摘要',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '创建请求幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '原创建业务内容摘要',
|
||||
record_version BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '创建操作人',
|
||||
updated_by VARCHAR(64) NOT NULL COMMENT '最后操作人',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '创建时间(UTC)',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '更新时间(UTC)',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_vehicle_record_no (record_no),
|
||||
UNIQUE KEY uk_vehicle_record_creation (vehicle_id, created_by, idempotency_key),
|
||||
KEY idx_vehicle_record_page (vehicle_id, record_type, occurred_at, id),
|
||||
CONSTRAINT fk_vehicle_record_vehicle FOREIGN KEY (vehicle_id) REFERENCES ll_vehicle(id),
|
||||
CONSTRAINT ck_vehicle_record_type CHECK (record_type IN ('MAINTENANCE','FUEL','REPAIR','BREAKDOWN','RESCUE','EXCEPTION'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆运维记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_vehicle_record_history (
|
||||
id BIGINT NOT NULL COMMENT '历史内部主键',
|
||||
record_id BIGINT NOT NULL COMMENT '运维记录主键',
|
||||
record_version BIGINT NOT NULL COMMENT '对应业务版本',
|
||||
action VARCHAR(32) NOT NULL COMMENT '操作类型',
|
||||
reason VARCHAR(1000) DEFAULT NULL COMMENT '更正原因或附件名称',
|
||||
operator_id VARCHAR(64) NOT NULL COMMENT '操作人',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '操作时间(UTC)',
|
||||
snapshot_json JSON NOT NULL COMMENT '当时完整业务快照',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_vehicle_record_revision (record_id, record_version),
|
||||
CONSTRAINT fk_vehicle_record_history FOREIGN KEY (record_id) REFERENCES ll_fleet_vehicle_record(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆运维记录不可变历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_trip (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
trip_no VARCHAR(32) NOT NULL COMMENT '车次业务编号',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '创建请求幂等键',
|
||||
request_hash VARCHAR(64) NOT NULL COMMENT '创建请求SHA256',
|
||||
status VARCHAR(16) NOT NULL COMMENT '车次状态',
|
||||
warehouse_id BIGINT NOT NULL COMMENT '起运公共仓库引用',
|
||||
warehouse_code VARCHAR(64) NOT NULL COMMENT '起运仓编码快照',
|
||||
warehouse_name VARCHAR(256) NOT NULL COMMENT '起运仓名称快照',
|
||||
vehicle_id BIGINT NULL COMMENT '车辆档案引用',
|
||||
vehicle_plate VARCHAR(64) NULL COMMENT '分配时车牌快照',
|
||||
driver_id BIGINT NULL COMMENT '司机档案引用',
|
||||
driver_name VARCHAR(128) NULL COMMENT '分配时司机姓名',
|
||||
driver_phone VARCHAR(64) NULL COMMENT '分配时司机电话',
|
||||
max_pallet_spaces INT NULL COMMENT '分配时托位容量快照',
|
||||
max_load_kg DECIMAL(12,3) NULL COMMENT '分配时载重千克',
|
||||
max_volume_cbm DECIMAL(12,3) NULL COMMENT '分配时可用容积立方米',
|
||||
planned_departure_at DATETIME(3) NOT NULL COMMENT '计划发车UTC',
|
||||
expected_driver_end_at DATETIME(3) NOT NULL COMMENT '司机预计任务结束UTC,含承担的返程转场',
|
||||
departed_at DATETIME(3) NULL COMMENT '实际发车UTC',
|
||||
completed_at DATETIME(3) NULL COMMENT '实际运输完成UTC',
|
||||
stops_json JSON NOT NULL COMMENT '按顺序保存的路线站点',
|
||||
planned_load_json JSON NULL COMMENT '拣货前自有货计划载荷与逐段剩余容量',
|
||||
load_json JSON NULL COMMENT '已确认实物装载及客户货物来源快照',
|
||||
loading_review_json LONGTEXT NULL COMMENT '整车理货审核快照,空值为待审核',
|
||||
departure_key VARCHAR(128) NULL COMMENT '整车发车幂等标识',
|
||||
departure_hash CHAR(64) NULL COMMENT '整车发车请求摘要',
|
||||
departure_result_json JSON NULL COMMENT '整车发车结果及各客户出库结果',
|
||||
completion_json JSON NULL COMMENT '货物结案来源及幂等结果快照',
|
||||
vehicle_release_json JSON NULL COMMENT '车辆任务结束地点状态与幂等记录',
|
||||
driver_release_json JSON NULL COMMENT '司机任务结束地点与幂等记录',
|
||||
remark VARCHAR(1000) NULL COMMENT '备注',
|
||||
record_version BIGINT NOT NULL DEFAULT 0 COMMENT '并发版本',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '创建UTC时间',
|
||||
updated_by VARCHAR(64) NOT NULL COMMENT '更新人',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '更新UTC时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_fleet_trip_no (trip_no),
|
||||
UNIQUE KEY uk_fleet_trip_create (created_by,idempotency_key),
|
||||
KEY idx_fleet_trip_page (warehouse_id,status,updated_at,id),
|
||||
CONSTRAINT fk_fleet_trip_vehicle FOREIGN KEY (vehicle_id) REFERENCES ll_vehicle(id),
|
||||
CONSTRAINT fk_fleet_trip_driver FOREIGN KEY (driver_id) REFERENCES ll_fleet_driver(id),
|
||||
CONSTRAINT chk_fleet_trip_status CHECK (status IN ('PLANNED','DEPARTING','IN_TRANSIT','COMPLETED','CANCELLED')),
|
||||
CONSTRAINT chk_fleet_trip_time CHECK (expected_driver_end_at > planned_departure_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台运输车次';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_capacity_offer (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
offer_no VARCHAR(64) NOT NULL COMMENT '拼车运力发布业务编号',
|
||||
trip_id BIGINT NOT NULL COMMENT '唯一所属车次',
|
||||
trip_no VARCHAR(64) NOT NULL COMMENT '车次业务编号',
|
||||
status VARCHAR(16) NOT NULL COMMENT '发布状态:草稿、开放、暂停、关闭',
|
||||
cutoff_at DATETIME(3) NOT NULL COMMENT '最晚交货及新询价截止UTC时间',
|
||||
contact_name VARCHAR(128) NULL COMMENT '对内部销售公开的调度联系人',
|
||||
contact_phone VARCHAR(128) NULL COMMENT '对内部销售公开的调度联系电话',
|
||||
remark VARCHAR(1000) NULL COMMENT '公开承运说明',
|
||||
published_fingerprint CHAR(64) NULL COMMENT '已核对路线车辆及容量计划事实摘要',
|
||||
record_version BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '创建UTC时间',
|
||||
updated_by VARCHAR(64) NOT NULL COMMENT '修改人',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '修改UTC时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_fleet_capacity_offer_no(offer_no),
|
||||
UNIQUE KEY uk_fleet_capacity_offer_trip(trip_id),
|
||||
KEY idx_fleet_capacity_offer_status(status,cutoff_at,id),
|
||||
CONSTRAINT fk_fleet_capacity_offer_trip FOREIGN KEY(trip_id) REFERENCES ll_fleet_trip(id),
|
||||
CONSTRAINT chk_fleet_capacity_offer_status CHECK(status IN ('DRAFT','OPEN','PAUSED','CLOSED'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='内部拼车车次余位发布;容量实时计算不在此另存';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_capacity_offer_command (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
trip_id BIGINT NOT NULL COMMENT '所属车次锁作用域',
|
||||
operator_id VARCHAR(64) NOT NULL COMMENT '原请求操作者',
|
||||
action VARCHAR(32) NOT NULL COMMENT '业务命令类型',
|
||||
scope_no VARCHAR(64) NOT NULL COMMENT '发布或预订业务编号作用域',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '原请求幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '原请求业务内容摘要',
|
||||
result_json JSON NOT NULL COMMENT '成功命令原始结果用于精确重放',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '命令完成UTC时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_fleet_capacity_offer_command(trip_id,operator_id,action,scope_no,idempotency_key),
|
||||
CONSTRAINT fk_fleet_capacity_command_trip FOREIGN KEY(trip_id) REFERENCES ll_fleet_trip(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拼车发布及销售意向持久幂等结果';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_external_booking (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
booking_no VARCHAR(32) NOT NULL COMMENT '外接预订业务编号',
|
||||
trip_id BIGINT NOT NULL COMMENT '所属车次',
|
||||
trip_no VARCHAR(32) NOT NULL COMMENT '车次业务编号',
|
||||
customer_id VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '委托客户及付款归属',
|
||||
customer_code VARCHAR(64) NOT NULL COMMENT '客户编号快照',
|
||||
customer_name VARCHAR(255) NOT NULL COMMENT '客户名称快照',
|
||||
status VARCHAR(16) NOT NULL COMMENT 'DRAFT询价/CONFIRMED占位/RECEIVED确认实收/BOUND关联发运/CANCELLED取消',
|
||||
cargo_json JSON NOT NULL COMMENT '货物、计划载荷、联系人及约定报价快照',
|
||||
quote_source_json JSON NULL COMMENT '选用销售报价的可信版本快照,与实际成交金额分开保存',
|
||||
destination_key VARCHAR(160) NOT NULL COMMENT '已有路线卸货站匹配键',
|
||||
destination_label VARCHAR(255) NOT NULL COMMENT '约定卸货站名称',
|
||||
agreed_amount DECIMAL(16,2) NULL COMMENT '约定金额,非已入账应收或实收',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '登记请求键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '登记内容摘要',
|
||||
confirm_key VARCHAR(128) NULL COMMENT '承运确认请求键',
|
||||
confirm_hash CHAR(64) NULL COMMENT '承运确认摘要',
|
||||
cancel_key VARCHAR(128) NULL COMMENT '取消请求键',
|
||||
cancel_hash CHAR(64) NULL COMMENT '取消摘要',
|
||||
incoming_no VARCHAR(64) NULL COMMENT '正式来货单业务编号',
|
||||
dispatch_no VARCHAR(64) NULL COMMENT '已关联客户排车业务编号',
|
||||
receipt_json JSON NULL COMMENT '实测来源及运营确认的托位和运费依据',
|
||||
incoming_key VARCHAR(128) NULL COMMENT '来货登记请求键',
|
||||
incoming_hash CHAR(64) NULL COMMENT '来货登记摘要',
|
||||
receipt_key VARCHAR(128) NULL COMMENT '实收确认请求键',
|
||||
receipt_hash CHAR(64) NULL COMMENT '实收确认摘要',
|
||||
binding_key VARCHAR(128) NULL COMMENT '关联发运请求键',
|
||||
binding_hash CHAR(64) NULL COMMENT '关联发运摘要',
|
||||
offer_id BIGINT NULL COMMENT '来源运力发布,普通车次预订为空',
|
||||
sales_owner_user_id VARCHAR(64) NULL COMMENT '登录身份冻结的销售负责人',
|
||||
business_type VARCHAR(32) NULL COMMENT '本土商业类型LOCAL_LOOSEBOARD或US_TRUCK',
|
||||
delivery_mode VARCHAR(32) NULL COMMENT '客户送仓或人工安排提货回仓',
|
||||
estimated_arrival_at DATETIME(3) NULL COMMENT '预计实际到我仓UTC时间',
|
||||
expected_pallet_count INT NULL COMMENT '预计实托数,与托位分开且未知为空',
|
||||
record_version BIGINT NOT NULL DEFAULT 0 COMMENT '记录版本',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '登记人',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '登记UTC时间',
|
||||
updated_by VARCHAR(64) NOT NULL COMMENT '修改人',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '修改UTC时间',
|
||||
KEY idx_fleet_booking_offer(offer_id,status,id),
|
||||
KEY idx_fleet_booking_sales(sales_owner_user_id,offer_id,id),
|
||||
CONSTRAINT fk_fleet_booking_offer FOREIGN KEY(offer_id) REFERENCES ll_fleet_capacity_offer(id),
|
||||
CONSTRAINT chk_fleet_booking_expected_pallet CHECK(expected_pallet_count IS NULL OR expected_pallet_count > 0),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_fleet_booking_no(booking_no),
|
||||
UNIQUE KEY uk_fleet_booking_create(trip_id,created_by,idempotency_key),
|
||||
KEY idx_fleet_booking_trip(trip_id,status,id),
|
||||
UNIQUE KEY uk_fleet_booking_incoming(incoming_no),
|
||||
KEY idx_fleet_booking_dispatch(dispatch_no),
|
||||
CONSTRAINT fk_fleet_booking_trip FOREIGN KEY(trip_id) REFERENCES ll_fleet_trip(id),
|
||||
CONSTRAINT chk_fleet_booking_status CHECK(status IN ('DRAFT','CONFIRMED','RECEIVED','BOUND','CANCELLED'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='外接询价及确认承运占位;货物收发和费用入账分别引用正式事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_pool_order_link (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
booking_id BIGINT NOT NULL COMMENT '唯一外接预订',
|
||||
booking_no VARCHAR(64) NOT NULL COMMENT '预订业务编号',
|
||||
customer_id VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商业客户隔离范围',
|
||||
business_type VARCHAR(32) NOT NULL COMMENT '本土散板或本土卡车商业类型',
|
||||
business_order_id BIGINT NOT NULL COMMENT '对应商业订单内部关联',
|
||||
business_order_no VARCHAR(64) NOT NULL COMMENT '本土商业订单业务编号',
|
||||
warehouse_id VARCHAR(128) NOT NULL COMMENT '真实我仓上车仓库',
|
||||
destination_key VARCHAR(160) NOT NULL COMMENT '冻结卸货站匹配键',
|
||||
delivery_mode VARCHAR(32) NOT NULL COMMENT '客户送仓或人工提货回仓',
|
||||
delivery_instructions VARCHAR(1000) NULL COMMENT '实际到仓交货说明',
|
||||
incoming_no VARCHAR(64) NULL COMMENT '真实来货业务编号,未登记为空',
|
||||
lastleg_order_id BIGINT NULL COMMENT '唯一真实尾程来源,未发生为空',
|
||||
dispatch_no VARCHAR(64) NULL COMMENT '已关联客户排车编号',
|
||||
fulfillment_stage VARCHAR(32) NOT NULL COMMENT '实际履约阶段,不能手工冒充完成',
|
||||
stage_occurred_at DATETIME(3) NOT NULL COMMENT '实际阶段发生UTC时间',
|
||||
actual_pallet_count INT NULL COMMENT '实际去重托盘数,未知为空',
|
||||
actual_package_count INT NULL COMMENT '实际箱数,未知为空',
|
||||
actual_pallet_spaces INT NULL COMMENT '实际核准占用托位,未知为空',
|
||||
record_version BIGINT NOT NULL DEFAULT 0 COMMENT '桥记录版本',
|
||||
creation_request_hash CHAR(64) NOT NULL COMMENT '商业单创建身份和预报事实摘要',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '确认承运操作者',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '创建UTC时间',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '修改UTC时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_fleet_pool_order_booking(booking_id),
|
||||
UNIQUE KEY uk_fleet_pool_order_booking_no(booking_no),
|
||||
UNIQUE KEY uk_fleet_pool_order_business(business_type,business_order_id),
|
||||
UNIQUE KEY uk_fleet_pool_order_incoming(incoming_no),
|
||||
UNIQUE KEY uk_fleet_pool_order_lastleg(lastleg_order_id),
|
||||
KEY idx_fleet_pool_order_customer(customer_id,business_type,business_order_no),
|
||||
KEY idx_fleet_pool_order_dispatch(customer_id,dispatch_no),
|
||||
CONSTRAINT fk_fleet_pool_order_booking FOREIGN KEY(booking_id) REFERENCES ll_fleet_external_booking(id),
|
||||
CONSTRAINT chk_fleet_pool_order_type CHECK(business_type IN ('LOCAL_LOOSEBOARD','US_TRUCK')),
|
||||
CONSTRAINT chk_fleet_pool_order_actual CHECK((actual_pallet_count IS NULL OR actual_pallet_count >= 0) AND (actual_package_count IS NULL OR actual_package_count >= 0) AND (actual_pallet_spaces IS NULL OR actual_pallet_spaces >= 0))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拼车预订与本土商业单唯一真实履约关联,不重复记运费';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_resource_assignment (
|
||||
resource_kind VARCHAR(16) NOT NULL COMMENT '资源种类 VEHICLE或DRIVER',
|
||||
resource_id BIGINT NOT NULL COMMENT '对应车辆或司机档案引用',
|
||||
trip_id BIGINT NOT NULL COMMENT '占用车次',
|
||||
trip_no VARCHAR(32) NOT NULL COMMENT '占用车次业务编号',
|
||||
PRIMARY KEY (resource_kind,resource_id),
|
||||
KEY idx_fleet_resource_trip (trip_id),
|
||||
CONSTRAINT fk_fleet_resource_trip FOREIGN KEY (trip_id) REFERENCES ll_fleet_trip(id),
|
||||
CONSTRAINT chk_fleet_resource_kind CHECK (resource_kind IN ('VEHICLE','DRIVER'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆及司机当前车次占用,释放后删除锁行,历史保留在车次';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_trip_dispatch (
|
||||
dispatch_id BIGINT NOT NULL COMMENT '客户排车单内部引用,唯一有效车次关联',
|
||||
dispatch_no VARCHAR(64) NOT NULL COMMENT '客户排车业务单号',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '排车单自身客户归属',
|
||||
trip_id BIGINT NOT NULL COMMENT '共享运输车次内部引用',
|
||||
trip_no VARCHAR(32) NOT NULL COMMENT '共享运输车次业务编号',
|
||||
package_count INT NOT NULL COMMENT '关联时待发运箱数快照',
|
||||
bound_by VARCHAR(64) NOT NULL COMMENT '关联操作人',
|
||||
bound_at DATETIME(3) NOT NULL COMMENT '关联UTC时间',
|
||||
review_json LONGTEXT NULL COMMENT '客户排车调度审核快照,空值为待审核',
|
||||
PRIMARY KEY (dispatch_id),
|
||||
UNIQUE KEY uk_fleet_trip_dispatch_no (dispatch_no),
|
||||
KEY idx_fleet_trip_dispatch_trip (trip_id,dispatch_id),
|
||||
CONSTRAINT fk_fleet_trip_dispatch_trip FOREIGN KEY (trip_id) REFERENCES ll_fleet_trip(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车次与各客户排车单的有效关联,解除前快照保留在车次历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_trip_history (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
trip_id BIGINT NOT NULL COMMENT '车次内部引用',
|
||||
record_version BIGINT NOT NULL COMMENT '操作后版本',
|
||||
action VARCHAR(32) NOT NULL COMMENT '车次操作',
|
||||
reason VARCHAR(1000) NULL COMMENT '操作原因',
|
||||
operator_id VARCHAR(64) NOT NULL COMMENT '操作人',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '操作UTC时间',
|
||||
snapshot_json JSON NOT NULL COMMENT '操作后不可变车次快照',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_fleet_trip_history_version (trip_id,record_version),
|
||||
CONSTRAINT fk_fleet_trip_history FOREIGN KEY (trip_id) REFERENCES ll_fleet_trip(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运输车次操作历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_trip_operations_record (
|
||||
id BIGINT NOT NULL COMMENT '出车实测历史主键',
|
||||
trip_no VARCHAR(64) NOT NULL COMMENT '车次业务编号',
|
||||
record_version BIGINT NOT NULL COMMENT '实测记录版本从一递增',
|
||||
details_json JSON NOT NULL COMMENT '实际里程英里油耗升与备注快照',
|
||||
reason VARCHAR(1000) NOT NULL COMMENT '实测更正说明',
|
||||
operator_id VARCHAR(64) NOT NULL COMMENT '实测操作人标识',
|
||||
request_key VARCHAR(128) NOT NULL COMMENT '实测保存幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '实测请求内容摘要',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '实测保存时间UTC',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_fleet_operations_version (trip_no,record_version),
|
||||
UNIQUE KEY uk_fleet_operations_request (trip_no,operator_id,request_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车次里程油耗不可变更正记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_trip_handover (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
trip_id BIGINT NOT NULL COMMENT '所属车次',
|
||||
trip_version BIGINT NOT NULL COMMENT '交接对应车次历史版本',
|
||||
trip_no VARCHAR(32) NOT NULL COMMENT '车次业务编号',
|
||||
handover_no VARCHAR(32) NOT NULL COMMENT '交接业务编号',
|
||||
request_key VARCHAR(128) NOT NULL COMMENT '交接请求幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '交接请求摘要',
|
||||
record_version BIGINT NOT NULL DEFAULT 0 COMMENT '资源结束并发版本',
|
||||
handed_over_at DATETIME(3) NOT NULL COMMENT '实际交接UTC时间',
|
||||
snapshot_json JSON NOT NULL COMMENT '不可变资源及剩余货物交接快照',
|
||||
vehicle_release_json JSON NULL COMMENT '原车辆任务结束及幂等结果',
|
||||
driver_release_json JSON NULL COMMENT '原司机任务结束及幂等结果',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '记录操作人',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '记录UTC时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_fleet_handover_no (handover_no),
|
||||
UNIQUE KEY uk_fleet_handover_request (trip_id, request_key),
|
||||
UNIQUE KEY uk_fleet_handover_version (trip_id, trip_version),
|
||||
KEY idx_fleet_handover_page (trip_id, id),
|
||||
CONSTRAINT fk_fleet_handover_trip FOREIGN KEY (trip_id) REFERENCES ll_fleet_trip(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='在途车辆司机交接';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_finance_source (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '来源提交主键',
|
||||
record_id BIGINT UNSIGNED NOT NULL COMMENT '车辆运维记录主键',
|
||||
source_version BIGINT UNSIGNED NOT NULL COMMENT '独立财务来源版本,从1递增',
|
||||
record_version BIGINT UNSIGNED NOT NULL COMMENT '提交时运维记录版本',
|
||||
operator_id VARCHAR(64) NOT NULL COMMENT '提交操作人标识',
|
||||
request_key VARCHAR(128) NOT NULL COMMENT '原请求幂等键',
|
||||
request_hash VARCHAR(71) NOT NULL COMMENT '规范请求摘要,含sha256前缀',
|
||||
event_id VARCHAR(128) NOT NULL COMMENT '关联公共Outbox事件ID',
|
||||
payload_json JSON NOT NULL COMMENT '不可变公司成本来源快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '提交时间UTC',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_fleet_finance_source_version (record_id,source_version),
|
||||
UNIQUE KEY uk_fleet_finance_source_request (record_id,operator_id,request_key),
|
||||
UNIQUE KEY uk_fleet_finance_source_event (event_id),
|
||||
CONSTRAINT chk_fleet_finance_source_version CHECK (source_version>0)
|
||||
) ENGINE=InnoDB COMMENT='维修救援公司成本提交来源,不保存付款推定';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_quote (
|
||||
id BIGINT PRIMARY KEY COMMENT '报价记录主键',
|
||||
quote_no VARCHAR(64) NOT NULL COMMENT '报价业务编号',
|
||||
kind VARCHAR(16) NOT NULL COMMENT '报价用途采购或销售',
|
||||
party_id BIGINT NOT NULL COMMENT '供应商或客户主档标识',
|
||||
party_name VARCHAR(160) NOT NULL COMMENT '往来单位名称快照',
|
||||
origin_city VARCHAR(128) NOT NULL COMMENT '发货城市',
|
||||
destination_code VARCHAR(64) NOT NULL COMMENT '目的仓业务编码',
|
||||
valid_from DATE NOT NULL COMMENT '报价生效日期',
|
||||
valid_to DATE NOT NULL COMMENT '报价失效日期含当天',
|
||||
status VARCHAR(16) NOT NULL COMMENT '报价启停或删除状态',
|
||||
natural_key CHAR(64) NOT NULL COMMENT '报价组合去重摘要',
|
||||
details_json JSON NOT NULL COMMENT '完整报价明细快照',
|
||||
record_version BIGINT NOT NULL COMMENT '报价记录版本',
|
||||
create_key VARCHAR(128) NOT NULL COMMENT '创建幂等键',
|
||||
create_hash CHAR(64) NOT NULL COMMENT '创建请求含导入批次内容摘要',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '创建操作人标识',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '创建时间UTC',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '更新时间UTC',
|
||||
UNIQUE KEY uk_fleet_quote_no (quote_no),
|
||||
UNIQUE KEY uk_fleet_quote_natural (natural_key),
|
||||
UNIQUE KEY uk_fleet_quote_create (created_by,create_key),
|
||||
KEY idx_fleet_quote_search (kind,status,valid_from,valid_to)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='车辆采购与外接销售报价';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_quote_history (
|
||||
id BIGINT PRIMARY KEY COMMENT '报价历史主键',
|
||||
quote_id BIGINT NOT NULL COMMENT '报价记录标识',
|
||||
record_version BIGINT NOT NULL COMMENT '报价记录版本',
|
||||
action VARCHAR(24) NOT NULL COMMENT '报价操作类型',
|
||||
operator_id VARCHAR(64) NOT NULL COMMENT '操作人标识',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '操作时间UTC',
|
||||
snapshot_json JSON NOT NULL COMMENT '报价完整版本快照',
|
||||
UNIQUE KEY uk_fleet_quote_history_version (quote_id,record_version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='车辆报价不可变版本历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_trip_cost (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
trip_id BIGINT NOT NULL COMMENT '车次内部引用',
|
||||
cost_no VARCHAR(32) NOT NULL COMMENT '车次成本业务编号',
|
||||
category VARCHAR(16) NOT NULL COMMENT '直接成本类别FUEL/DRIVER/TOLL/PARKING/OTHER',
|
||||
currency VARCHAR(3) NOT NULL COMMENT '原始成本币种CNY/USD,不混币汇总',
|
||||
amount DECIMAL(14,2) NOT NULL COMMENT '运营登记金额,不代表付款',
|
||||
incurred_at DATETIME(3) NOT NULL COMMENT '实际费用发生UTC时间',
|
||||
remark VARCHAR(1000) NULL COMMENT '费用备注',
|
||||
record_version BIGINT NOT NULL COMMENT '成本记录版本',
|
||||
request_key VARCHAR(128) NOT NULL COMMENT '创建幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '创建请求摘要',
|
||||
created_by VARCHAR(64) NOT NULL COMMENT '登记人',
|
||||
updated_by VARCHAR(64) NOT NULL COMMENT '最近更正人',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '登记UTC时间',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '更正UTC时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_fleet_trip_cost_no(cost_no),
|
||||
UNIQUE KEY uk_fleet_trip_cost_request(trip_id,created_by,request_key),
|
||||
KEY idx_fleet_trip_cost_page(trip_id,incurred_at,id),
|
||||
CONSTRAINT fk_fleet_trip_cost_trip FOREIGN KEY(trip_id) REFERENCES ll_fleet_trip(id),
|
||||
CONSTRAINT ck_fleet_trip_cost_amount CHECK(amount>=0),
|
||||
CONSTRAINT ck_fleet_trip_cost_currency CHECK(currency IN ('CNY','USD')),
|
||||
CONSTRAINT ck_fleet_trip_cost_category CHECK(category IN ('FUEL','DRIVER','TOLL','PARKING','OTHER'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车次直接运营成本,不生成客户账单或付款';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_fleet_trip_cost_history (
|
||||
id BIGINT NOT NULL COMMENT '内部主键',
|
||||
cost_id BIGINT NOT NULL COMMENT '成本记录内部引用',
|
||||
record_version BIGINT NOT NULL COMMENT '成本更正后版本',
|
||||
action VARCHAR(16) NOT NULL COMMENT '成本操作CREATE/UPDATE',
|
||||
reason VARCHAR(1000) NULL COMMENT '登记或更正说明',
|
||||
operator_id VARCHAR(64) NOT NULL COMMENT '操作人',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '操作UTC时间',
|
||||
snapshot_json JSON NOT NULL COMMENT '成本与当时整趟汇总及分摊快照',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_fleet_trip_cost_history(cost_id,record_version),
|
||||
CONSTRAINT fk_fleet_trip_cost_history FOREIGN KEY(cost_id) REFERENCES ll_fleet_trip_cost(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车次成本不可变更正历史';
|
||||
|
||||
|
||||
-- 来源:return-carton-counting-incremental-20261009.sql
|
||||
-- 退货托盘签收箱内清点;MySQL 8;仅在目标尾程库执行,可重复运行。
|
||||
USE baiyue_lastleg;
|
||||
SET @rc_count_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='count_status'),'SELECT 1','ALTER TABLE ll_return_inventory_carton ADD COLUMN count_status VARCHAR(16) NOT NULL DEFAULT ''CONFIRMED'' COMMENT ''箱内清点状态 PENDING待清点 CONFIRMED已确认''');
|
||||
PREPARE rc_count_stmt FROM @rc_count_ddl; EXECUTE rc_count_stmt; DEALLOCATE PREPARE rc_count_stmt;
|
||||
SET @rc_count_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='counted_at'),'SELECT 1','ALTER TABLE ll_return_inventory_carton ADD COLUMN counted_at DATETIME(3) NULL COMMENT ''箱内清点确认时间''');
|
||||
PREPARE rc_count_stmt FROM @rc_count_ddl; EXECUTE rc_count_stmt; DEALLOCATE PREPARE rc_count_stmt;
|
||||
SET @rc_count_ddl = IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='counted_by'),'SELECT 1','ALTER TABLE ll_return_inventory_carton ADD COLUMN counted_by VARCHAR(128) NULL COMMENT ''箱内清点确认操作人''');
|
||||
PREPARE rc_count_stmt FROM @rc_count_ddl; EXECUTE rc_count_stmt; DEALLOCATE PREPARE rc_count_stmt;
|
||||
SET @rc_count_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND index_name='idx_ll_return_count'),'SELECT 1','CREATE INDEX idx_ll_return_count ON ll_return_inventory_carton(overseas_warehouse_id,count_status,status,source_receipt_id)');
|
||||
PREPARE rc_count_stmt FROM @rc_count_ddl; EXECUTE rc_count_stmt; DEALLOCATE PREPARE rc_count_stmt;
|
||||
-- 历史箱:只迁移整批仍可用、没有换标/合箱占用、未确认清点的托盘签收箱。
|
||||
-- 已进入履约或合箱的批次不回退;后续需要逐批人工核实。
|
||||
UPDATE ll_return_inventory_carton c
|
||||
JOIN (SELECT source_receipt_id FROM (SELECT a.source_receipt_id
|
||||
FROM ll_return_inventory_carton a WHERE a.source_receipt_id IS NOT NULL AND a.source_unit_type='PALLET'
|
||||
GROUP BY a.source_receipt_id
|
||||
HAVING SUM(a.status<>'AVAILABLE' OR a.relabel_application_id IS NOT NULL OR a.relabel_no IS NOT NULL OR a.counted_at IS NOT NULL)=0
|
||||
AND SUM(EXISTS(SELECT 1 FROM ll_return_inventory_carton_item i WHERE i.carton_id=a.id AND COALESCE(i.packing_reserved_quantity,0)>0))=0
|
||||
AND SUM(EXISTS(SELECT 1 FROM ll_return_packing_trace t WHERE t.source_carton_mark=a.carton_mark OR t.target_carton_mark=a.carton_mark))=0
|
||||
) pending) p ON p.source_receipt_id=c.source_receipt_id
|
||||
SET c.count_status='PENDING' WHERE c.source_unit_type='PALLET' AND c.counted_at IS NULL;
|
||||
|
||||
-- 退货箱内清点作业会话:每位操作员只能认领一个批次,每个批次只能被一人认领。
|
||||
CREATE TABLE IF NOT EXISTS ll_return_carton_count_session (
|
||||
operator_id VARCHAR(128) NOT NULL COMMENT '平台PDA操作员标识',
|
||||
warehouse_id VARCHAR(128) NOT NULL COMMENT '认领时绑定海外仓标识',
|
||||
receipt_id BIGINT NULL COMMENT '正在清点的签收批次,完成后释放为NULL',
|
||||
started_at DATETIME(3) NULL COMMENT '本次开始清点时间',
|
||||
created_at DATETIME(3) NOT NULL COMMENT '会话创建时间',
|
||||
updated_at DATETIME(3) NOT NULL COMMENT '会话更新时间',
|
||||
PRIMARY KEY(operator_id),
|
||||
UNIQUE KEY uk_ll_return_count_session_receipt(receipt_id)
|
||||
) ENGINE=InnoDB COMMENT='退货箱内清点单人单任务持久会话';
|
||||
|
||||
|
||||
-- 来源:return-tail-order-number-incremental-20261009.sql
|
||||
-- 退货业务统一单号;只修改已验证关联的单号快照,保留订单ID、库存、清点、换标及履约状态。
|
||||
USE baiyue_lastleg;
|
||||
UPDATE ll_order o
|
||||
JOIN ll_return_order r ON r.lastleg_order_id=o.id AND r.customer_id=o.customer_id
|
||||
SET o.order_no=r.return_no, r.lastleg_order_no=r.return_no
|
||||
WHERE o.order_type='RETURN_RELABEL' AND o.source_order_no=r.return_no
|
||||
AND (NOT (o.order_no <=> r.return_no) OR NOT (r.lastleg_order_no <=> r.return_no));
|
||||
|
||||
-- 应返回零行;非零表示来源关系异常,须核实,脚本不猜测或改绑。
|
||||
SELECT r.return_no, o.order_no, r.lastleg_order_no
|
||||
FROM ll_return_order r JOIN ll_order o ON o.id=r.lastleg_order_id AND o.customer_id=r.customer_id
|
||||
WHERE o.order_type='RETURN_RELABEL'
|
||||
AND (NOT (o.order_no <=> r.return_no) OR NOT (r.lastleg_order_no <=> r.return_no));
|
||||
|
||||
|
||||
-- 来源:return-single-sku-quantity-incremental.sql
|
||||
-- 退货单 SKU 按件数出库快照。目标库:baiyue_lastleg。可重复执行。
|
||||
USE baiyue_lastleg;
|
||||
SET @return_sku_quantity_ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_outbound_pallet_item' AND column_name='return_sku_quantity'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE ll_outbound_pallet_item ADD COLUMN return_sku_quantity BIGINT NULL COMMENT ''退货单SKU本来源计划出库件数快照,NULL为历史整箱出库'' AFTER sku_code'
|
||||
);
|
||||
PREPARE return_sku_quantity_stmt FROM @return_sku_quantity_ddl;
|
||||
EXECUTE return_sku_quantity_stmt;
|
||||
DEALLOCATE PREPARE return_sku_quantity_stmt;
|
||||
|
||||
-- 部分出库仅扣件数;整箱扣一箱,保留守恒和非负约束。
|
||||
SET @return_sku_check_ddl = IF(EXISTS(SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND constraint_name='chk_ll_return_outbound_movement_delta'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP CHECK chk_ll_return_outbound_movement_delta, ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)', 'ALTER TABLE ll_return_inventory_outbound_movement ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)');
|
||||
PREPARE return_sku_check_stmt FROM @return_sku_check_ddl;
|
||||
EXECUTE return_sku_check_stmt;
|
||||
DEALLOCATE PREPARE return_sku_check_stmt;
|
||||
-- 同箱余数允许再次出库,防重仍由拣货ID+SKU唯一索引保证。
|
||||
SET @return_sku_index_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND index_name='uk_ll_return_outbound_carton_sku'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP INDEX uk_ll_return_outbound_carton_sku, ADD INDEX idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code)', 'SELECT 1');
|
||||
PREPARE return_sku_index_stmt FROM @return_sku_index_ddl;
|
||||
EXECUTE return_sku_index_stmt;
|
||||
DEALLOCATE PREPARE return_sku_index_stmt;
|
||||
|
||||
USE baiyue_lastleg;
|
||||
SELECT 'OVERSEAS_INCREMENTAL_COMPLETE' AS migration_status,DATABASE() AS business_database;
|
||||
@@ -33,7 +33,8 @@ JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code
|
||||
'platform:fleet:trip:dispatch-review',
|
||||
'platform:fleet:trip:loading-review')
|
||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='PLATFORM_ADMIN'
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0
|
||||
AND NOT EXISTS(SELECT 1 FROM iam_role_permission existing WHERE existing.role_id=role.id AND existing.permission_id=permission.id);
|
||||
USE baiyue_lastleg;
|
||||
USE baiyue_lastleg;
|
||||
SET @return_fleet_ddl = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_vehicle' AND column_name='max_pallet_spaces')=0, 'ALTER TABLE ll_vehicle ADD COLUMN max_pallet_spaces INT NULL COMMENT ''最大标准托位,未知时为空''', 'SELECT 1');
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- 退货单 SKU 按件数出库快照。目标库:baiyue_lastleg。可重复执行。
|
||||
USE baiyue_lastleg;
|
||||
SET @return_sku_quantity_ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_outbound_pallet_item' AND column_name='return_sku_quantity'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE ll_outbound_pallet_item ADD COLUMN return_sku_quantity BIGINT NULL COMMENT ''退货单SKU本来源计划出库件数快照,NULL为历史整箱出库'' AFTER sku_code'
|
||||
);
|
||||
PREPARE return_sku_quantity_stmt FROM @return_sku_quantity_ddl;
|
||||
EXECUTE return_sku_quantity_stmt;
|
||||
DEALLOCATE PREPARE return_sku_quantity_stmt;
|
||||
|
||||
-- 部分出库仅扣件数;整箱扣一箱,保留守恒和非负约束。
|
||||
SET @return_sku_check_ddl = IF(EXISTS(SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND constraint_name='chk_ll_return_outbound_movement_delta'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP CHECK chk_ll_return_outbound_movement_delta, ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)', 'ALTER TABLE ll_return_inventory_outbound_movement ADD CONSTRAINT chk_ll_return_outbound_movement_delta CHECK(quantity>0 AND total_after>=0 AND available_after>=0 AND total_before-total_after=quantity AND available_before-available_after=quantity AND cartons_after>=0 AND cartons_before-cartons_after IN (0,1) AND total_after>=available_after)');
|
||||
PREPARE return_sku_check_stmt FROM @return_sku_check_ddl;
|
||||
EXECUTE return_sku_check_stmt;
|
||||
DEALLOCATE PREPARE return_sku_check_stmt;
|
||||
-- 同箱余数允许再次出库,防重仍由拣货ID+SKU唯一索引保证。
|
||||
SET @return_sku_index_ddl = IF(EXISTS(SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_outbound_movement' AND index_name='uk_ll_return_outbound_carton_sku'), 'ALTER TABLE ll_return_inventory_outbound_movement DROP INDEX uk_ll_return_outbound_carton_sku, ADD INDEX idx_ll_return_outbound_carton_sku(customer_id,carton_id,sku_code)', 'SELECT 1');
|
||||
PREPARE return_sku_index_stmt FROM @return_sku_index_ddl;
|
||||
EXECUTE return_sku_index_stmt;
|
||||
DEALLOCATE PREPARE return_sku_index_stmt;
|
||||
Reference in New Issue
Block a user