diff --git a/deploy/sql/README.md b/deploy/sql/README.md index 861cb876d..52c301a9b 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -6,7 +6,7 @@ | 区域 | 完整入口 | 服务 schema | 表数 | |---|---|---|---:| -| 国内 | `头程数据库统一增量-MySQL8.sql` | `baiyue_auth`、`baiyue_iam`、`baiyue_order`、`baiyue_warehouse`、`baiyue_firstleg`、`baiyue_file`、`baiyue_audit`、`baiyue_event_bridge`、`baiyue_finance` | 208 | +| 国内 | `头程数据库统一增量-MySQL8.sql` | `baiyue_auth`、`baiyue_iam`、`baiyue_order`、`baiyue_warehouse`、`baiyue_firstleg`、`baiyue_file`、`baiyue_audit`、`baiyue_event_bridge`、`baiyue_finance` | 221 | | 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse`、`baiyue_event_bridge_overseas` | 138 | - 国内和海外是两个部署区域,不是把微服务合并为两个 schema。`baiyue_lastlag_warehouse` 保留当前正式的 `lastlag` 拼写。 @@ -24,7 +24,7 @@ 5. DDL 隐式提交,不可整体回滚。失败后停止并保留错误;经另行授权准备空库后从头执行,不针对半初始化库追加迁移或从失败行续跑。 6. Firstleg 测量项与拆分项互为外键:建空表期间暂设会话 `foreign_key_checks=0`,随后立即恢复 `1`,再插入种子。失败时关闭连接。两边外键完整保留在 CREATE TABLE 内。 -成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=208/138`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。 +成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=221/138`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。 ## 平台管理员密码 diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index c15f04c7f..a03b9c636 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -7891,8 +7891,8 @@ CREATE TABLE fl_sailing ( warehouse_name_snapshot VARCHAR(128) NULL, departure_port_code VARCHAR(32) NOT NULL, destination_port_code VARCHAR(32) NOT NULL, - vessel_name VARCHAR(128) NOT NULL, - voyage_no VARCHAR(64) NOT NULL, + vessel_name VARCHAR(128) NULL, + voyage_no VARCHAR(64) NULL, etd_at DATETIME(3) NOT NULL, warehouse_cutoff_at DATETIME(3) NOT NULL, eta_at DATETIME(3) NULL, @@ -8087,4 +8087,4 @@ CREATE TABLE fl_sailing_booking_link ( CONSTRAINT chk_sailing_booking_released CHECK (booking_status_snapshot='RELEASED') ) ENGINE=InnoDB COMMENT='船期计划与已放舱订舱的最小关联及历史'; -SELECT 'PASS' AS initialization_status, 'CN' AS region, 219 AS initialized_tables; +SELECT 'PASS' AS initialization_status, 'CN' AS region, 221 AS initialized_tables; diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index c75df3757..176fe0178 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -2937,12 +2937,6 @@ CREATE TABLE wh_location ( KEY idx_location_query (customer_id,warehouse_id,status,deleted) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='仓库库位表:按租户和仓库维护库位及承重信息'; -SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='wh_location' AND index_name='idx_location_query'), 'ALTER TABLE wh_location DROP INDEX idx_location_query', 'SELECT 1')); -PREPARE location_zone_index_stmt FROM @ddl; EXECUTE location_zone_index_stmt; DEALLOCATE PREPARE location_zone_index_stmt; -SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='wh_location' AND column_name='zone_code'), 'ALTER TABLE wh_location DROP COLUMN zone_code', 'SELECT 1')); -PREPARE location_zone_column_stmt FROM @ddl; EXECUTE location_zone_column_stmt; DEALLOCATE PREPARE location_zone_column_stmt; - - CREATE TABLE wh_pallet ( id bigint unsigned NOT NULL COMMENT '托盘ID(雪花ID)', customer_id bigint unsigned NOT NULL COMMENT '数据所属租户ID', diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java index 88ec01845..4f44ad03d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/dto/SailingCalendarDtos.java @@ -132,14 +132,14 @@ public final class SailingCalendarDtos { public record SailingData(@Positive Long templateId, @Positive Long channelId, @NotNull @Positive Long warehouseId, @Size(max = 128) String warehouseName, - @NotBlank @Size(max = 64) String channelCode, - @NotBlank @Size(max = 128) String channelName, + @Size(max = 64) String channelCode, + @Size(max = 128) String channelName, @NotBlank @Size(max = 32) String departurePortCode, @Size(max = 128) String departurePortName, @NotBlank @Size(max = 32) String destinationPortCode, @Size(max = 128) String destinationPortName, - @NotBlank @Size(max = 128) String vesselName, - @NotBlank @Size(max = 64) String voyageNo, + @Size(max = 128) String vesselName, + @Size(max = 64) String voyageNo, @NotNull LocalDateTime etdAt, LocalDateTime etaAt, @NotNull LocalDateTime cutoffAt, @@ -151,14 +151,14 @@ public final class SailingCalendarDtos { @Positive Long channelId, @NotNull @Positive Long warehouseId, @Size(max = 128) String warehouseName, - @NotBlank @Size(max = 64) String channelCode, - @NotBlank @Size(max = 128) String channelName, + @Size(max = 64) String channelCode, + @Size(max = 128) String channelName, @NotBlank @Size(max = 32) String departurePortCode, @Size(max = 128) String departurePortName, @NotBlank @Size(max = 32) String destinationPortCode, @Size(max = 128) String destinationPortName, - @NotBlank @Size(max = 128) String vesselName, - @NotBlank @Size(max = 64) String voyageNo, + @Size(max = 128) String vesselName, + @Size(max = 64) String voyageNo, @NotNull LocalDateTime etdAt, LocalDateTime etaAt, @NotNull LocalDateTime cutoffAt, @@ -174,14 +174,14 @@ public final class SailingCalendarDtos { @Positive Long channelId, @NotNull @Positive Long warehouseId, @Size(max = 128) String warehouseName, - @NotBlank @Size(max = 64) String channelCode, - @NotBlank @Size(max = 128) String channelName, + @Size(max = 64) String channelCode, + @Size(max = 128) String channelName, @NotBlank @Size(max = 32) String departurePortCode, @Size(max = 128) String departurePortName, @NotBlank @Size(max = 32) String destinationPortCode, @Size(max = 128) String destinationPortName, - @NotBlank @Size(max = 128) String vesselName, - @NotBlank @Size(max = 64) String voyageNo, + @Size(max = 128) String vesselName, + @Size(max = 64) String voyageNo, @NotNull LocalDateTime etdAt, LocalDateTime etaAt, @NotNull LocalDateTime cutoffAt, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingCalendarDisplayMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingCalendarDisplayMapper.java index c16e5d571..b18539354 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingCalendarDisplayMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingCalendarDisplayMapper.java @@ -33,9 +33,10 @@ public interface SailingCalendarDisplayMapper { AND b.channel_code = #{channelCode} AND b.departure_port_code = #{departurePortCode} AND b.destination_port_code = #{destinationPortCode} - AND b.vessel_name = #{vesselName} - AND b.voyage_no = #{voyageNo} - AND (b.estimated_sailing_at IS NULL OR DATE(b.estimated_sailing_at) = DATE(#{etdAt})) + AND (#{vesselName} IS NOT NULL AND #{voyageNo} IS NOT NULL + AND b.vessel_name = #{vesselName} + AND b.voyage_no = #{voyageNo} + AND (b.estimated_sailing_at IS NULL OR DATE(b.estimated_sailing_at) = DATE(#{etdAt}))) ) ) """) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingMapper.java index 59e5b96f1..766b44c24 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/mapper/SailingMapper.java @@ -99,6 +99,12 @@ public interface SailingMapper extends BaseMapper { int updateSailing(@Param("value") SailingEntity value, @Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); + @Update("UPDATE fl_sailing SET vessel_name=#{vesselName},voyage_no=#{voyageNo},etd_at=COALESCE(#{etdAt},etd_at),eta_at=#{etaAt},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1 WHERE id=#{id} AND version=#{expectedVersion}") + int updateBookingDetails(@Param("id") long id, @Param("expectedVersion") int expectedVersion, + @Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo, + @Param("etdAt") java.time.LocalDateTime etdAt, @Param("etaAt") java.time.LocalDateTime etaAt, + @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); + @Update("UPDATE fl_sailing SET status=#{status},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1 WHERE id=#{id} AND version=#{expectedVersion}") int updateStatus(@Param("id") long id, @Param("status") String status, @Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/repository/SailingCalendarRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/repository/SailingCalendarRepository.java index e9947a9e3..7be9277a6 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/repository/SailingCalendarRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/repository/SailingCalendarRepository.java @@ -95,6 +95,9 @@ public class SailingCalendarRepository { public int update(SailingEntity value, int version, long operatorId, String operatorName) { return sailings.updateSailing(value, version, operatorId, operatorName); } + public int updateBookingDetails(long id, int version, String vesselName, String voyageNo, LocalDateTime etdAt, LocalDateTime etaAt, long operatorId, String operatorName) { + return sailings.updateBookingDetails(id, version, vesselName, voyageNo, etdAt, etaAt, operatorId, operatorName); + } public int updateStatus(long id, String status, int version, long operatorId, String operatorName) { return sailings.updateStatus(id, status, version, operatorId, operatorName); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java index 53db7fd26..5b3450f46 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/sailingcalendar/service/SailingCalendarService.java @@ -349,6 +349,9 @@ public class SailingCalendarService { if (!Objects.equals(sailing.getWarehouseId(), booking.getWarehouseId()) || !Objects.equals(sailing.getChannelCodeSnapshot(), booking.getChannelCode()) || !Objects.equals(sailing.getDeparturePortCode(), booking.getDeparturePortCode()) || !Objects.equals(sailing.getDestinationPortCode(), booking.getDestinationPortCode())) throw conflict("订舱的仓库、渠道或港口与船期不一致"); SailingBookingLinkEntity value = new SailingBookingLinkEntity(); value.setId(IdWorker.getId()); value.setTenantId(booking.getCustomerId() == null ? 0L : booking.getCustomerId()); value.setSailingId(id); value.setOceanBookingId(request.oceanBookingId()); value.setLinkStatus("ACTIVE"); value.setActiveBookingGuard(1); value.setBookingStatusSnapshot(booking.getStatus()); value.setWarehouseId(booking.getWarehouseId()); value.setChannelCode(booking.getChannelCode()); value.setDeparturePortCode(booking.getDeparturePortCode()); value.setDestinationPortCode(booking.getDestinationPortCode()); value.setLinkedAt(LocalDateTime.now(ZONE)); value.setOperatorId(operatorId); value.setOperatorName(operatorName); repository.insertBookingLink(value); + if (StringUtils.hasText(booking.getVesselName()) || StringUtils.hasText(booking.getVoyageNo())) { + if (repository.updateBookingDetails(id, sailing.getVersion(), booking.getVesselName(), booking.getVoyageNo(), booking.getEstimatedSailingAt(), booking.getEstimatedArrivalAt(), operatorId, operatorName) != 1) throw conflict("船期已被其他人修改"); + } int booked = repository.requirements(booking.getId()).stream().mapToInt(v -> v.getQuantity() == null ? 0 : v.getQuantity()).sum(); return Map.of("id", value.getId(), "sailingId", id, "oceanBookingId", request.oceanBookingId(), "status", value.getLinkStatus(), "bookedContainerCount", booked); } @@ -449,7 +452,7 @@ public class SailingCalendarService { int mask = d.weekdays() == null || d.weekdays().isEmpty() ? 1 : d.weekdays().stream().mapToInt(day -> 1 << (day - 1)).reduce(0, (a, b) -> a | b); v.setWeekday(Integer.numberOfTrailingZeros(mask) + 1); v.setWeekdayMask(mask); v.setEtdLocalTime(d.departureTime()); v.setCutoffDays(d.cutoffDays()); v.setCutoffLocalTime(d.cutoffLocalTime()); v.setValidFrom(d.effectiveFrom()); v.setValidTo(d.effectiveTo()); v.setVoyageRule(trim(d.voyageRule())); v.setVesselName(trim(d.vesselName())); v.setRemark(trim(d.remark())); } - private static void copy(SailingEntity v, SailingData d, SailingChannelEntity c) { v.setTemplateId(d.templateId()); v.setChannelId(c.getId()); v.setChannelCodeSnapshot(c.getChannelCode()); v.setChannelNameSnapshot(c.getChannelName()); v.setWarehouseId(d.warehouseId()); v.setWarehouseNameSnapshot(trim(d.warehouseName())); v.setDeparturePortCode(d.departurePortCode().trim().toUpperCase()); v.setDestinationPortCode(d.destinationPortCode().trim().toUpperCase()); v.setVesselName(d.vesselName().trim()); v.setVoyageNo(d.voyageNo().trim()); v.setEtdAt(d.etdAt()); v.setEtaAt(d.etaAt()); v.setWarehouseCutoffAt(d.cutoffAt()); v.setPlannedCapacityCbm(d.plannedCapacityCbm()); v.setRemark(trim(d.remark())); } + private static void copy(SailingEntity v, SailingData d, SailingChannelEntity c) { v.setTemplateId(d.templateId()); v.setChannelId(c.getId()); v.setChannelCodeSnapshot(c.getChannelCode()); v.setChannelNameSnapshot(c.getChannelName()); v.setWarehouseId(d.warehouseId()); v.setWarehouseNameSnapshot(trim(d.warehouseName())); v.setDeparturePortCode(d.departurePortCode().trim().toUpperCase()); v.setDestinationPortCode(d.destinationPortCode().trim().toUpperCase()); v.setVesselName(trim(d.vesselName())); v.setVoyageNo(trim(d.voyageNo())); v.setEtdAt(d.etdAt()); v.setEtaAt(d.etaAt()); v.setWarehouseCutoffAt(d.cutoffAt()); v.setPlannedCapacityCbm(d.plannedCapacityCbm()); v.setRemark(trim(d.remark())); } private static SailingAssignmentEntity copyAssignment(SailingAssignmentEntity source) { SailingAssignmentEntity target = new SailingAssignmentEntity(); target.setTenantId(source.getTenantId()); target.setOriginSailingId(source.getOriginSailingId()); target.setInboundId(source.getInboundId()); target.setInboundCode(source.getInboundCode()); target.setReceiptBatchId(source.getReceiptBatchId()); target.setForecastId(source.getForecastId()); target.setVolumeCbm(source.getVolumeCbm()); target.setForecastVolumeCbm(source.getForecastVolumeCbm()); target.setRouteSource(source.getRouteSource()); target.setVolumeSource(source.getVolumeSource()); target.setRuleVersion(source.getRuleVersion()); return target; } private static void validateChannel(ChannelData d) { if (!List.of("CATALOG", "SCHEDULE_ONLY").contains(d.sourceType().trim().toUpperCase())) throw bad("sourceType仅支持CATALOG或SCHEDULE_ONLY"); } private static void validateTemplate(TemplateData d, SailingChannelEntity channel) { @@ -470,7 +473,7 @@ 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()); SailingChannelEntity v = repository.channelByCode(d.channelCode().trim().toUpperCase()); if (v == null) throw notFound("渠道不存在"); return v; } + 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 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; } diff --git a/firstleg/src/main/resources/db/phase4-sailing-calendar-schema.sql b/firstleg/src/main/resources/db/phase4-sailing-calendar-schema.sql index f923420cc..828bd262f 100644 --- a/firstleg/src/main/resources/db/phase4-sailing-calendar-schema.sql +++ b/firstleg/src/main/resources/db/phase4-sailing-calendar-schema.sql @@ -72,8 +72,8 @@ CREATE TABLE fl_sailing ( warehouse_name_snapshot VARCHAR(128) NULL, departure_port_code VARCHAR(32) NOT NULL, destination_port_code VARCHAR(32) NOT NULL, - vessel_name VARCHAR(128) NOT NULL, - voyage_no VARCHAR(64) NOT NULL, + vessel_name VARCHAR(128) NULL, + voyage_no VARCHAR(64) NULL, etd_at DATETIME(3) NOT NULL, warehouse_cutoff_at DATETIME(3) NOT NULL, eta_at DATETIME(3) NULL, diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java index c097d4b10..9482eb047 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java @@ -73,7 +73,7 @@ class Phase3FinalUnifiedSqlContractTest { @Test void initializationPassIsUniqueAndFinal() throws Exception { String sql = sql(); - String pass = "SELECT 'PASS' AS initialization_status, 'CN' AS region, 219 AS initialized_tables;"; + String pass = "SELECT 'PASS' AS initialization_status, 'CN' AS region, 221 AS initialized_tables;"; assertThat(count(sql, pass)).isEqualTo(1); assertThat(sql.trim()).endsWith(pass); assertThat(sql) diff --git a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs index 25fa45816..ddb5c3e66 100644 --- a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs +++ b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs @@ -9,7 +9,7 @@ const domesticSchemas = ['baiyue_auth', 'baiyue_iam', 'baiyue_order', 'baiyue_wa const overseasSchemas = ['baiyue_event_bridge_overseas', 'baiyue_lastleg', 'baiyue_lastlag_warehouse'] test('两个入口分别只操作国内九库与海外三库,包括限定名引用', () => { - for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 219], [overseas, overseasSchemas, domesticSchemas, 138]]) { + for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 221], [overseas, overseasSchemas, domesticSchemas, 138]]) { assert.deepEqual([...sql.matchAll(/^CREATE DATABASE IF NOT EXISTS (\w+)/gm)].map(m => m[1]), allowed) assert.deepEqual([...new Set([...sql.matchAll(/^USE (\w+);/gm)].map(m => m[1]))], allowed) const body = sql.replace(/^--.*$/gm, '') @@ -28,7 +28,7 @@ test('纯初始化禁止迁移、数据回填、清理、重复建表和提前 assert.doesNotMatch(body.replace(/^CREATE TRIGGER [\s\S]*?\$\$/gm, ''), /^\s*(?:UPDATE|DELETE FROM|REPLACE INTO)\b/gmi) assert.doesNotMatch(body, /CREATE TABLE IF NOT EXISTS/i) assert.equal((body.match(/AS initialization_status/g) ?? []).length, 1) - assert.match(body.trim(), /SELECT 'PASS' AS initialization_status, '(CN|OVERSEAS)' AS region, (219|138) AS initialized_tables;$/) + assert.match(body.trim(), /SELECT 'PASS' AS initialization_status, '(CN|OVERSEAS)' AS region, (221|138) AS initialized_tables;$/) } })