发布当前测试版本至临时灰度环境 #19

Merged
admin merged 177 commits from test into main 2026-10-07 11:04:53 +08:00
7 changed files with 78 additions and 27 deletions
Showing only changes of commit ed36c43af4 - Show all commits
@@ -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
@@ -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);
}
@@ -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;
@@ -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);
@@ -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);
@@ -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,
@@ -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) {