test(firstleg): active price book sailing channels
This commit is contained in:
@@ -3,10 +3,6 @@ ALTER TABLE ll_cargo_discard_item
|
||||
MODIFY COLUMN profile_id BIGINT NULL COMMENT '托盘档案ID',
|
||||
MODIFY COLUMN sku_inventory_id BIGINT NULL COMMENT 'SKU库存记录ID';
|
||||
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='completed_at'),
|
||||
'ALTER TABLE ll_cargo_discard_application ADD COLUMN completed_at DATETIME(3) NULL COMMENT ''废弃完成时间'' AFTER reviewed_at', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @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 stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_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));
|
||||
@@ -26,6 +22,3 @@ PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND index_name='idx_ll_cargo_discard_return_carton'),
|
||||
'ALTER TABLE ll_cargo_discard_item ADD KEY idx_ll_cargo_discard_return_carton(customer_id,return_carton_id)', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'),
|
||||
'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
@@ -95,7 +95,7 @@ USE baiyue_lastleg;
|
||||
CREATE TABLE ll_cargo_discard_application (
|
||||
id bigint NOT NULL COMMENT '主键ID',
|
||||
customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID',
|
||||
discard_no varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '废弃申请单号', outbound_no varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '废弃出库单号',
|
||||
discard_no varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '废弃申请单号',
|
||||
overseas_warehouse_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '海外仓ID',
|
||||
overseas_warehouse_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '海外仓名称快照',
|
||||
inbound_codes varchar(2000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '入仓单号集合',
|
||||
@@ -112,7 +112,7 @@ CREATE TABLE ll_cargo_discard_application (
|
||||
review_remark varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核备注',
|
||||
review_idempotency_key varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核幂等键',
|
||||
review_request_hash char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核请求内容哈希',
|
||||
reviewed_at datetime(3) DEFAULT NULL COMMENT '审核时间', completed_at datetime(3) DEFAULT NULL COMMENT '废弃完成时间',
|
||||
reviewed_at datetime(3) DEFAULT NULL COMMENT '审核时间',
|
||||
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
-- 货物废弃申请 + 废弃出库单字段补齐
|
||||
-- 目标数据库:baiyue_lastleg
|
||||
-- MySQL 8.0;脚本可重复执行,已存在的字段/索引会跳过。
|
||||
USE baiyue_lastleg;
|
||||
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='warehouse_pallet_id'),
|
||||
'ALTER TABLE ll_outbound_pallet_item ADD COLUMN warehouse_pallet_id BIGINT NULL COMMENT ''仓库实物托盘ID'' AFTER outbound_order_id',
|
||||
IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='warehouse_pallet_id' AND (is_nullable <> 'YES' OR COALESCE(column_comment,'') <> '仓库实物托盘ID')),
|
||||
'ALTER TABLE ll_outbound_pallet_item MODIFY COLUMN warehouse_pallet_id BIGINT NULL COMMENT ''仓库实物托盘ID''', 'SELECT 1'));
|
||||
PREPARE outbound_pallet_schema FROM @ddl; EXECUTE outbound_pallet_schema; DEALLOCATE PREPARE outbound_pallet_schema;
|
||||
|
||||
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'),
|
||||
'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='completed_at'),
|
||||
'ALTER TABLE ll_cargo_discard_application ADD COLUMN completed_at DATETIME(3) NULL COMMENT ''废弃完成时间'' AFTER reviewed_at', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='order_id' AND is_nullable='NO'),
|
||||
'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN order_id BIGINT NULL COMMENT ''一件代发订单ID''', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='profile_id' AND is_nullable='NO'),
|
||||
'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN profile_id BIGINT NULL COMMENT ''托盘档案ID''', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='sku_inventory_id' AND is_nullable='NO'),
|
||||
'ALTER TABLE ll_cargo_discard_item MODIFY COLUMN sku_inventory_id BIGINT NULL COMMENT ''SKU库存记录ID''', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='source_type'),
|
||||
'ALTER TABLE ll_cargo_discard_item ADD COLUMN source_type VARCHAR(32) NOT NULL DEFAULT ''DROPSHIP'' COMMENT ''库存来源:DROPSHIP/RETURN_CARTON'' AFTER order_id', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='return_carton_id'),
|
||||
'ALTER TABLE ll_cargo_discard_item ADD COLUMN return_carton_id BIGINT NULL COMMENT ''退货库存箱ID'' AFTER source_type', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='actual_discard_packages'),
|
||||
'ALTER TABLE ll_cargo_discard_item ADD COLUMN actual_discard_packages INT NULL COMMENT ''PDA确认实际废弃箱数'' AFTER discard_packages', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_status'),
|
||||
'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_status VARCHAR(32) NOT NULL DEFAULT ''PENDING'' COMMENT ''PDA状态:PENDING/COMPLETED'' AFTER actual_discard_packages', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_operator_id'),
|
||||
'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_operator_id VARCHAR(128) NULL COMMENT ''PDA操作人'' AFTER pda_status', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND column_name='pda_completed_at'),
|
||||
'ALTER TABLE ll_cargo_discard_item ADD COLUMN pda_completed_at DATETIME(3) NULL COMMENT ''PDA完成时间'' AFTER pda_operator_id', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_item' AND index_name='idx_ll_cargo_discard_return_carton'),
|
||||
'ALTER TABLE ll_cargo_discard_item ADD KEY idx_ll_cargo_discard_return_carton(customer_id,return_carton_id)', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='outbound_type'),
|
||||
'ALTER TABLE ll_outbound_order ADD COLUMN outbound_type VARCHAR(32) NOT NULL DEFAULT ''DISPATCH_DELIVERY'' COMMENT ''出库类型'' AFTER status', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='recipient_info'),
|
||||
'ALTER TABLE ll_outbound_order ADD COLUMN recipient_info VARCHAR(1000) NULL COMMENT ''收件/业务说明'' AFTER lx_codes', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='request_hash'),
|
||||
'ALTER TABLE ll_outbound_order ADD COLUMN request_hash CHAR(64) NULL COMMENT ''请求摘要'' AFTER recipient_info', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='sku_code'),
|
||||
'ALTER TABLE ll_outbound_pallet_item ADD COLUMN sku_code VARCHAR(128) NULL COMMENT ''SKU''', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pallet_item' AND column_name='carton_marks'),
|
||||
'ALTER TABLE ll_outbound_pallet_item ADD COLUMN carton_marks VARCHAR(4000) NULL COMMENT ''箱唛快照'' AFTER sku_code', 'SELECT 1');
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- 退货库存废弃需要允许中间锁定状态;若本地已有约束,按名称重建。
|
||||
SET @ddl = 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 s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED'));
|
||||
|
||||
SELECT 'cargo discard/outbound schema patch completed' AS result, DATABASE() AS database_name;
|
||||
+21
-1
@@ -43,7 +43,7 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
Long findEffectiveVersionId(@Param("customerId") long customerId);
|
||||
|
||||
@Select("""
|
||||
SELECT c.price_book_version_id AS priceBookVersionId,
|
||||
SELECT catalog.id AS channelCatalogId, c.price_book_version_id AS priceBookVersionId,
|
||||
c.channel_code AS channelCode, c.channel_name AS channelName,
|
||||
c.price_model AS priceModel,
|
||||
c.estimated_departure_date AS estimatedDepartureDate,
|
||||
@@ -53,6 +53,8 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
c.surcharge_description AS surchargeDescription,
|
||||
c.remark AS remark
|
||||
FROM fl_price_book_channel c
|
||||
JOIN fl_price_channel_catalog catalog
|
||||
ON catalog.channel_code = c.channel_code AND catalog.status = 'ACTIVE'
|
||||
WHERE c.price_book_version_id = #{versionId}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM fl_carrier_fba_rate r
|
||||
@@ -66,6 +68,24 @@ public interface PriceBookQueryMapper extends PriceBookQueryReader {
|
||||
List<ChannelRow> findChannels(@Param("versionId") long versionId,
|
||||
@Param("originWarehouseId") long originWarehouseId);
|
||||
|
||||
@Select("""
|
||||
SELECT catalog.id AS channelCatalogId, c.price_book_version_id AS priceBookVersionId,
|
||||
c.channel_code AS channelCode, c.channel_name AS channelName,
|
||||
c.price_model AS priceModel,
|
||||
c.estimated_departure_date AS estimatedDepartureDate,
|
||||
c.estimated_departure_text AS estimatedDepartureText,
|
||||
c.latest_warehouse_entry AS latestWarehouseEntry,
|
||||
c.reference_transit AS referenceTransit,
|
||||
c.surcharge_description AS surchargeDescription,
|
||||
c.remark AS remark
|
||||
FROM fl_price_book_channel c
|
||||
JOIN fl_price_channel_catalog catalog
|
||||
ON catalog.channel_code = c.channel_code AND catalog.status = 'ACTIVE'
|
||||
WHERE c.price_book_version_id = #{versionId}
|
||||
ORDER BY c.id
|
||||
""")
|
||||
List<ChannelRow> findAllChannels(@Param("versionId") long versionId);
|
||||
|
||||
@Select("""
|
||||
SELECT r.channel_code AS channelCode, r.pricing_tax_mode AS pricingTaxMode
|
||||
FROM fl_carrier_fba_rate r
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ public class PriceBookQueryService implements PriceBookQueryUseCase {
|
||||
@Override public List<ChannelRow> channels(long versionId, long originWarehouseId) {
|
||||
return reader.findChannels(versionId, originWarehouseId);
|
||||
}
|
||||
@Override public List<ChannelRow> allChannels(long versionId) { return reader.findAllChannels(versionId); }
|
||||
@Override public List<ChannelTaxModeRow> channelTaxModes(long versionId, long originWarehouseId) {
|
||||
return reader.findChannelTaxModes(versionId, originWarehouseId);
|
||||
}
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ public final class PriceBookQueryRows {
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ChannelRow {
|
||||
private Long channelCatalogId;
|
||||
private Long priceBookVersionId;
|
||||
private String channelCode;
|
||||
private String channelName;
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ public interface PriceBookQueryUseCase {
|
||||
Long activeVersionId();
|
||||
Long effectiveVersionId(long customerId);
|
||||
List<ChannelRow> channels(long versionId, long originWarehouseId);
|
||||
List<ChannelRow> allChannels(long versionId);
|
||||
List<ChannelTaxModeRow> channelTaxModes(long versionId, long originWarehouseId);
|
||||
List<RateRow> fbaRates(long versionId, long originWarehouseId, String channelCode,
|
||||
String destinationCode, String pricingTaxMode);
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ public interface PriceBookQueryReader {
|
||||
Long findActiveVersionId();
|
||||
Long findEffectiveVersionId(long customerId);
|
||||
List<ChannelRow> findChannels(long versionId, long originWarehouseId);
|
||||
List<ChannelRow> findAllChannels(long versionId);
|
||||
List<ChannelTaxModeRow> findChannelTaxModes(long versionId, long originWarehouseId);
|
||||
List<RateRow> findFbaRates(long versionId, long originWarehouseId, String channelCode,
|
||||
String destinationCode, String pricingTaxMode);
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import java.util.List;
|
||||
public final class SailingCalendarDtos {
|
||||
private SailingCalendarDtos() {}
|
||||
|
||||
public record ChannelQuery(@Size(max = 64) String keyword, String status) {}
|
||||
public record ChannelQuery(@Size(max = 64) String keyword, String status, @Positive Long warehouseId) {}
|
||||
public record ChannelData(@NotBlank @Size(max = 64) String channelCode,
|
||||
@NotBlank @Size(max = 128) String channelName,
|
||||
@NotBlank String sourceType,
|
||||
|
||||
+52
-25
@@ -29,6 +29,8 @@ import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingAssignmentEntity;
|
||||
import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingBookingLinkEntity;
|
||||
import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingInboundVolumeForecastEntity;
|
||||
import com.baiyue.firstleg.sailingcalendar.fact.entity.SailingReceiptBatchFactEntity;
|
||||
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
|
||||
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
|
||||
import com.baiyue.firstleg.sailingcalendar.mapper.SailingCalendarDisplayMapper;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
@@ -41,30 +43,30 @@ public class SailingCalendarService {
|
||||
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final String OPEN = "OPEN";
|
||||
@Resource private SailingCalendarRepository repository;
|
||||
@Resource private PriceBookQueryUseCase priceBooks;
|
||||
|
||||
public List<ChannelView> channels(ChannelQuery query) {
|
||||
return repository.channels(query).stream().map(v -> new ChannelView(v, "ACTIVE".equals(v.getStatus()))).toList();
|
||||
Long versionId = priceBooks.activeVersionId();
|
||||
if (versionId == null || "INACTIVE".equalsIgnoreCase(trim(query.status()))) return List.of();
|
||||
List<ChannelRow> rows = query.warehouseId() == null
|
||||
? priceBooks.allChannels(versionId)
|
||||
: priceBooks.channels(versionId, query.warehouseId());
|
||||
String keyword = trimUpper(query.keyword());
|
||||
return rows.stream()
|
||||
.filter(row -> !StringUtils.hasText(keyword)
|
||||
|| (row.getChannelCode() != null && row.getChannelCode().toUpperCase().contains(keyword))
|
||||
|| (row.getChannelName() != null && row.getChannelName().toUpperCase().contains(keyword)))
|
||||
.map(this::priceBookChannel)
|
||||
.map(v -> new ChannelView(v, false))
|
||||
.toList();
|
||||
}
|
||||
@Transactional
|
||||
public SailingChannelEntity createChannel(long operatorId, String operatorName, ChannelCreateRequest request) {
|
||||
if (version(request.expectedVersion()) != 0) throw bad("新建渠道expectedVersion必须为0");
|
||||
validateChannel(request.data());
|
||||
if (repository.channelByCode(request.data().channelCode().trim().toUpperCase()) != null) throw conflict("渠道代码已存在");
|
||||
SailingChannelEntity value = new SailingChannelEntity(); value.setId(IdWorker.getId());
|
||||
copy(value, request.data()); value.setStatus("ACTIVE"); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0); now(value);
|
||||
try { repository.insert(value); } catch (DuplicateKeyException ex) { throw conflict("渠道代码已存在"); }
|
||||
return value;
|
||||
throw conflict("船期渠道来自当前生效船期价格表,不能手工维护");
|
||||
}
|
||||
@Transactional
|
||||
public SailingChannelEntity updateChannel(long id, long operatorId, String operatorName, ChannelUpdateRequest request) {
|
||||
SailingChannelEntity before = requiredChannel(id); validateChannel(request.data());
|
||||
if (!before.getChannelCode().equalsIgnoreCase(request.data().channelCode().trim())
|
||||
&& repository.channelByCode(request.data().channelCode().trim().toUpperCase()) != null) throw conflict("渠道代码已存在");
|
||||
SailingChannelEntity value = new SailingChannelEntity(); value.setId(id); copy(value, request.data());
|
||||
value.setStatus(normalizeStatus(request.status(), "渠道状态"));
|
||||
if (repository.update(value, version(request.expectedVersion()), operatorId, operatorName) != 1) throw conflict("渠道已被其他人修改");
|
||||
return requiredChannel(id);
|
||||
throw conflict("船期渠道来自当前生效船期价格表,不能手工维护");
|
||||
}
|
||||
|
||||
public PageResult<SailingTemplateEntity> templates(TemplateQuery query) { return repository.templates(query); }
|
||||
@@ -73,7 +75,7 @@ public class SailingCalendarService {
|
||||
@Transactional
|
||||
public SailingTemplateEntity createTemplate(long operatorId, String operatorName, TemplateCreateRequest request) {
|
||||
if (version(request.expectedVersion()) != 0) throw bad("新建模板expectedVersion必须为0");
|
||||
SailingChannelEntity channel = requiredChannel(request.data().channelId()); validateTemplate(request.data(), channel);
|
||||
SailingChannelEntity channel = resolvePriceBookChannel(request.data().channelId(), request.data().warehouseId(), null); validateTemplate(request.data(), channel);
|
||||
if (!"ACTIVE".equals(channel.getStatus())) throw conflict("停用渠道不能创建模板");
|
||||
SailingTemplateEntity value = new SailingTemplateEntity(); value.setId(IdWorker.getId()); value.setTemplateNo("ST-" + value.getId());
|
||||
copy(value, request.data(), channel); value.setStatus("ACTIVE"); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
@@ -82,7 +84,7 @@ public class SailingCalendarService {
|
||||
}
|
||||
@Transactional
|
||||
public SailingTemplateEntity updateTemplate(long id, long operatorId, String operatorName, TemplateUpdateRequest request) {
|
||||
requiredTemplate(id); SailingChannelEntity channel = requiredChannel(request.data().channelId()); validateTemplate(request.data(), channel);
|
||||
requiredTemplate(id); SailingChannelEntity channel = resolvePriceBookChannel(request.data().channelId(), request.data().warehouseId(), null); validateTemplate(request.data(), channel);
|
||||
SailingTemplateEntity value = new SailingTemplateEntity(); value.setId(id); copy(value, request.data(), channel);
|
||||
value.setStatus(StringUtils.hasText(request.status()) ? normalizeStatus(request.status(), "模板状态") : requiredTemplate(id).getStatus());
|
||||
if (repository.update(value, version(request.expectedVersion()), operatorId, operatorName) != 1) throw conflict("模板已被其他人修改");
|
||||
@@ -196,8 +198,8 @@ public class SailingCalendarService {
|
||||
throw bad("预计体积必须大于0");
|
||||
}
|
||||
String channelCode = trimUpper(request.channelCode());
|
||||
SailingChannelEntity channel = StringUtils.hasText(channelCode) ? repository.channelByCode(channelCode) : null;
|
||||
if (channel == null || !"ACTIVE".equals(channel.getStatus())) throw bad("预报必须填写已启用的渠道编码");
|
||||
SailingChannelEntity channel = StringUtils.hasText(channelCode) ? resolvePriceBookChannelByCode(request.receivingWarehouseId(), channelCode) : null;
|
||||
if (channel == null) throw bad("预报必须填写当前生效船期价格表中的渠道编码");
|
||||
String departurePort = trimUpper(request.departurePortCode());
|
||||
String destinationPort = trimUpper(request.destinationPortCode());
|
||||
if (!StringUtils.hasText(departurePort)) departurePort = trimUpper(channel.getDefaultDeparturePortCode());
|
||||
@@ -225,8 +227,8 @@ public class SailingCalendarService {
|
||||
if (request.forecastVolumeCbm().signum() <= 0) throw bad("预计体积必须大于0");
|
||||
String channelCode = trimUpper(request.channelCode());
|
||||
if (!StringUtils.hasText(channelCode)) channelCode = current.getChannelCode();
|
||||
SailingChannelEntity channel = repository.channelByCode(channelCode);
|
||||
if (channel == null || !"ACTIVE".equals(channel.getStatus())) throw bad("预报必须填写已启用的渠道编码");
|
||||
SailingChannelEntity channel = resolvePriceBookChannelByCode(current.getReceivingWarehouseId(), channelCode);
|
||||
if (channel == null) throw bad("预报必须填写当前生效船期价格表中的渠道编码");
|
||||
String departurePort = trimUpper(request.departurePortCode());
|
||||
String destinationPort = trimUpper(request.destinationPortCode());
|
||||
if (!StringUtils.hasText(departurePort)) departurePort = trimUpper(channel.getDefaultDeparturePortCode());
|
||||
@@ -391,7 +393,7 @@ public class SailingCalendarService {
|
||||
@Transactional
|
||||
public List<SailingEntity> generate(long operatorId, String operatorName, long templateId, LocalDate from, LocalDate to) {
|
||||
SailingTemplateEntity template = requiredTemplate(templateId); if (!"ACTIVE".equals(template.getStatus())) throw conflict("停用模板不能生成船期");
|
||||
SailingChannelEntity channel = requiredChannel(template.getChannelId());
|
||||
SailingChannelEntity channel = resolvePriceBookChannel(template.getChannelId(), template.getWarehouseId(), template.getChannelCodeSnapshot());
|
||||
if (!"ACTIVE".equals(channel.getStatus())) throw conflict("停用渠道不能生成船期");
|
||||
validateTemplateSnapshot(template);
|
||||
List<SailingEntity> result = new java.util.ArrayList<>();
|
||||
@@ -473,8 +475,33 @@ public class SailingCalendarService {
|
||||
if (d.etaAt() != null && d.etaAt().isBefore(d.etdAt())) throw bad("ETA不能早于ETD");
|
||||
if (d.cutoffAt() == null || !d.cutoffAt().isBefore(d.etdAt())) throw bad("收货截止必须早于开船时间");
|
||||
}
|
||||
private SailingChannelEntity resolveChannel(SailingData d) { if (d.channelId() != null) return requiredChannel(d.channelId()); if (!StringUtils.hasText(d.channelCode())) throw bad("必须选择价格表渠道"); SailingChannelEntity v = repository.channelByCode(d.channelCode().trim().toUpperCase()); if (v == null) throw notFound("渠道不存在"); return v; }
|
||||
private SailingChannelEntity requiredChannel(long id) { SailingChannelEntity v = repository.channel(id); if (v == null) throw notFound("渠道不存在"); return v; }
|
||||
private SailingChannelEntity resolveChannel(SailingData d) {
|
||||
return resolvePriceBookChannel(d.channelId(), d.warehouseId(), d.channelCode());
|
||||
}
|
||||
private SailingChannelEntity resolvePriceBookChannel(Long channelId, Long warehouseId, String channelCode) {
|
||||
if (warehouseId == null) throw bad("必须选择收货仓后选择价格表渠道");
|
||||
Long versionId = priceBooks.activeVersionId();
|
||||
if (versionId == null) throw conflict("当前没有生效的船期价格表");
|
||||
List<ChannelRow> rows = priceBooks.channels(versionId, warehouseId);
|
||||
String normalized = trimUpper(channelCode);
|
||||
ChannelRow row = rows.stream()
|
||||
.filter(v -> channelId != null && Objects.equals(v.getChannelCatalogId(), channelId))
|
||||
.findFirst()
|
||||
.orElseGet(() -> rows.stream()
|
||||
.filter(v -> normalized != null && normalized.equalsIgnoreCase(v.getChannelCode()))
|
||||
.findFirst().orElse(null));
|
||||
if (row == null) throw conflict("渠道必须来自当前生效船期价格表,且在当前收货仓有有效报价");
|
||||
return priceBookChannel(row);
|
||||
}
|
||||
private SailingChannelEntity resolvePriceBookChannelByCode(Long warehouseId, String channelCode) {
|
||||
return resolvePriceBookChannel(null, warehouseId, channelCode);
|
||||
}
|
||||
private SailingChannelEntity priceBookChannel(ChannelRow row) {
|
||||
SailingChannelEntity value = new SailingChannelEntity();
|
||||
value.setId(row.getChannelCatalogId()); value.setChannelCode(trimUpper(row.getChannelCode())); value.setChannelName(trim(row.getChannelName()));
|
||||
value.setSourceType("PRICE_BOOK"); value.setDefaultCutoffDays(3); value.setStatus("ACTIVE"); value.setRemark(row.getRemark()); value.setVersion(0);
|
||||
return value;
|
||||
}
|
||||
private SailingTemplateEntity requiredTemplate(long id) { SailingTemplateEntity v = repository.template(id); if (v == null) throw notFound("船期模板不存在"); return v; }
|
||||
private SailingEntity requiredSailing(long id) { SailingEntity v = repository.sailing(id); if (v == null) throw notFound("船期不存在"); return v; }
|
||||
private boolean isCopyOf(SailingEntity value, String reason) {
|
||||
|
||||
+1
-3
@@ -87,9 +87,7 @@ public class CargoDiscardController {
|
||||
|
||||
private void requireApprovalPermission(AuthenticatedUser user) {
|
||||
if (user == null || user.permissions() == null
|
||||
|| !(user.permissions().contains(CargoDiscardService.APPROVAL_PERMISSION)
|
||||
|| user.permissions().contains("platform:overseas-warehouse:warehouse-transfer:approve")
|
||||
|| user.permissions().contains("platform:overseas-warehouse:discard:approve"))) {
|
||||
|| !user.permissions().contains(CargoDiscardService.APPROVAL_PERMISSION)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cargo discard approval permission is required");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,10 +14,10 @@ public final class CargoDiscardResponses {
|
||||
List<CandidateSku> skus) {}
|
||||
public record CandidateSku(String skuCode, String productName, long availablePackages) {}
|
||||
|
||||
public record Summary(Long id, String discardNo, String outboundNo, String inboundCodes,
|
||||
public record Summary(Long id, String discardNo, String inboundCodes,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
int totalPackages, BigDecimal totalFee, String currency, String status,
|
||||
String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt, LocalDateTime completedAt) {}
|
||||
String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt) {}
|
||||
|
||||
public record Item(Long id, Long orderId, String inboundCode, String skuCode,
|
||||
String productName, int availablePackagesSnapshot, int discardPackages) {}
|
||||
|
||||
-2
@@ -14,7 +14,6 @@ public class CargoDiscardApplication {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String discardNo;
|
||||
private String outboundNo;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseName;
|
||||
private String inboundCodes;
|
||||
@@ -30,7 +29,6 @@ public class CargoDiscardApplication {
|
||||
private String reviewedBy;
|
||||
private String reviewRemark;
|
||||
private LocalDateTime reviewedAt;
|
||||
private LocalDateTime completedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+5
-91
@@ -16,14 +16,8 @@ import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper;
|
||||
import com.baiyue.lastleg.discard.mapper.CargoDiscardStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.discard.model.CargoDiscardCandidateRow;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
|
||||
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
|
||||
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
@@ -53,8 +47,6 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class CargoDiscardService {
|
||||
@@ -76,9 +68,6 @@ public class CargoDiscardService {
|
||||
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
|
||||
@Resource private OutboundOrderMapper outboundOrders;
|
||||
@Resource private OutboundPalletItemMapper outboundPalletItems;
|
||||
@Resource private DropshipOrderProfileMapper profiles;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@@ -280,10 +269,6 @@ public class CargoDiscardService {
|
||||
release(customerId, links, applicationItems);
|
||||
}
|
||||
|
||||
if (APPROVED.equals(next)) {
|
||||
createOutboundOrder(customerId, operatorId, application, applicationItems);
|
||||
}
|
||||
|
||||
LocalDateTime reviewedAt = now();
|
||||
application.setStatus(next); application.setReviewedBy(operatorId);
|
||||
application.setReviewRemark(trim(request.remark())); application.setReviewedAt(reviewedAt);
|
||||
@@ -292,9 +277,6 @@ public class CargoDiscardService {
|
||||
if (linked != null) {
|
||||
String linkedStatus = mainOrderStatus(next);
|
||||
linked.setCurrentStatus(linkedStatus); linked.setVersion((linked.getVersion() == null ? 0 : linked.getVersion()) + 1); linked.setUpdatedAt(reviewedAt);
|
||||
if (APPROVED.equals(next) && StringUtils.hasText(application.getOutboundNo())) {
|
||||
linked.setOutboundOrderNo(application.getOutboundNo());
|
||||
}
|
||||
orders.updateById(linked);
|
||||
LastlegOrderStatusHistory orderHistory = new LastlegOrderStatusHistory(); orderHistory.setId(IdWorker.getId());
|
||||
orderHistory.setCustomerId(customerId); orderHistory.setOrderId(linked.getId()); orderHistory.setFromStatus(PENDING_REVIEW);
|
||||
@@ -305,68 +287,6 @@ public class CargoDiscardService {
|
||||
return detail(customerId, discardNo);
|
||||
}
|
||||
|
||||
private void createOutboundOrder(String customerId, String operatorId, CargoDiscardApplication application,
|
||||
List<CargoDiscardItem> applicationItems) {
|
||||
// Unit tests that exercise the approval state transition without the delivery module
|
||||
// do not wire these optional persistence collaborators; the running service always does.
|
||||
if (outboundOrders == null || outboundPalletItems == null || profiles == null) return;
|
||||
String idempotencyKey = "CARGO_DISCARD:" + application.getId();
|
||||
OutboundOrder existing = outboundOrders.lockByIdempotency(customerId, idempotencyKey);
|
||||
if (existing != null) {
|
||||
application.setOutboundNo(existing.getOutboundNo());
|
||||
return;
|
||||
}
|
||||
LocalDateTime now = now();
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId());
|
||||
outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("DISCARD_OUTBOUND", application.getOverseasWarehouseId(), outbound.getId()));
|
||||
outbound.setOverseasWarehouseId(application.getOverseasWarehouseId());
|
||||
outbound.setOutboundType("CARGO_DISCARD_OUTBOUND");
|
||||
outbound.setStatus("WAIT_PICK");
|
||||
outbound.setRecipientInfo("货物废弃:" + application.getDiscardNo());
|
||||
outbound.setPackageCount(applicationItems.stream().mapToInt(i -> Objects.requireNonNullElse(i.getDiscardPackages(), 0)).sum());
|
||||
outbound.setPalletCount(applicationItems.stream().map(this::physicalKey).collect(Collectors.toSet()).size());
|
||||
outbound.setIdempotencyKey(idempotencyKey);
|
||||
outbound.setCreatedBy(operatorId);
|
||||
outbound.setCreatedAt(now);
|
||||
outbound.setUpdatedAt(now);
|
||||
outboundOrders.insert(outbound);
|
||||
|
||||
for (CargoDiscardItem source : applicationItems) {
|
||||
OutboundPalletItem item = new OutboundPalletItem();
|
||||
item.setId(IdWorker.getId());
|
||||
item.setCustomerId(customerId);
|
||||
item.setOutboundOrderId(outbound.getId());
|
||||
item.setPackageCount(Objects.requireNonNullElse(source.getDiscardPackages(), 0));
|
||||
item.setPickedPackageCount(0);
|
||||
item.setStatus("WAIT_PICK");
|
||||
item.setSkuCode(source.getSkuCode());
|
||||
item.setInboundCode(source.getInboundCode());
|
||||
item.setOrderRefs(application.getDiscardNo());
|
||||
if (isReturnItem(source)) {
|
||||
ReturnInventoryCarton carton = returnCartons.selectById(source.getReturnCartonId());
|
||||
item.setPalletNo(carton == null ? source.getInboundCode() : firstNonBlank(carton.getPalletCode(), carton.getCartonMark()));
|
||||
item.setCartonMarks(carton == null ? null : carton.getCartonMark());
|
||||
} else {
|
||||
DropshipOrderProfile profile = profiles.selectById(source.getProfileId());
|
||||
if (profile != null) {
|
||||
item.setWarehousePalletId(profile.getWarehousePalletId());
|
||||
item.setPalletNo(firstNonBlank(profile.getPalletNo(), profile.getSourcePalletNo()));
|
||||
}
|
||||
}
|
||||
item.setCreatedAt(now);
|
||||
outboundPalletItems.insert(item);
|
||||
}
|
||||
application.setOutboundNo(outbound.getOutboundNo());
|
||||
application.setUpdatedAt(now);
|
||||
applications.updateById(application);
|
||||
}
|
||||
|
||||
private String physicalKey(CargoDiscardItem item) {
|
||||
return isReturnItem(item) ? "RETURN:" + item.getReturnCartonId() : "DROPSHIP:" + item.getProfileId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CargoDiscardResponses.Detail complete(String customerId, String discardNo, String operatorId) {
|
||||
CargoDiscardApplication application = applications.lockNo(customerId, discardNo);
|
||||
@@ -379,9 +299,7 @@ public class CargoDiscardService {
|
||||
|
||||
public void updateLinkedStatus(String customerId, CargoDiscardApplication application, String from, String to,
|
||||
String operatorId, String remark) {
|
||||
LocalDateTime at = now(); application.setStatus(to); application.setUpdatedAt(at);
|
||||
if (COMPLETED.equals(to)) application.setCompletedAt(at);
|
||||
applications.updateById(application);
|
||||
LocalDateTime at = now(); application.setStatus(to); application.setUpdatedAt(at); applications.updateById(application);
|
||||
LastlegOrder linked = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId());
|
||||
if (linked != null) {
|
||||
String linkedFrom = mainOrderStatus(from);
|
||||
@@ -502,26 +420,22 @@ public class CargoDiscardService {
|
||||
return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null;
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String first, String second) {
|
||||
return first != null && !first.isBlank() ? first : second;
|
||||
}
|
||||
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
|
||||
private LastlegOrder requireDropshipOrder(String customerId, Long orderId) {
|
||||
LastlegOrder order = orders.selectById(orderId);
|
||||
if (order == null || !customerId.equals(order.getCustomerId())) throw notFound("Inbound order not found");
|
||||
if (!StringUtils.hasText(order.getOverseasWarehouseId())) {
|
||||
throw conflict("Cargo discard inventory is not linked to an overseas warehouse");
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) {
|
||||
throw conflict("Cargo discard currently requires SKU/carton inventory from a one-click dropship order");
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
private CargoDiscardResponses.Summary summary(CargoDiscardApplication value) {
|
||||
return new CargoDiscardResponses.Summary(value.getId(), value.getDiscardNo(), value.getOutboundNo(), value.getInboundCodes(),
|
||||
return new CargoDiscardResponses.Summary(value.getId(), value.getDiscardNo(), value.getInboundCodes(),
|
||||
value.getOverseasWarehouseId(), value.getOverseasWarehouseName(), value.getTotalPackages(),
|
||||
value.getTotalFee(), value.getCurrency(), value.getStatus(), value.getCreatedBy(), value.getCreatedAt(),
|
||||
value.getReviewedAt(), value.getCompletedAt());
|
||||
value.getReviewedAt());
|
||||
}
|
||||
|
||||
private void history(String customerId, Long applicationId, String from, String to, String operatorId,
|
||||
|
||||
@@ -122,10 +122,7 @@ public final class RelabelRequests {
|
||||
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String relabelNo,
|
||||
String customerOrderNo, String skuCode, String status,
|
||||
String overseasWarehouseId, LocalDateTime createdFrom, LocalDateTime createdTo,
|
||||
LocalDateTime reviewedFrom, LocalDateTime reviewedTo,
|
||||
LocalDateTime completedFrom, LocalDateTime completedTo,
|
||||
LocalDateTime outboundFrom, LocalDateTime outboundTo) {
|
||||
String overseasWarehouseId, LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ public final class RelabelResponses {
|
||||
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
|
||||
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
||||
String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt,
|
||||
String reviewChannel, String reviewRemark, LocalDateTime reviewedAt,
|
||||
LocalDateTime relabelCompletedAt, LocalDateTime outboundAt, LocalDateTime createdAt) {}
|
||||
String reviewChannel, String reviewRemark, LocalDateTime createdAt) {}
|
||||
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
int notifiedPackageCount, String shipmentNo, String trackingCode,
|
||||
|
||||
+2
-24
@@ -502,12 +502,6 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
trim(query.overseasWarehouseId()))
|
||||
.ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo())
|
||||
.apply(query.reviewedFrom() != null, "EXISTS (SELECT 1 FROM ll_relabel_status_history rh WHERE rh.application_id=ll_relabel_application.id AND rh.customer_id=ll_relabel_application.customer_id AND rh.to_status IN ('PENDING_PROCESSING','PROCESSING','REJECTED') AND rh.created_at >= {0})", query.reviewedFrom())
|
||||
.apply(query.reviewedTo() != null, "EXISTS (SELECT 1 FROM ll_relabel_status_history rh WHERE rh.application_id=ll_relabel_application.id AND rh.customer_id=ll_relabel_application.customer_id AND rh.to_status IN ('PENDING_PROCESSING','PROCESSING','REJECTED') AND rh.created_at <= {0})", query.reviewedTo())
|
||||
.apply(query.completedFrom() != null, "EXISTS (SELECT 1 FROM ll_relabel_pda_task pt WHERE pt.application_id=ll_relabel_application.id AND pt.customer_id=ll_relabel_application.customer_id AND pt.reviewed_at >= {0})", query.completedFrom())
|
||||
.apply(query.completedTo() != null, "EXISTS (SELECT 1 FROM ll_relabel_pda_task pt WHERE pt.application_id=ll_relabel_application.id AND pt.customer_id=ll_relabel_application.customer_id AND pt.reviewed_at <= {0})", query.completedTo())
|
||||
.apply(query.outboundFrom() != null, "EXISTS (SELECT 1 FROM ll_outbound_order oo WHERE oo.customer_id=ll_relabel_application.customer_id AND oo.outbound_no=ll_relabel_application.outbound_order_no AND oo.created_at >= {0})", query.outboundFrom())
|
||||
.apply(query.outboundTo() != null, "EXISTS (SELECT 1 FROM ll_outbound_order oo WHERE oo.customer_id=ll_relabel_application.customer_id AND oo.outbound_no=ll_relabel_application.outbound_order_no AND oo.created_at <= {0})", query.outboundTo())
|
||||
.apply(StringUtils.hasText(query.skuCode()),
|
||||
"EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.customer_id=ll_relabel_application.customer_id AND ri.sku_code LIKE CONCAT('%',{0},'%'))",
|
||||
query.skuCode())
|
||||
@@ -1055,24 +1049,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(),
|
||||
value.getFulfillmentStatus(), value.getOutboundOrderNo(), value.getCustomerConfirmedAt(),
|
||||
order != null && "RELABEL_APPLICATION".equals(order.getOrderType()) ? "TAIL_ORDER" : "RELABEL_ORDER",
|
||||
null, reviewTime(value), completionTime(value), outboundTime(value), value.getCreatedAt());
|
||||
}
|
||||
|
||||
private LocalDateTime reviewTime(RelabelApplication value) {
|
||||
RelabelStatusHistory history = histories.selectOne(new LambdaQueryWrapper<RelabelStatusHistory>()
|
||||
.eq(RelabelStatusHistory::getApplicationId, value.getId()).eq(RelabelStatusHistory::getCustomerId, value.getCustomerId())
|
||||
.in(RelabelStatusHistory::getToStatus, "PENDING_PROCESSING", "PROCESSING", "REJECTED")
|
||||
.orderByDesc(RelabelStatusHistory::getCreatedAt).last("LIMIT 1"));
|
||||
return history == null ? null : history.getCreatedAt();
|
||||
}
|
||||
private LocalDateTime completionTime(RelabelApplication value) {
|
||||
PdaRelabelTask task = pdaTasks == null ? null : pdaTasks.find(value.getCustomerId(), value.getId());
|
||||
return task == null ? null : task.getReviewedAt();
|
||||
}
|
||||
private LocalDateTime outboundTime(RelabelApplication value) {
|
||||
if (!StringUtils.hasText(value.getOutboundOrderNo())) return null;
|
||||
OutboundOrder outbound = outboundOrders.findByNo(value.getCustomerId(), value.getOutboundOrderNo());
|
||||
return outbound == null ? null : outbound.getCreatedAt();
|
||||
null,
|
||||
value.getCreatedAt());
|
||||
}
|
||||
|
||||
@Override public boolean supports(String businessType) { return "RELABEL_ORDER".equals(businessType); }
|
||||
|
||||
-5
@@ -24,11 +24,6 @@ public class MerchantReturnOrderController {
|
||||
@PostMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE + "')")
|
||||
public ApiResponse<ReturnOrderResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ReturnOrderRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); }
|
||||
@PutMapping("/{returnNo}") @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE + "')")
|
||||
public ApiResponse<ReturnOrderResponses.Detail> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String returnNo, @Valid @RequestBody ReturnOrderRequests.Create request) {
|
||||
return ApiResponse.success(service.updatePending(customer(user), returnNo, request));
|
||||
}
|
||||
@GetMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RETURN_ORDER_LIST + "')")
|
||||
public ApiResponse<PageResult<ReturnOrderResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid ReturnOrderRequests.Query query) { return ApiResponse.success(service.page(customer(user), query)); }
|
||||
|
||||
+1
-4
@@ -168,10 +168,7 @@ public final class WarehouseTransferRequests {
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String transferNo, String inboundCode, String operatingWarehouseId,
|
||||
String fbaWarehouseCode, String status, Boolean needRelabel,
|
||||
LocalDateTime createdFrom, LocalDateTime createdTo,
|
||||
LocalDateTime reviewedFrom, LocalDateTime reviewedTo,
|
||||
LocalDateTime measuredFrom, LocalDateTime measuredTo,
|
||||
LocalDateTime outboundFrom, LocalDateTime outboundTo) {
|
||||
LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -31,9 +31,7 @@ public final class WarehouseTransferResponses {
|
||||
String destinationType, String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int packageCount, String deliveryMethod, boolean needRelabel, String relabelNo,
|
||||
String relabelApplicationStatus, String relabelFulfillmentStatus,
|
||||
String status, LocalDateTime estimatedArrivalAt, LocalDateTime estimatedShipAt,
|
||||
LocalDateTime actualShipAt, LocalDateTime reviewedAt, LocalDateTime measuredAt,
|
||||
LocalDateTime outboundAt, LocalDateTime relabelCompletedAt, LocalDateTime createdAt) {}
|
||||
String status, LocalDateTime estimatedArrivalAt, LocalDateTime createdAt) {}
|
||||
public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode,
|
||||
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
|
||||
String status,
|
||||
|
||||
-2
@@ -56,8 +56,6 @@ public class WarehouseTransferOrder {
|
||||
private String outboundIdempotencyKey;
|
||||
private String outboundRequestHash;
|
||||
private LocalDateTime outboundAt;
|
||||
private LocalDateTime measuredAt;
|
||||
private LocalDateTime actualShipAt;
|
||||
private String palletizeIdempotencyKey;
|
||||
private String palletizeRequestHash;
|
||||
private String confirmationIdempotencyKey;
|
||||
|
||||
+2
-2
@@ -20,10 +20,10 @@ import org.apache.ibatis.annotations.Update;
|
||||
WarehouseTransferOrder lockByRelabelNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE operating_warehouse_id=#{warehouseId} AND status='WAIT_PDA_RECEIVING' ORDER BY created_at,id")
|
||||
java.util.List<WarehouseTransferOrder> pdaCandidates(@Param("warehouseId") String warehouseId);
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},actual_ship_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL")
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL")
|
||||
int markOutboundByOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("outboundAt") java.time.LocalDateTime outboundAt);
|
||||
@Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.actual_ship_at=#{outboundAt},wto.updated_at=#{outboundAt} " +
|
||||
@Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.updated_at=#{outboundAt} " +
|
||||
"WHERE wto.customer_id=#{customerId} AND wto.relabel_no=#{relabelNo} AND wto.outbound_at IS NULL " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_pallet wtp " +
|
||||
"JOIN ll_inventory_position ip ON ip.customer_id=wtp.customer_id AND ip.pallet_id=wtp.warehouse_pallet_id " +
|
||||
|
||||
+26
-23
@@ -147,6 +147,12 @@ public class WarehouseTransferService {
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
// A split relabel transfer may intentionally defer the whole-order
|
||||
// destination: relabeled cartons go through relabel dispatch while the
|
||||
// remaining cartons are bound later from warehouse-transfer outbound.
|
||||
// All other transfer types must still select FBA or PRIVATE up front.
|
||||
validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(),
|
||||
request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty());
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
|
||||
@@ -266,8 +272,8 @@ public class WarehouseTransferService {
|
||||
WarehouseTransferRequests.Create request) {
|
||||
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
if (!Set.of(DRAFT, REJECTED).contains(transfer.getStatus())) {
|
||||
throw conflict("Only a draft or rejected warehouse transfer order can be edited");
|
||||
if (!DRAFT.equals(transfer.getStatus())) {
|
||||
throw conflict("Only a draft warehouse transfer order can be edited");
|
||||
}
|
||||
int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum();
|
||||
if (itemPackages != request.packageCount()) {
|
||||
@@ -277,6 +283,8 @@ public class WarehouseTransferService {
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
validateTransferDestinationRequired(request.destinationType(), request.fbaWarehouseCode(),
|
||||
request.needRelabel() && request.relabelItems() != null && !request.relabelItems().isEmpty());
|
||||
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -312,7 +320,7 @@ public class WarehouseTransferService {
|
||||
order.setVolumeCbm(request.volumeCbm());
|
||||
order.setEstimatedWarehouseAt(request.estimatedArrivalAt());
|
||||
order.setRelabelStatus(request.needRelabel() ? "PENDING_BINDING" : "NOT_REQUIRED");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus() + ";RELABEL:" + order.getRelabelStatus());
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + DRAFT + ";RELABEL:" + order.getRelabelStatus());
|
||||
order.setCustomerRemark(trim(request.remark()));
|
||||
order.setSourcePayloadHash("sha256:" + requestHash);
|
||||
order.setUpdatedAt(now);
|
||||
@@ -365,10 +373,7 @@ public class WarehouseTransferService {
|
||||
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo);
|
||||
String previousStatus = transfer.getStatus();
|
||||
if (!Set.of(DRAFT, REJECTED).contains(previousStatus)) {
|
||||
throw conflict("Only a draft or rejected warehouse transfer order can be submitted");
|
||||
}
|
||||
if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted");
|
||||
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -383,13 +388,12 @@ public class WarehouseTransferService {
|
||||
|
||||
LastlegInboundOrder inbound = transfer.getTransferNo().equals(transfer.getInboundCode())
|
||||
? inboundOrders.selectById(transfer.getInboundOrderId()) : null;
|
||||
if (inbound != null && customerId.equals(inbound.getCustomerId())
|
||||
&& Set.of(DRAFT, REJECTED, "CANCELLED").contains(inbound.getStatus())) {
|
||||
if (inbound != null && customerId.equals(inbound.getCustomerId()) && DRAFT.equals(inbound.getStatus())) {
|
||||
inbound.setStatus(PENDING_REVIEW);
|
||||
inbound.setUpdatedAt(now);
|
||||
inboundOrders.updateById(inbound);
|
||||
}
|
||||
insertOrderHistory(customerId, order.getId(), previousStatus, PENDING_REVIEW, transferNo,
|
||||
insertOrderHistory(customerId, order.getId(), DRAFT, PENDING_REVIEW, transferNo,
|
||||
"Warehouse transfer submitted for review by " + operatorId, now);
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
@@ -420,12 +424,6 @@ public class WarehouseTransferService {
|
||||
.eq(query.needRelabel() != null, WarehouseTransferOrder::getNeedRelabel, query.needRelabel())
|
||||
.ge(query.createdFrom() != null, WarehouseTransferOrder::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, WarehouseTransferOrder::getCreatedAt, query.createdTo())
|
||||
.ge(query.reviewedFrom() != null, WarehouseTransferOrder::getReviewedAt, query.reviewedFrom())
|
||||
.le(query.reviewedTo() != null, WarehouseTransferOrder::getReviewedAt, query.reviewedTo())
|
||||
.ge(query.measuredFrom() != null, WarehouseTransferOrder::getMeasuredAt, query.measuredFrom())
|
||||
.le(query.measuredTo() != null, WarehouseTransferOrder::getMeasuredAt, query.measuredTo())
|
||||
.ge(query.outboundFrom() != null, WarehouseTransferOrder::getOutboundAt, query.outboundFrom())
|
||||
.le(query.outboundTo() != null, WarehouseTransferOrder::getOutboundAt, query.outboundTo())
|
||||
.orderByDesc(WarehouseTransferOrder::getCreatedAt).orderByDesc(WarehouseTransferOrder::getId);
|
||||
Page<WarehouseTransferOrder> result = transfers.selectPage(Page.of(query.page(), query.size()), wrapper);
|
||||
Map<String, RelabelApplication> relabelApplicationsByNo = relabelApplications(result.getRecords());
|
||||
@@ -438,9 +436,7 @@ public class WarehouseTransferService {
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(),
|
||||
value.getStatus(),
|
||||
value.getEstimatedArrivalAt(), value.getEstimatedShipAt(), value.getActualShipAt(),
|
||||
value.getReviewedAt(), value.getMeasuredAt(), value.getOutboundAt(), value.getRelabelCompletedAt(),
|
||||
value.getCreatedAt())).toList();
|
||||
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
|
||||
return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
@@ -858,7 +854,7 @@ public class WarehouseTransferService {
|
||||
completedPalletItems, warehouse, now);
|
||||
completeTransferInboundSnapshot(customerId, transfer, persistedPallets, warehouse, now);
|
||||
completeTransferInboundPlan(customerId, transfer, persistedPallets, completedPalletItems, now);
|
||||
transfer.setStatus(WAIT_CUSTOMER_CONFIRMATION); transfer.setMeasuredAt(now); transfer.setPalletizeIdempotencyKey(request.idempotencyKey().trim());
|
||||
transfer.setStatus(WAIT_CUSTOMER_CONFIRMATION); transfer.setPalletizeIdempotencyKey(request.idempotencyKey().trim());
|
||||
transfer.setPalletizeRequestHash(requestHash); transfer.setUpdatedAt(now); transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus();
|
||||
order.setCurrentStatus(INBOUND_COMPLETED); order.setPalletCount(persistedPallets.size());
|
||||
@@ -1248,9 +1244,7 @@ public class WarehouseTransferService {
|
||||
relabel == null ? null : relabel.getStatus(),
|
||||
relabel == null ? null : relabel.getFulfillmentStatus(),
|
||||
value.getStatus(),
|
||||
value.getEstimatedArrivalAt(), value.getEstimatedShipAt(), value.getActualShipAt(),
|
||||
value.getReviewedAt(), value.getMeasuredAt(), value.getOutboundAt(), value.getRelabelCompletedAt(),
|
||||
value.getCreatedAt());
|
||||
value.getEstimatedArrivalAt(), value.getCreatedAt());
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -1430,6 +1424,15 @@ public class WarehouseTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTransferDestinationRequired(String destinationType, String fbaWarehouseCode,
|
||||
boolean allowDeferredForRelabel) {
|
||||
String type = trim(destinationType);
|
||||
if (type == null && StringUtils.hasText(fbaWarehouseCode)) type = "FBA";
|
||||
if ((type == null || "NONE".equalsIgnoreCase(type)) && !allowDeferredForRelabel) {
|
||||
throw badRequest("仓储转运必须选择 FBA 仓或私人仓");
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyDestination(WarehouseTransferOrder transfer, WarehouseTransferRequests.Create request) {
|
||||
String type = trim(request.destinationType());
|
||||
if (type == null && StringUtils.hasText(request.fbaWarehouseCode())) type = "FBA";
|
||||
|
||||
@@ -1628,10 +1628,6 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
|
||||
PREPARE transfer_outbound_hash_stmt FROM @ddl; EXECUTE transfer_outbound_hash_stmt; DEALLOCATE PREPARE transfer_outbound_hash_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_at DATETIME(3) NULL COMMENT ''出库时间'' AFTER outbound_request_hash'));
|
||||
PREPARE transfer_outbound_at_stmt FROM @ddl; EXECUTE transfer_outbound_at_stmt; DEALLOCATE PREPARE transfer_outbound_at_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='measured_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN measured_at DATETIME(3) NULL COMMENT ''PDA测量完成时间'' AFTER outbound_at'));
|
||||
PREPARE transfer_measured_at_stmt FROM @ddl; EXECUTE transfer_measured_at_stmt; DEALLOCATE PREPARE transfer_measured_at_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='actual_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN actual_ship_at DATETIME(3) NULL COMMENT ''实际发货时间'' AFTER measured_at'));
|
||||
PREPARE transfer_actual_ship_at_stmt FROM @ddl; EXECUTE transfer_actual_ship_at_stmt; DEALLOCATE PREPARE transfer_actual_ship_at_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='estimated_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN estimated_ship_at DATETIME(3) NULL COMMENT ''预计发货时间'' AFTER estimated_arrival_at'));
|
||||
PREPARE transfer_estimated_ship_stmt FROM @ddl; EXECUTE transfer_estimated_ship_stmt; DEALLOCATE PREPARE transfer_estimated_ship_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='pickup_contact_name'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN pickup_contact_name VARCHAR(128) NULL COMMENT ''提货联系人'' AFTER delivery_address'));
|
||||
@@ -2060,7 +2056,7 @@ CREATE TABLE IF NOT EXISTS ll_dropship_carton_ledger (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_cargo_discard_application (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', outbound_no VARCHAR(64) NULL COMMENT '废弃出库单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID',
|
||||
discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID',
|
||||
overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称快照', inbound_codes VARCHAR(2000) NOT NULL COMMENT '入仓单号集合',
|
||||
total_packages INT NOT NULL COMMENT '总箱数', fee_per_package DECIMAL(18,2) NOT NULL COMMENT '每箱处理费用',
|
||||
total_fee DECIMAL(18,2) NOT NULL COMMENT '费用合计', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种',
|
||||
@@ -2068,7 +2064,7 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_application (
|
||||
idempotency_key VARCHAR(255) NOT NULL COMMENT '业务幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容哈希',
|
||||
created_by VARCHAR(128) NULL COMMENT '创建人ID', reviewed_by VARCHAR(128) NULL COMMENT '审核人ID', review_remark VARCHAR(1000) NULL COMMENT '审核备注',
|
||||
review_idempotency_key VARCHAR(255) NULL COMMENT '审核幂等键', review_request_hash CHAR(64) NULL COMMENT '审核请求内容哈希',
|
||||
reviewed_at DATETIME(3) NULL COMMENT '审核时间', completed_at DATETIME(3) NULL COMMENT '废弃完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
reviewed_at DATETIME(3) NULL COMMENT '审核时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_cargo_discard_no(customer_id,discard_no),
|
||||
UNIQUE KEY uk_ll_cargo_discard_key(customer_id,idempotency_key),
|
||||
|
||||
@@ -1556,13 +1556,6 @@ paths:
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: returnNo, in: path, required: true, schema: { type: string } }]
|
||||
responses: { '200': { description: 当前客户退货订单详情 }, '404': { description: 当前客户下退货订单不存在 } }
|
||||
put:
|
||||
summary: 客户编辑待审核退货预报
|
||||
description: 权限 merchant:overseas-warehouse:return-order:create;仅允许修改当前客户且仍处于 PENDING_REVIEW 的退货预报,SKU 预报明细会整体替换。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: returnNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ReturnOrderCreate' } } } }
|
||||
responses: { '200': { description: 更新后的退货订单详情 }, '404': { description: 当前客户下退货订单不存在 }, '409': { description: 退货预报已不处于待审核状态 } }
|
||||
/api/merchant/v1/last-leg/return-orders/{returnNo}/photos/files/{fileId}/download:
|
||||
get:
|
||||
summary: 客户下载退货签收打托照片
|
||||
|
||||
-1
@@ -85,7 +85,6 @@ class MerchantOverseasCustomerApiContractTest {
|
||||
"cancel", OverseasPermissions.MERCHANT_DISCARD_CANCEL));
|
||||
assertPermissions(MerchantReturnOrderController.class, Map.of(
|
||||
"create", OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE,
|
||||
"update", OverseasPermissions.MERCHANT_RETURN_ORDER_CREATE,
|
||||
"page", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST,
|
||||
"detail", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST,
|
||||
"downloadPhoto", OverseasPermissions.MERCHANT_RETURN_ORDER_LIST,
|
||||
|
||||
+1
-2
@@ -17,8 +17,7 @@ class MerchantRelabelApplicationControllerTest {
|
||||
private final RelabelApplicationService service = mock(RelabelApplicationService.class);
|
||||
private final MerchantRelabelApplicationController controller = controller();
|
||||
private final RelabelRequests.Query query = new RelabelRequests.Query(
|
||||
1, 20, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null);
|
||||
1, 20, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void merchantPageAndDetailUseOnlyAuthenticatedTenant() {
|
||||
|
||||
+1
-2
@@ -17,8 +17,7 @@ class MerchantWarehouseTransferControllerTest {
|
||||
private final WarehouseTransferService service = mock(WarehouseTransferService.class);
|
||||
private final MerchantWarehouseTransferController controller = controller();
|
||||
private final WarehouseTransferRequests.Query query = new WarehouseTransferRequests.Query(
|
||||
1, 20, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null);
|
||||
1, 20, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void merchantPageUsesOnlyAuthenticatedTenant() {
|
||||
|
||||
+7
-61
@@ -120,8 +120,7 @@ class WarehouseTransferServiceTest {
|
||||
when(relabelApplications.selectList(any())).thenReturn(List.of(relabel));
|
||||
|
||||
var result = service.page("TENANT-A", new WarehouseTransferRequests.Query(
|
||||
1, 20, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null));
|
||||
1, 20, null, null, null, null, null, null, null, null));
|
||||
|
||||
assertEquals("RL-20260923-000001", result.records().get(0).relabelNo());
|
||||
assertEquals("COMPLETED", result.records().get(0).relabelApplicationStatus());
|
||||
@@ -388,25 +387,19 @@ class WarehouseTransferServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferAllowsRequestWithoutDestinationForLaterBinding() {
|
||||
void transferRejectsRequestWithoutDestination() {
|
||||
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
|
||||
"IN-1", List.of(11L), "WH-1", null, "123 Pickup Road", 5,
|
||||
new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0),
|
||||
null, null, null, "FBA destination will be supplied later", false, "key-no-fba",
|
||||
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")));
|
||||
|
||||
when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L);
|
||||
when(orders.selectList(any())).thenReturn(List.of(historicalOrder()));
|
||||
when(transfers.findNo(eq("TENANT-A"), anyString())).thenAnswer(invocation -> transfer(WarehouseTransferService.DRAFT));
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.create("TENANT-A", "USER-1", request));
|
||||
|
||||
var result = service.create("TENANT-A", "USER-1", request);
|
||||
|
||||
assertEquals(WarehouseTransferService.DRAFT, result.status());
|
||||
ArgumentCaptor<WarehouseTransferOrder> transfer = ArgumentCaptor.forClass(WarehouseTransferOrder.class);
|
||||
verify(transfers).insert(transfer.capture());
|
||||
assertNull(transfer.getValue().getDestinationType());
|
||||
assertEquals(400, exception.getStatusCode().value());
|
||||
assertEquals("仓储转运必须选择 FBA 仓或私人仓", exception.getReason());
|
||||
verifyNoInteractions(transfers);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -618,53 +611,6 @@ class WarehouseTransferServiceTest {
|
||||
verify(orderDetails).insert(any(LastlegOrderDetail.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantUpdatesRejectedTransferBeforeResubmission() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
transfer.setStatus(WarehouseTransferService.REJECTED);
|
||||
LastlegOrder tailOrder = historicalOrder();
|
||||
tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setCurrentStatus("CANCELLED");
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
var result = service.updateDraft("TENANT-A", "WTR-1", "USER-1", request(false));
|
||||
|
||||
assertEquals(WarehouseTransferService.REJECTED, result.status());
|
||||
assertTrue(tailOrder.getSubOrderStatus().startsWith("WAREHOUSE_TRANSFER:REJECTED"));
|
||||
verify(transfers).updateById(transfer);
|
||||
verify(orders).updateById(tailOrder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantResubmitsRejectedTransferForReview() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
transfer.setStatus(WarehouseTransferService.REJECTED);
|
||||
transfer.setInboundCode("WTR-1");
|
||||
LastlegOrder tailOrder = historicalOrder();
|
||||
tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setCurrentStatus("CANCELLED");
|
||||
tailOrder.setRelabelStatus("NOT_REQUIRED");
|
||||
LastlegInboundOrder inbound = new LastlegInboundOrder();
|
||||
inbound.setId(101L); inbound.setCustomerId("TENANT-A"); inbound.setStatus(WarehouseTransferService.PENDING_REVIEW);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder);
|
||||
when(inboundOrders.selectById(101L)).thenReturn(inbound);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
var result = service.submit("TENANT-A", "WTR-1", "USER-1");
|
||||
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, result.status());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, tailOrder.getCurrentStatus());
|
||||
var history = ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderStatusHistory.class);
|
||||
verify(orderHistories).insert(history.capture());
|
||||
assertEquals(WarehouseTransferService.REJECTED, history.getValue().getFromStatus());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, history.getValue().getToStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantCannotUpdateSubmittedTransfer() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
|
||||
@@ -1369,7 +1369,7 @@ USE baiyue_lastleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_cargo_discard_application (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', outbound_no VARCHAR(64) NULL COMMENT '废弃出库单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID',
|
||||
discard_no VARCHAR(64) NOT NULL COMMENT '废弃申请单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID',
|
||||
overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称快照', inbound_codes VARCHAR(2000) NOT NULL COMMENT '入仓单号集合',
|
||||
total_packages INT NOT NULL COMMENT '总箱数', fee_per_package DECIMAL(18,2) NOT NULL COMMENT '每箱处理费用',
|
||||
total_fee DECIMAL(18,2) NOT NULL COMMENT '费用合计', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种',
|
||||
@@ -1493,11 +1493,7 @@ PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLO
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_request_hash'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_request_hash CHAR(64) NULL COMMENT ''出库请求哈希'' AFTER outbound_idempotency_key'));
|
||||
PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLOCATE PREPARE transfer_inventory_024;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='outbound_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN outbound_at DATETIME(3) NULL COMMENT ''出库时间'' AFTER outbound_request_hash'));
|
||||
PREPARE transfer_outbound_at_stmt FROM @ddl; EXECUTE transfer_outbound_at_stmt; DEALLOCATE PREPARE transfer_outbound_at_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='measured_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN measured_at DATETIME(3) NULL COMMENT ''PDA测量完成时间'' AFTER outbound_at'));
|
||||
PREPARE transfer_measured_at_stmt FROM @ddl; EXECUTE transfer_measured_at_stmt; DEALLOCATE PREPARE transfer_measured_at_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='actual_ship_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN actual_ship_at DATETIME(3) NULL COMMENT ''实际发货时间'' AFTER measured_at'));
|
||||
PREPARE transfer_actual_ship_at_stmt FROM @ddl; EXECUTE transfer_actual_ship_at_stmt; DEALLOCATE PREPARE transfer_actual_ship_at_stmt;
|
||||
PREPARE transfer_inventory_024 FROM @ddl; EXECUTE transfer_inventory_024; DEALLOCATE PREPARE transfer_inventory_024;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_inventory_allocation (
|
||||
id BIGINT NOT NULL COMMENT '雪花主键', transfer_order_id BIGINT NOT NULL COMMENT '转仓单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
@@ -4308,5 +4304,3 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
|
||||
PREPARE dispatch_postal_stmt FROM @ddl; EXECUTE dispatch_postal_stmt; DEALLOCATE PREPARE dispatch_postal_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='postal_code'), 'SELECT 1', 'ALTER TABLE ll_delivery_order ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编'' AFTER delivery_address'));
|
||||
PREPARE delivery_postal_stmt FROM @ddl; EXECUTE delivery_postal_stmt; DEALLOCATE PREPARE delivery_postal_stmt;
|
||||
SET @ddl = IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_cargo_discard_application' AND column_name='outbound_no'), 'ALTER TABLE ll_cargo_discard_application ADD COLUMN outbound_no VARCHAR(64) NULL COMMENT ''废弃出库单号'' AFTER discard_no', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
Reference in New Issue
Block a user